diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 571b10d..aa803f6 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -4,11 +4,24 @@ "features": { "ghcr.io/devcontainers/features/node:1": { // Should match what is defined in ui/package.json - "version": "21.1.0" + "version": "22.15.0" } }, "mounts": [ "source=${localEnv:HOME}/.ssh,target=/home/vscode/.ssh,type=bind,consistency=cached" - ] + ], + "customizations": { + "vscode": { + "extensions": [ + "bradlc.vscode-tailwindcss", + "GitHub.vscode-pull-request-github", + "dbaeumer.vscode-eslint", + "golang.go", + "ms-vscode.makefile-tools", + "esbenp.prettier-vscode", + "github.vscode-github-actions" + ] + } + } } diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml new file mode 100644 index 0000000..fd3be8a --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug.yml @@ -0,0 +1,76 @@ +--- +name: Bug report +description: 🐛 Let us know about an unexpected error, a crash, or an unexpected behavior. +type: 'Bug' +labels: + - 'type: bug' +body: + - type: checkboxes + attributes: + label: Disclaimer + description: | + For support questions, please use the [discussions][] or [Discord][] instead. Before + opening a bug report, ensure you have read the [documentation][], + [Troubleshooting][] and [Device FAQs][]. Only use bug reports for actual + bugs. + + [documentation]: https://jetkvm.com/docs + [Troubleshooting]: https://jetkvm.com/docs/getting-started/troubleshooting + [Device FAQs]: https://jetkvm.com/docs/getting-started/faq + [discussions]: https://github.com/jetkvm/kvm/discussions + [Discord]: https://jetkvm.com/discord + options: + - label: I have read and understood the disclaimer. + required: true + - type: input + attributes: + label: Application version + description: | + Provide the application version (can be found in General settings) + validations: + required: true + - type: input + attributes: + label: System version + description: | + Provide the system version (can be found in General settings) + validations: + required: true + - type: dropdown + attributes: + label: Device model + description: Provide the device model + options: + - JetKVM + - JetKVM (POE) + validations: + required: false + - type: dropdown + attributes: + label: Extension model + description: Provide the extension model (if the bug is related to the extension) + options: + - ATX Power Control + - DC Power Control + - Serial Console + validations: + required: false + - type: input + attributes: + label: Remote device Hardware + description: If the bug is related to a remote device, please provide its hardware information e.g. Raspberry Pi 5 + validations: + required: false + - type: input + attributes: + label: Remote device OS + description: If the bug is related to a remote device, please provide its OS information as detailed as possible e.g. Debian 12. + validations: + required: false + - type: textarea + attributes: + label: Bug description + description: | + Provide a description of the problem: steps to reproduce it, what you are expecting and what you got. + validations: + required: true \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..6b45cd1 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,10 @@ +blank_issues_enabled: true + +contact_links: + - name: Hardware Issues + url: https://jetkvm.com/contact + about: If your hardware is not powering on or is not working, please contact us. + + - name: Discord + url: https://jetkvm.com/discord + about: Engage with the JetKVM team and other community members. \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/feature.yml b/.github/ISSUE_TEMPLATE/feature.yml new file mode 100644 index 0000000..9f105cb --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature.yml @@ -0,0 +1,46 @@ +name: Feature +type: 'Feature' +description: 🚀 Request a new feature. +labels: +- 'type: feature' +body: + - type: textarea + attributes: + label: A note for the community + value: | + > [!NOTE] + > Please vote on this issue by adding a 👍 [reaction](https://blog.github.com/2016-03-10-add-reactions-to-pull-requests-issues-and-comments/) to the original issue to help the community and maintainers prioritize this request. + validations: + required: true + - type: checkboxes + attributes: + label: Disclaimer + description: | + Before requesting a feature, check it does not already exist in the [documentation](https://jetkvm.com/docs) or our [roadmap](https://jetkvm.com/roadmap). + You are quite welcome opening a feature request before spending time to implement it yourself. + options: + - label: I have read and understood the disclaimer. + required: true + - label: I plan to implement the feature myself. + - type: dropdown + attributes: + label: Subsystem + description: Provide the subsystem of the feature you request, you can choose multiple if you think it fits in multiple areas. + options: + - Hardware + - Device Compatibility + - Keyboard + - Mouse + - Power + - UI: Screen + - UI: Application + - UI: Cloud + validations: + required: false + - type: textarea + attributes: + label: Feature description + description: | + Provide a description of the feature you request. + validations: + required: true \ No newline at end of file diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..cc36cf7 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,17 @@ +version: 2 +updates: + - package-ecosystem: gomod + directory: / + schedule: + interval: monthly + open-pull-requests-limit: 10 + - package-ecosystem: github-actions + directory: / + schedule: + interval: monthly + open-pull-requests-limit: 10 + - package-ecosystem: npm + directory: /ui + open-pull-requests-limit: 10 + schedule: + interval: monthly diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 84bc4b1..59053da 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -10,133 +10,42 @@ on: jobs: build: - runs-on: buildjet-4vcpu-ubuntu-2204 + runs-on: ubuntu-latest name: Build + if: github.event_name != 'pull_request_review' || github.event.review.state == 'approved' steps: - name: Checkout uses: actions/checkout@v4 - name: Set up Node.js uses: actions/setup-node@v4 with: - node-version: v21.1.0 + node-version: "22" cache: "npm" cache-dependency-path: "**/package-lock.json" - name: Set up Golang - uses: actions/setup-go@v4 + uses: actions/setup-go@v5.5.0 with: - go-version: "1.24.0" + go-version: "1.24.4" - name: Build frontend run: | make frontend - name: Build application run: | make build_dev + - name: Run tests + run: | + go test ./... -json > testreport.json + - name: Make test cases + run: | + make build_dev_test + - name: Golang Test Report + uses: becheran/go-testreport@v0.3.2 + with: + input: "testreport.json" - name: Upload artifact uses: actions/upload-artifact@v4 with: name: jetkvm-app - path: bin/jetkvm_app - deploy_and_test: - runs-on: buildjet-4vcpu-ubuntu-2204 - name: Smoke test - needs: build - concurrency: - group: smoketest-jk - steps: - - name: Download artifact - uses: actions/download-artifact@v4 - with: - name: jetkvm-app - - name: Configure WireGuard and check connectivity - run: | - WG_KEY_FILE=$(mktemp) - echo -n "$CI_WG_PRIVATE" > $WG_KEY_FILE && \ - sudo apt-get update && sudo apt-get install -y wireguard-tools && \ - sudo ip link add dev wg-ci type wireguard && \ - sudo ip addr add $CI_WG_IPS dev wg-ci && \ - sudo wg set wg-ci listen-port 51820 \ - private-key $WG_KEY_FILE \ - peer $CI_WG_PUBLIC \ - allowed-ips $CI_WG_ALLOWED_IPS \ - endpoint $CI_WG_ENDPOINT \ - persistent-keepalive 15 && \ - sudo ip link set up dev wg-ci && \ - sudo ip r r $CI_HOST via $CI_WG_GATEWAY dev wg-ci - ping -c1 $CI_HOST || (echo "Failed to ping $CI_HOST" && sudo wg show wg-ci && ip r && exit 1) - env: - CI_HOST: ${{ vars.JETKVM_CI_HOST }} - CI_WG_IPS: ${{ vars.JETKVM_CI_WG_IPS }} - CI_WG_GATEWAY: ${{ vars.JETKVM_CI_GATEWAY }} - CI_WG_ALLOWED_IPS: ${{ vars.JETKVM_CI_WG_ALLOWED_IPS }} - CI_WG_PUBLIC: ${{ secrets.JETKVM_CI_WG_PUBLIC }} - CI_WG_PRIVATE: ${{ secrets.JETKVM_CI_WG_PRIVATE }} - CI_WG_ENDPOINT: ${{ secrets.JETKVM_CI_WG_ENDPOINT }} - - name: Configure SSH - run: | - # Write SSH private key to a file - SSH_PRIVATE_KEY=$(mktemp) - echo "$CI_SSH_PRIVATE" > $SSH_PRIVATE_KEY - chmod 0600 $SSH_PRIVATE_KEY - # Configure SSH - mkdir -p ~/.ssh - cat <> ~/.ssh/config - Host jkci - HostName $CI_HOST - User $CI_USER - StrictHostKeyChecking no - UserKnownHostsFile /dev/null - IdentityFile $SSH_PRIVATE_KEY - EOF - env: - CI_USER: ${{ vars.JETKVM_CI_USER }} - CI_HOST: ${{ vars.JETKVM_CI_HOST }} - CI_SSH_PRIVATE: ${{ secrets.JETKVM_CI_SSH_PRIVATE }} - - name: Deploy application - run: | - set -e - # Copy the binary to the remote host - echo "+ Copying the application to the remote host" - cat jetkvm_app | gzip | ssh jkci "cat > /userdata/jetkvm/jetkvm_app.update.gz" - # Deploy and run the application on the remote host - echo "+ Deploying the application on the remote host" - ssh jkci ash < /proc/sys/vm/drop_caches - # Reboot the application - reboot -d 5 -f & - EOF - sleep 10 - echo "Deployment complete, waiting for JetKVM to come back online " - function check_online() { - for i in {1..60}; do - if ping -c1 -w1 -W1 -q $CI_HOST >/dev/null; then - echo "JetKVM is back online" - return 0 - fi - echo -n "." - sleep 1 - done - echo "JetKVM did not come back online within 60 seconds" - return 1 - } - check_online - env: - CI_HOST: ${{ vars.JETKVM_CI_HOST }} - - name: Run smoke tests - run: | - echo "+ Checking the status of the device" - curl -v http://$CI_HOST/device/status && echo - echo "+ Collecting logs" - ssh jkci "cat /userdata/jetkvm/last.log" > last.log - cat last.log - env: - CI_HOST: ${{ vars.JETKVM_CI_HOST }} - - name: Upload logs - uses: actions/upload-artifact@v4 - with: - name: device-logs - path: last.log + path: | + bin/jetkvm_app + device-tests.tar.gz \ No newline at end of file diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml index 7ec9229..fa1fe22 100644 --- a/.github/workflows/golangci-lint.yml +++ b/.github/workflows/golangci-lint.yml @@ -24,14 +24,14 @@ jobs: - name: Checkout repository uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Install Go - uses: actions/setup-go@41dfa10bad2bb2ae585af6ee5bb4d7d973ad74ed # v5.1.0 + uses: actions/setup-go@fa96338abe5531f6e34c5cc0bbe28c1a533d5505 # v4.2.1 with: - go-version: 1.23.x + go-version: 1.24.4 - name: Create empty resource directory run: | mkdir -p static && touch static/.gitkeep - name: Lint - uses: golangci/golangci-lint-action@971e284b6050e8a5849b72094c50ab08da042db8 # v6.1.1 + uses: golangci/golangci-lint-action@1481404843c368bc19ca9406f87d6e0fc97bdcfd # v7.0.0 with: args: --verbose - version: v1.62.0 + version: v2.0.2 diff --git a/.github/workflows/smoketest.yml b/.github/workflows/smoketest.yml new file mode 100644 index 0000000..41aa07e --- /dev/null +++ b/.github/workflows/smoketest.yml @@ -0,0 +1,174 @@ +name: smoketest +on: + repository_dispatch: + types: [smoketest] + +jobs: + ghbot_payload: + name: Ghbot payload + runs-on: ubuntu-latest + steps: + - name: "GH_CHECK_RUN_ID=${{ github.event.client_payload.check_run_id }}" + run: | + echo "== START GHBOT_PAYLOAD ==" + cat <<'GHPAYLOAD_EOF' | base64 + ${{ toJson(github.event.client_payload) }} + GHPAYLOAD_EOF + echo "== END GHBOT_PAYLOAD ==" + deploy_and_test: + runs-on: buildjet-4vcpu-ubuntu-2204 + name: Smoke test + concurrency: + group: smoketest-jk + steps: + - name: Download artifact + run: | + wget -O /tmp/jk.zip "${{ github.event.client_payload.artifact_download_url }}" + unzip /tmp/jk.zip + - name: Configure WireGuard and check connectivity + run: | + WG_KEY_FILE=$(mktemp) + echo -n "$CI_WG_PRIVATE" > $WG_KEY_FILE && \ + sudo apt-get update && sudo apt-get install -y wireguard-tools && \ + sudo ip link add dev wg-ci type wireguard && \ + sudo ip addr add $CI_WG_IPS dev wg-ci && \ + sudo wg set wg-ci listen-port 51820 \ + private-key $WG_KEY_FILE \ + peer $CI_WG_PUBLIC \ + allowed-ips $CI_WG_ALLOWED_IPS \ + endpoint $CI_WG_ENDPOINT \ + persistent-keepalive 15 && \ + sudo ip link set up dev wg-ci && \ + sudo ip r r $CI_HOST via $CI_WG_GATEWAY dev wg-ci + ping -c1 $CI_HOST || (echo "Failed to ping $CI_HOST" && sudo wg show wg-ci && ip r && exit 1) + env: + CI_HOST: ${{ vars.JETKVM_CI_HOST }} + CI_WG_IPS: ${{ vars.JETKVM_CI_WG_IPS }} + CI_WG_GATEWAY: ${{ vars.JETKVM_CI_GATEWAY }} + CI_WG_ALLOWED_IPS: ${{ vars.JETKVM_CI_WG_ALLOWED_IPS }} + CI_WG_PUBLIC: ${{ secrets.JETKVM_CI_WG_PUBLIC }} + CI_WG_PRIVATE: ${{ secrets.JETKVM_CI_WG_PRIVATE }} + CI_WG_ENDPOINT: ${{ secrets.JETKVM_CI_WG_ENDPOINT }} + - name: Configure SSH + run: | + # Write SSH private key to a file + SSH_PRIVATE_KEY=$(mktemp) + echo "$CI_SSH_PRIVATE" > $SSH_PRIVATE_KEY + chmod 0600 $SSH_PRIVATE_KEY + # Configure SSH + mkdir -p ~/.ssh + cat <> ~/.ssh/config + Host jkci + HostName $CI_HOST + User $CI_USER + StrictHostKeyChecking no + UserKnownHostsFile /dev/null + IdentityFile $SSH_PRIVATE_KEY + EOF + env: + CI_USER: ${{ vars.JETKVM_CI_USER }} + CI_HOST: ${{ vars.JETKVM_CI_HOST }} + CI_SSH_PRIVATE: ${{ secrets.JETKVM_CI_SSH_PRIVATE }} + - name: Run tests + run: | + set -e + echo "+ Copying device-tests.tar.gz to remote host" + ssh jkci "cat > /tmp/device-tests.tar.gz" < device-tests.tar.gz + echo "+ Running go tests" + ssh jkci ash << 'EOF' + set -e + TMP_DIR=$(mktemp -d) + cd ${TMP_DIR} + tar zxf /tmp/device-tests.tar.gz + ./gotestsum --format=testdox \ + --jsonfile=/tmp/device-tests.json \ + --post-run-command 'sh -c "echo $TESTS_FAILED > /tmp/device-tests.failed"' \ + --raw-command -- ./run_all_tests -json + + GOTESTSUM_EXIT_CODE=$? + if [ $GOTESTSUM_EXIT_CODE -ne 0 ]; then + echo "❌ Tests failed (exit code: $GOTESTSUM_EXIT_CODE)" + rm -rf ${TMP_DIR} /tmp/device-tests.tar.gz + exit 1 + fi + + TESTS_FAILED=$(cat /tmp/device-tests.failed) + if [ "$TESTS_FAILED" -ne 0 ]; then + echo "❌ Tests failed $TESTS_FAILED tests failed" + rm -rf ${TMP_DIR} /tmp/device-tests.tar.gz + exit 1 + fi + + echo "✅ Tests passed" + rm -rf ${TMP_DIR} /tmp/device-tests.tar.gz + EOF + ssh jkci "cat /tmp/device-tests.json" > device-tests.json + - name: Set up Golang + uses: actions/setup-go@v5.5.0 + with: + go-version: "1.24.4" + - name: Golang Test Report + uses: becheran/go-testreport@v0.3.2 + with: + input: "device-tests.json" + - name: Deploy application + run: | + set -e + # Copy the binary to the remote host + echo "+ Copying the application to the remote host" + cat bin/jetkvm_app | gzip | ssh jkci "cat > /userdata/jetkvm/jetkvm_app.update.gz" + # Deploy and run the application on the remote host + echo "+ Deploying the application on the remote host" + ssh jkci ash < /proc/sys/vm/drop_caches + # Reboot the application + reboot -d 5 -f & + EOF + sleep 10 + echo "Deployment complete, waiting for JetKVM to come back online " + function check_online() { + for i in {1..60}; do + if ping -c1 -w1 -W1 -q $CI_HOST >/dev/null; then + echo "JetKVM is back online" + return 0 + fi + echo -n "." + sleep 1 + done + echo "JetKVM did not come back online within 60 seconds" + return 1 + } + check_online + env: + CI_HOST: ${{ vars.JETKVM_CI_HOST }} + - name: Run smoke tests + run: | + echo "+ Checking the status of the device" + curl -v http://$CI_HOST/device/status && echo + echo "+ Waiting for 15 seconds to allow all services to start" + sleep 15 + echo "+ Collecting logs" + local_log_tar=$(mktemp) + ssh jkci ash > $local_log_tar <<'EOF' + log_path=$(mktemp -d) + dmesg > $log_path/dmesg.log + cp /userdata/jetkvm/last.log $log_path/last.log + tar -czf - -C $log_path . + EOF + tar -xf $local_log_tar + cat dmesg.log last.log + env: + CI_HOST: ${{ vars.JETKVM_CI_HOST }} + - name: Upload logs + uses: actions/upload-artifact@v4 + with: + name: device-logs + path: | + last.log + dmesg.log + device-tests.json diff --git a/.github/workflows/ui-lint.yml b/.github/workflows/ui-lint.yml index 492a5fe..ad002fc 100644 --- a/.github/workflows/ui-lint.yml +++ b/.github/workflows/ui-lint.yml @@ -14,16 +14,16 @@ permissions: jobs: ui-lint: name: UI Lint - runs-on: buildjet-4vcpu-ubuntu-2204 + runs-on: ubuntu-latest steps: - name: Checkout repository uses: actions/checkout@v4 - name: Set up Node.js uses: actions/setup-node@v4 with: - node-version: v21.1.0 + node-version: "22" cache: "npm" - cache-dependency-path: "ui/package-lock.json" + cache-dependency-path: "**/package-lock.json" - name: Install dependencies run: | cd ui diff --git a/.gitignore b/.gitignore index 740f554..f37d922 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ bin/* static/* .idea +.DS_Store + +device-tests.tar.gz \ No newline at end of file diff --git a/.golangci.yml b/.golangci.yml index ddf4443..dd8a079 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -1,12 +1,44 @@ ---- +version: "2" linters: enable: - # - goimports - # - misspell - # - revive - -issues: - exclude-rules: - - path: _test.go - linters: - - errcheck + - forbidigo + - misspell + - whitespace + - gochecknoinits + settings: + forbidigo: + forbid: + - pattern: ^fmt\.Print.*$ + msg: Do not commit print statements. Use logger package. + - pattern: ^log\.(Fatal|Panic|Print)(f|ln)?.*$ + msg: Do not commit log statements. Use logger package. + exclusions: + generated: lax + presets: + - comments + - common-false-positives + - legacy + - std-error-handling + rules: + - linters: + - errcheck + path: _test.go + - linters: + - forbidigo + path: cmd/main.go + - linters: + - gochecknoinits + path: internal/logging/sse.go + paths: + - third_party$ + - builtin$ + - examples$ +formatters: + enable: + - goimports + exclusions: + generated: lax + paths: + - third_party$ + - builtin$ + - examples$ diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..de91a5d --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "tailwindCSS.classFunctions": ["cva", "cx"] +} \ No newline at end of file diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md new file mode 100644 index 0000000..d95db77 --- /dev/null +++ b/DEVELOPMENT.md @@ -0,0 +1,355 @@ +
+ JetKVM logo + +### Development Guide + +[Discord](https://jetkvm.com/discord) | [Website](https://jetkvm.com) | [Issues](https://github.com/jetkvm/cloud-api/issues) | [Docs](https://jetkvm.com/docs) + +[![Twitter](https://img.shields.io/twitter/url/https/twitter.com/jetkvm.svg?style=social&label=Follow%20%40JetKVM)](https://twitter.com/jetkvm) + +[![Go Report Card](https://goreportcard.com/badge/github.com/jetkvm/kvm)](https://goreportcard.com/report/github.com/jetkvm/kvm) + +
+ +# JetKVM Development Guide + +Welcome to JetKVM development! This guide will help you get started quickly, whether you're fixing bugs, adding features, or just exploring the codebase. + +## Get Started + +### Prerequisites +- **A JetKVM device** (for full development) +- **[Go 1.24.4+](https://go.dev/doc/install)** and **[Node.js 22.15.0](https://nodejs.org/en/download/)** +- **[Git](https://git-scm.com/downloads)** for version control +- **[SSH access](https://jetkvm.com/docs/advanced-usage/developing#developer-mode)** to your JetKVM device + +### Development Environment + +**Recommended:** Development is best done on **Linux** or **macOS**. + +If you're using Windows, we strongly recommend using **WSL (Windows Subsystem for Linux)** for the best development experience: +- [Install WSL on Windows](https://docs.microsoft.com/en-us/windows/wsl/install) +- [WSL Setup Guide](https://docs.microsoft.com/en-us/windows/wsl/setup/environment) + +This ensures compatibility with shell scripts and build tools used in the project. + +### Project Setup + +1. **Clone the repository:** + ```bash + git clone https://github.com/jetkvm/kvm.git + cd kvm + ``` + +2. **Check your tools:** + ```bash + go version && node --version + ``` + +3. **Find your JetKVM IP address** (check your router or device screen) + +4. **Deploy and test:** + ```bash + ./dev_deploy.sh -r 192.168.1.100 # Replace with your device IP + ``` + +5. **Open in browser:** `http://192.168.1.100` + +That's it! You're now running your own development version of JetKVM. + +--- + +## Common Tasks + +### Modify the UI + +```bash +cd ui +npm install +./dev_device.sh 192.168.1.100 # Replace with your device IP +``` + +Now edit files in `ui/src/` and see changes live in your browser! + +### Modify the backend + +```bash +# Edit Go files (config.go, web.go, etc.) +./dev_deploy.sh -r 192.168.1.100 --skip-ui-build +``` + +### Run tests + +```bash +./dev_deploy.sh -r 192.168.1.100 --run-go-tests +``` + +### View logs + +```bash +ssh root@192.168.1.100 +tail -f /var/log/jetkvm.log +``` + +--- + +## Project Layout + +``` +/kvm/ +├── main.go # App entry point +├── config.go # Settings & configuration +├── web.go # API endpoints +├── ui/ # React frontend +│ ├── src/routes/ # Pages (login, settings, etc.) +│ └── src/components/ # UI components +└── internal/ # Internal Go packages +``` + +**Key files for beginners:** + +- `web.go` - Add new API endpoints here +- `config.go` - Add new settings here +- `ui/src/routes/` - Add new pages here +- `ui/src/components/` - Add new UI components here + +--- + +## Development Modes + +### Full Development (Recommended) + +*Best for: Complete feature development* + +```bash +# Deploy everything to your JetKVM device +./dev_deploy.sh -r +``` + +### Frontend Only + +*Best for: UI changes without device* + +```bash +cd ui +npm install +./dev_device.sh +``` + +### Quick Backend Changes + +*Best for: API or backend logic changes* + +```bash +# Skip frontend build for faster deployment +./dev_deploy.sh -r --skip-ui-build +``` + +--- + +## Debugging Made Easy + +### Check if everything is working + +```bash +# Test connection to device +ping 192.168.1.100 + +# Check if JetKVM is running +ssh root@192.168.1.100 ps aux | grep jetkvm +``` + +### View live logs + +```bash +ssh root@192.168.1.100 +tail -f /var/log/jetkvm.log +``` + +### Reset everything (if stuck) + +```bash +ssh root@192.168.1.100 +rm /userdata/kvm_config.json +systemctl restart jetkvm +``` + +--- + +## Testing Your Changes + +### Manual Testing + +1. Deploy your changes: `./dev_deploy.sh -r ` +2. Open browser: `http://` +3. Test your feature +4. Check logs: `ssh root@ tail -f /var/log/jetkvm.log` + +### Automated Testing + +```bash +# Run all tests +./dev_deploy.sh -r --run-go-tests + +# Frontend linting +cd ui && npm run lint +``` + +### API Testing + +```bash +# Test login endpoint +curl -X POST http:///auth/password-local \ + -H "Content-Type: application/json" \ + -d '{"password": "test123"}' +``` + +--- + +## Common Issues & Solutions + +### "Build failed" or "Permission denied" + +```bash +# Fix permissions +ssh root@ chmod +x /userdata/jetkvm/bin/jetkvm_app_debug + +# Clean and rebuild +go clean -modcache +go mod tidy +make build_dev +``` + +### "Can't connect to device" + +```bash +# Check network +ping + +# Check SSH +ssh root@ echo "Connection OK" +``` + +### "Frontend not updating" + +```bash +# Clear cache and rebuild +cd ui +npm cache clean --force +rm -rf node_modules +npm install +``` + +--- + +## Next Steps + +### Adding a New Feature + +1. **Backend:** Add API endpoint in `web.go` +2. **Config:** Add settings in `config.go` +3. **Frontend:** Add UI in `ui/src/routes/` +4. **Test:** Deploy and test with `./dev_deploy.sh` + +### Code Style + +- **Go:** Follow standard Go conventions +- **TypeScript:** Use TypeScript for type safety +- **React:** Keep components small and reusable + +### Environment Variables + +```bash +# Enable debug logging +export LOG_TRACE_SCOPES="jetkvm,cloud,websocket,native,jsonrpc" + +# Frontend development +export JETKVM_PROXY_URL="ws://" +``` + +--- + +## Need Help? + +1. **Check logs first:** `ssh root@ tail -f /var/log/jetkvm.log` +2. **Search issues:** [GitHub Issues](https://github.com/jetkvm/kvm/issues) +3. **Ask on Discord:** [JetKVM Discord](https://jetkvm.com/discord) +4. **Read docs:** [JetKVM Documentation](https://jetkvm.com/docs) + +--- + +## Contributing + +### Ready to contribute? + +1. Fork the repository +2. Create a feature branch +3. Make your changes +4. Test thoroughly +5. Submit a pull request + +### Before submitting: + +- [ ] Code works on device +- [ ] Tests pass +- [ ] Code follows style guidelines +- [ ] Documentation updated (if needed) + +--- + +## Advanced Topics + +### Performance Profiling + +```bash +# Enable profiling +go build -o bin/jetkvm_app -ldflags="-X main.enableProfiling=true" cmd/main.go + +# Access profiling +curl http://:6060/debug/pprof/ +``` +### Advanced Environment Variables + +```bash +# Enable trace logging (useful for debugging) +export LOG_TRACE_SCOPES="jetkvm,cloud,websocket,native,jsonrpc" + +# For frontend development +export JETKVM_PROXY_URL="ws://" + +# Enable SSL in development +export USE_SSL=true +``` + +### Configuration Management + +The application uses a JSON configuration file stored at `/userdata/kvm_config.json`. + +#### Adding New Configuration Options + +1. **Update the Config struct in `config.go`:** + + ```go + type Config struct { + // ... existing fields + NewFeatureEnabled bool `json:"new_feature_enabled"` + } + ``` + +2. **Update the default configuration:** + + ```go + var defaultConfig = &Config{ + // ... existing defaults + NewFeatureEnabled: false, + } + ``` + +3. **Add migration logic if needed for existing installations** + + +--- + +**Happy coding!** + +For more information, visit the [JetKVM Documentation](https://jetkvm.com/docs) or join our [Discord Server](https://jetkvm.com/discord). diff --git a/Makefile b/Makefile index 2aefdea..c696dca 100644 --- a/Makefile +++ b/Makefile @@ -2,12 +2,14 @@ BRANCH ?= $(shell git rev-parse --abbrev-ref HEAD) BUILDDATE ?= $(shell date -u +%FT%T%z) BUILDTS ?= $(shell date -u +%s) REVISION ?= $(shell git rev-parse HEAD) -VERSION_DEV := 0.3.9-dev$(shell date +%Y%m%d%H%M) -VERSION := 0.3.8 +VERSION_DEV ?= 0.4.7-dev$(shell date +%Y%m%d%H%M) +VERSION ?= 0.4.6 PROMETHEUS_TAG := github.com/prometheus/common/version KVM_PKG_NAME := github.com/jetkvm/kvm +GO_BUILD_ARGS := -tags netgo +GO_RELEASE_BUILD_ARGS := -trimpath $(GO_BUILD_ARGS) GO_LDFLAGS := \ -s -w \ -X $(PROMETHEUS_TAG).Branch=$(BRANCH) \ @@ -15,12 +17,49 @@ GO_LDFLAGS := \ -X $(PROMETHEUS_TAG).Revision=$(REVISION) \ -X $(KVM_PKG_NAME).builtTimestamp=$(BUILDTS) +GO_CMD := GOOS=linux GOARCH=arm GOARM=7 go +BIN_DIR := $(shell pwd)/bin + +TEST_DIRS := $(shell find . -name "*_test.go" -type f -exec dirname {} \; | sort -u) + hash_resource: @shasum -a 256 resource/jetkvm_native | cut -d ' ' -f 1 > resource/jetkvm_native.sha256 build_dev: hash_resource @echo "Building..." - GOOS=linux GOARCH=arm GOARM=7 go build -ldflags="$(GO_LDFLAGS) -X $(KVM_PKG_NAME).builtAppVersion=$(VERSION_DEV)" -o bin/jetkvm_app cmd/main.go + $(GO_CMD) build \ + -ldflags="$(GO_LDFLAGS) -X $(KVM_PKG_NAME).builtAppVersion=$(VERSION_DEV)" \ + $(GO_RELEASE_BUILD_ARGS) \ + -o $(BIN_DIR)/jetkvm_app cmd/main.go + +build_test2json: + $(GO_CMD) build -o $(BIN_DIR)/test2json cmd/test2json + +build_gotestsum: + @echo "Building gotestsum..." + $(GO_CMD) install gotest.tools/gotestsum@latest + cp $(shell $(GO_CMD) env GOPATH)/bin/linux_arm/gotestsum $(BIN_DIR)/gotestsum + +build_dev_test: build_test2json build_gotestsum +# collect all directories that contain tests + @echo "Building tests for devices ..." + @rm -rf $(BIN_DIR)/tests && mkdir -p $(BIN_DIR)/tests + + @cat resource/dev_test.sh > $(BIN_DIR)/tests/run_all_tests + @for test in $(TEST_DIRS); do \ + test_pkg_name=$$(echo $$test | sed 's/^.\///g'); \ + test_pkg_full_name=$(KVM_PKG_NAME)/$$(echo $$test | sed 's/^.\///g'); \ + test_filename=$$(echo $$test_pkg_name | sed 's/\//__/g')_test; \ + $(GO_CMD) test -v \ + -ldflags="$(GO_LDFLAGS) -X $(KVM_PKG_NAME).builtAppVersion=$(VERSION_DEV)" \ + $(GO_BUILD_ARGS) \ + -c -o $(BIN_DIR)/tests/$$test_filename $$test; \ + echo "runTest ./$$test_filename $$test_pkg_full_name" >> $(BIN_DIR)/tests/run_all_tests; \ + done; \ + chmod +x $(BIN_DIR)/tests/run_all_tests; \ + cp $(BIN_DIR)/test2json $(BIN_DIR)/tests/ && chmod +x $(BIN_DIR)/tests/test2json; \ + cp $(BIN_DIR)/gotestsum $(BIN_DIR)/tests/ && chmod +x $(BIN_DIR)/tests/gotestsum; \ + tar czfv device-tests.tar.gz -C $(BIN_DIR)/tests . frontend: cd ui && npm ci && npm run build:device @@ -33,7 +72,10 @@ dev_release: frontend build_dev build_release: frontend hash_resource @echo "Building release..." - GOOS=linux GOARCH=arm GOARM=7 go build -ldflags="$(GO_LDFLAGS) -X $(KVM_PKG_NAME).builtAppVersion=$(VERSION)" -o bin/jetkvm_app cmd/main.go + $(GO_CMD) build \ + -ldflags="$(GO_LDFLAGS) -X $(KVM_PKG_NAME).builtAppVersion=$(VERSION)" \ + $(GO_RELEASE_BUILD_ARGS) \ + -o bin/jetkvm_app cmd/main.go release: @if rclone lsf r2://jetkvm-update/app/$(VERSION)/ | grep -q "jetkvm_app"; then \ diff --git a/README.md b/README.md index 1b516d7..541578c 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,8 @@ [![Twitter](https://img.shields.io/twitter/url/https/twitter.com/jetkvm.svg?style=social&label=Follow%20%40JetKVM)](https://twitter.com/jetkvm) +[![Go Report Card](https://goreportcard.com/badge/github.com/jetkvm/kvm)](https://goreportcard.com/report/github.com/jetkvm/kvm) + JetKVM is a high-performance, open-source KVM over IP (Keyboard, Video, Mouse) solution designed for efficient remote management of computers, servers, and workstations. Whether you're dealing with boot failures, installing a new operating system, adjusting BIOS settings, or simply taking control of a machine from afar, JetKVM provides the tools to get it done effectively. @@ -23,7 +25,7 @@ We welcome contributions from the community! Whether it's improving the firmware ## I need help -The best place to search for answers is our [Documentation](https://jetkvm.com/docs). If you can't find the answer there, check our [Discord Server](https://discord.gg/8MaAhua7NW). +The best place to search for answers is our [Documentation](https://jetkvm.com/docs). If you can't find the answer there, check our [Discord Server](https://jetkvm.com/discord). ## I want to report an issue @@ -35,7 +37,9 @@ JetKVM is written in Go & TypeScript. with some bits and pieces written in C. An The project contains two main parts, the backend software that runs on the KVM device and the frontend software that is served by the KVM device, and also the cloud. -For most of local device development, all you need is to use the `./dev_deploy.sh` script. It will build the frontend and backend and deploy them to the local KVM device. Run `./dev_deploy.sh --help` for more information. +For comprehensive development information, including setup, testing, debugging, and contribution guidelines, see **[DEVELOPMENT.md](DEVELOPMENT.md)**. + +For quick device development, use the `./dev_deploy.sh` script. It will build the frontend and backend and deploy them to the local KVM device. Run `./dev_deploy.sh --help` for more information. ## Backend diff --git a/block_device.go b/block_device.go index 3a44135..2274098 100644 --- a/block_device.go +++ b/block_device.go @@ -7,8 +7,8 @@ import ( "os" "time" - "github.com/pojntfx/go-nbd/pkg/client" "github.com/pojntfx/go-nbd/pkg/server" + "github.com/rs/zerolog" ) type remoteImageBackend struct { @@ -16,8 +16,8 @@ type remoteImageBackend struct { func (r remoteImageBackend) ReadAt(p []byte, off int64) (n int, err error) { virtualMediaStateMutex.RLock() - logger.Debugf("currentVirtualMediaState is %v", currentVirtualMediaState) - logger.Debugf("read size: %d, off: %d", len(p), off) + logger.Debug().Interface("currentVirtualMediaState", currentVirtualMediaState).Msg("currentVirtualMediaState") + logger.Debug().Int64("read size", int64(len(p))).Int64("off", off).Msg("read size and off") if currentVirtualMediaState == nil { return 0, errors.New("image not mounted") } @@ -33,16 +33,17 @@ func (r remoteImageBackend) ReadAt(p []byte, off int64) (n int, err error) { readLen = mountedImageSize - off } var data []byte - if source == WebRTC { + switch source { + case WebRTC: data, err = webRTCDiskReader.Read(ctx, off, readLen) if err != nil { return 0, err } n = copy(p, data) return n, nil - } else if source == HTTP { + case HTTP: return httpRangeReader.ReadAt(p, off) - } else { + default: return 0, errors.New("unknown image source") } } @@ -72,6 +73,8 @@ type NBDDevice struct { serverConn net.Conn clientConn net.Conn dev *os.File + + l *zerolog.Logger } func NewNBDDevice() *NBDDevice { @@ -90,10 +93,18 @@ func (d *NBDDevice) Start() error { return err } + if d.l == nil { + scopedLogger := nbdLogger.With(). + Str("socket_path", nbdSocketPath). + Str("device_path", nbdDevicePath). + Logger() + d.l = &scopedLogger + } + // Remove the socket file if it already exists if _, err := os.Stat(nbdSocketPath); err == nil { if err := os.Remove(nbdSocketPath); err != nil { - logger.Errorf("Failed to remove existing socket file %s: %v", nbdSocketPath, err) + d.l.Error().Err(err).Msg("failed to remove existing socket file") os.Exit(1) } } @@ -134,32 +145,6 @@ func (d *NBDDevice) runServerConn() { MaximumBlockSize: uint32(16 * 1024), SupportsMultiConn: false, }) - logger.Infof("nbd server exited: %v", err) -} -func (d *NBDDevice) runClientConn() { - err := client.Connect(d.clientConn, d.dev, &client.Options{ - ExportName: "jetkvm", - BlockSize: uint32(4 * 1024), - }) - logger.Infof("nbd client exited: %v", err) -} - -func (d *NBDDevice) Close() { - if d.dev != nil { - err := client.Disconnect(d.dev) - if err != nil { - logger.Warnf("error disconnecting nbd client: %v", err) - } - _ = d.dev.Close() - } - if d.listener != nil { - _ = d.listener.Close() - } - if d.clientConn != nil { - _ = d.clientConn.Close() - } - if d.serverConn != nil { - _ = d.serverConn.Close() - } + d.l.Info().Err(err).Msg("nbd server exited") } diff --git a/block_device_linux.go b/block_device_linux.go new file mode 100644 index 0000000..8ca9372 --- /dev/null +++ b/block_device_linux.go @@ -0,0 +1,34 @@ +//go:build linux + +package kvm + +import ( + "github.com/pojntfx/go-nbd/pkg/client" +) + +func (d *NBDDevice) runClientConn() { + err := client.Connect(d.clientConn, d.dev, &client.Options{ + ExportName: "jetkvm", + BlockSize: uint32(4 * 1024), + }) + d.l.Info().Err(err).Msg("nbd client exited") +} + +func (d *NBDDevice) Close() { + if d.dev != nil { + err := client.Disconnect(d.dev) + if err != nil { + d.l.Warn().Err(err).Msg("error disconnecting nbd client") + } + _ = d.dev.Close() + } + if d.listener != nil { + _ = d.listener.Close() + } + if d.clientConn != nil { + _ = d.clientConn.Close() + } + if d.serverConn != nil { + _ = d.serverConn.Close() + } +} diff --git a/block_device_notlinux.go b/block_device_notlinux.go new file mode 100644 index 0000000..b6a9aba --- /dev/null +++ b/block_device_notlinux.go @@ -0,0 +1,17 @@ +//go:build !linux + +package kvm + +import ( + "os" +) + +func (d *NBDDevice) runClientConn() { + d.l.Error().Msg("platform not supported") + os.Exit(1) +} + +func (d *NBDDevice) Close() { + d.l.Error().Msg("platform not supported") + os.Exit(1) +} diff --git a/cloud.go b/cloud.go index a30a14c..cec749e 100644 --- a/cloud.go +++ b/cloud.go @@ -4,17 +4,23 @@ import ( "bytes" "context" "encoding/json" + "errors" "fmt" "net/http" "net/url" + "sync" "time" "github.com/coder/websocket/wsjson" + "github.com/google/uuid" + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promauto" "github.com/coreos/go-oidc/v3/oidc" "github.com/coder/websocket" "github.com/gin-gonic/gin" + "github.com/rs/zerolog" ) type CloudRegisterRequest struct { @@ -32,10 +38,163 @@ const ( // CloudOidcRequestTimeout is the timeout for OIDC token verification requests // should be lower than the websocket response timeout set in cloud-api CloudOidcRequestTimeout = 10 * time.Second - // CloudWebSocketPingInterval is the interval at which the websocket client sends ping messages to the cloud - CloudWebSocketPingInterval = 15 * time.Second + // WebsocketPingInterval is the interval at which the websocket client sends ping messages to the cloud + WebsocketPingInterval = 15 * time.Second ) +var ( + metricCloudConnectionStatus = promauto.NewGauge( + prometheus.GaugeOpts{ + Name: "jetkvm_cloud_connection_status", + Help: "The status of the cloud connection", + }, + ) + metricCloudConnectionEstablishedTimestamp = promauto.NewGauge( + prometheus.GaugeOpts{ + Name: "jetkvm_cloud_connection_established_timestamp_seconds", + Help: "The timestamp when the cloud connection was established", + }, + ) + metricConnectionLastPingTimestamp = promauto.NewGaugeVec( + prometheus.GaugeOpts{ + Name: "jetkvm_connection_last_ping_timestamp_seconds", + Help: "The timestamp when the last ping response was received", + }, + []string{"type", "source"}, + ) + metricConnectionLastPingReceivedTimestamp = promauto.NewGaugeVec( + prometheus.GaugeOpts{ + Name: "jetkvm_connection_last_ping_received_timestamp_seconds", + Help: "The timestamp when the last ping request was received", + }, + []string{"type", "source"}, + ) + metricConnectionLastPingDuration = promauto.NewGaugeVec( + prometheus.GaugeOpts{ + Name: "jetkvm_connection_last_ping_duration_seconds", + Help: "The duration of the last ping response", + }, + []string{"type", "source"}, + ) + metricConnectionPingDuration = promauto.NewHistogramVec( + prometheus.HistogramOpts{ + Name: "jetkvm_connection_ping_duration_seconds", + Help: "The duration of the ping response", + Buckets: []float64{ + 0.1, 0.5, 1, 10, + }, + }, + []string{"type", "source"}, + ) + metricConnectionTotalPingSentCount = promauto.NewCounterVec( + prometheus.CounterOpts{ + Name: "jetkvm_connection_ping_sent_total", + Help: "The total number of pings sent to the connection", + }, + []string{"type", "source"}, + ) + metricConnectionTotalPingReceivedCount = promauto.NewCounterVec( + prometheus.CounterOpts{ + Name: "jetkvm_connection_ping_received_total", + Help: "The total number of pings received from the connection", + }, + []string{"type", "source"}, + ) + metricConnectionSessionRequestCount = promauto.NewCounterVec( + prometheus.CounterOpts{ + Name: "jetkvm_connection_session_requests_total", + Help: "The total number of session requests received", + }, + []string{"type", "source"}, + ) + metricConnectionSessionRequestDuration = promauto.NewHistogramVec( + prometheus.HistogramOpts{ + Name: "jetkvm_connection_session_request_duration_seconds", + Help: "The duration of session requests", + Buckets: []float64{ + 0.1, 0.5, 1, 10, + }, + }, + []string{"type", "source"}, + ) + metricConnectionLastSessionRequestTimestamp = promauto.NewGaugeVec( + prometheus.GaugeOpts{ + Name: "jetkvm_connection_last_session_request_timestamp_seconds", + Help: "The timestamp of the last session request", + }, + []string{"type", "source"}, + ) + metricConnectionLastSessionRequestDuration = promauto.NewGaugeVec( + prometheus.GaugeOpts{ + Name: "jetkvm_connection_last_session_request_duration", + Help: "The duration of the last session request", + }, + []string{"type", "source"}, + ) + metricCloudConnectionFailureCount = promauto.NewCounter( + prometheus.CounterOpts{ + Name: "jetkvm_cloud_connection_failure_total", + Help: "The number of times the cloud connection has failed", + }, + ) +) + +type CloudConnectionState uint8 + +const ( + CloudConnectionStateNotConfigured CloudConnectionState = iota + CloudConnectionStateDisconnected + CloudConnectionStateConnecting + CloudConnectionStateConnected +) + +var ( + cloudConnectionState CloudConnectionState = CloudConnectionStateNotConfigured + cloudConnectionStateLock = &sync.Mutex{} + + cloudDisconnectChan chan error + cloudDisconnectLock = &sync.Mutex{} +) + +func setCloudConnectionState(state CloudConnectionState) { + cloudConnectionStateLock.Lock() + defer cloudConnectionStateLock.Unlock() + + if cloudConnectionState == CloudConnectionStateDisconnected && + (config.CloudToken == "" || config.CloudURL == "") { + state = CloudConnectionStateNotConfigured + } + + previousState := cloudConnectionState + cloudConnectionState = state + + go waitCtrlAndRequestDisplayUpdate( + previousState != state, + ) +} + +func wsResetMetrics(established bool, sourceType string, source string) { + metricConnectionLastPingTimestamp.WithLabelValues(sourceType, source).Set(-1) + metricConnectionLastPingDuration.WithLabelValues(sourceType, source).Set(-1) + + metricConnectionLastPingReceivedTimestamp.WithLabelValues(sourceType, source).Set(-1) + + metricConnectionLastSessionRequestTimestamp.WithLabelValues(sourceType, source).Set(-1) + metricConnectionLastSessionRequestDuration.WithLabelValues(sourceType, source).Set(-1) + + if sourceType != "cloud" { + return + } + + if established { + metricCloudConnectionEstablishedTimestamp.SetToCurrentTime() + metricCloudConnectionStatus.Set(1) + } else { + metricCloudConnectionEstablishedTimestamp.Set(-1) + metricCloudConnectionStatus.Set(-1) + } +} + func handleCloudRegister(c *gin.Context) { var req CloudRegisterRequest @@ -90,11 +249,6 @@ func handleCloudRegister(c *gin.Context) { return } - if config.CloudToken == "" { - cloudLogger.Info("Starting websocket client due to adoption") - go RunWebsocketClient() - } - config.CloudToken = tokenResp.SecretToken provider, err := oidc.NewProvider(c, "https://accounts.google.com") @@ -125,74 +279,116 @@ func handleCloudRegister(c *gin.Context) { c.JSON(200, gin.H{"message": "Cloud registration successful"}) } +func disconnectCloud(reason error) { + cloudDisconnectLock.Lock() + defer cloudDisconnectLock.Unlock() + + if cloudDisconnectChan == nil { + cloudLogger.Trace().Msg("cloud disconnect channel is not set, no need to disconnect") + return + } + + // just in case the channel is closed, we don't want to panic + defer func() { + if r := recover(); r != nil { + cloudLogger.Warn().Interface("reason", r).Msg("cloud disconnect channel is closed, no need to disconnect") + } + }() + cloudDisconnectChan <- reason +} + func runWebsocketClient() error { if config.CloudToken == "" { time.Sleep(5 * time.Second) return fmt.Errorf("cloud token is not set") } + wsURL, err := url.Parse(config.CloudURL) if err != nil { return fmt.Errorf("failed to parse config.CloudURL: %w", err) } + if wsURL.Scheme == "http" { wsURL.Scheme = "ws" } else { wsURL.Scheme = "wss" } + + setCloudConnectionState(CloudConnectionStateConnecting) + header := http.Header{} header.Set("X-Device-ID", GetDeviceID()) + header.Set("X-App-Version", builtAppVersion) header.Set("Authorization", "Bearer "+config.CloudToken) dialCtx, cancelDial := context.WithTimeout(context.Background(), CloudWebSocketConnectTimeout) + + l := websocketLogger.With(). + Str("source", wsURL.Host). + Str("sourceType", "cloud"). + Logger() + + scopedLogger := &l + defer cancelDial() - c, _, err := websocket.Dial(dialCtx, wsURL.String(), &websocket.DialOptions{ + c, resp, err := websocket.Dial(dialCtx, wsURL.String(), &websocket.DialOptions{ HTTPHeader: header, + OnPingReceived: func(ctx context.Context, payload []byte) bool { + scopedLogger.Debug().Bytes("payload", payload).Int("length", len(payload)).Msg("ping frame received") + + metricConnectionTotalPingReceivedCount.WithLabelValues("cloud", wsURL.Host).Inc() + metricConnectionLastPingReceivedTimestamp.WithLabelValues("cloud", wsURL.Host).SetToCurrentTime() + + setCloudConnectionState(CloudConnectionStateConnected) + + return true + }, }) + + var connectionId string + if resp != nil { + // get the request id from the response header + connectionId = resp.Header.Get("X-Request-ID") + if connectionId == "" { + connectionId = resp.Header.Get("Cf-Ray") + } + } + + if connectionId == "" { + connectionId = uuid.New().String() + scopedLogger.Warn(). + Str("connectionId", connectionId). + Msg("no connection id received from the server, generating a new one") + } + + lWithConnectionId := scopedLogger.With(). + Str("connectionID", connectionId). + Logger() + scopedLogger = &lWithConnectionId + + // if the context is canceled, we don't want to return an error if err != nil { + if errors.Is(err, context.Canceled) { + cloudLogger.Info().Msg("websocket connection canceled") + setCloudConnectionState(CloudConnectionStateDisconnected) + + return nil + } return err } defer c.CloseNow() //nolint:errcheck - cloudLogger.Infof("websocket connected to %s", wsURL) - runCtx, cancelRun := context.WithCancel(context.Background()) - defer cancelRun() - go func() { - for { - time.Sleep(CloudWebSocketPingInterval) - err := c.Ping(runCtx) - if err != nil { - cloudLogger.Warnf("websocket ping error: %v", err) - cancelRun() - return - } - } - }() - for { - typ, msg, err := c.Read(runCtx) - if err != nil { - return err - } - if typ != websocket.MessageText { - // ignore non-text messages - continue - } - var req WebRTCSessionRequest - err = json.Unmarshal(msg, &req) - if err != nil { - cloudLogger.Warnf("unable to parse ws message: %v", string(msg)) - continue - } + cloudLogger.Info(). + Str("url", wsURL.String()). + Str("connectionID", connectionId). + Msg("websocket connected") - cloudLogger.Infof("new session request: %v", req.OidcGoogle) - cloudLogger.Tracef("session request info: %v", req) + // set the metrics when we successfully connect to the cloud. + wsResetMetrics(true, "cloud", wsURL.Host) - err = handleSessionRequest(runCtx, c, req) - if err != nil { - cloudLogger.Infof("error starting new session: %v", err) - continue - } - } + // we don't have a source for the cloud connection + return handleWebRTCSignalWsMessages(c, true, wsURL.Host, connectionId, scopedLogger) } -func handleSessionRequest(ctx context.Context, c *websocket.Conn, req WebRTCSessionRequest) error { +func authenticateSession(ctx context.Context, c *websocket.Conn, req WebRTCSessionRequest) error { oidcCtx, cancelOIDC := context.WithTimeout(ctx, CloudOidcRequestTimeout) defer cancelOIDC() provider, err := oidc.NewProvider(oidcCtx, "https://accounts.google.com") @@ -200,7 +396,7 @@ func handleSessionRequest(ctx context.Context, c *websocket.Conn, req WebRTCSess _ = wsjson.Write(context.Background(), c, gin.H{ "error": fmt.Sprintf("failed to initialize OIDC provider: %v", err), }) - cloudLogger.Errorf("failed to initialize OIDC provider: %v", err) + cloudLogger.Warn().Err(err).Msg("failed to initialize OIDC provider") return err } @@ -220,10 +416,43 @@ func handleSessionRequest(ctx context.Context, c *websocket.Conn, req WebRTCSess return fmt.Errorf("google identity mismatch") } + return nil +} + +func handleSessionRequest( + ctx context.Context, + c *websocket.Conn, + req WebRTCSessionRequest, + isCloudConnection bool, + source string, + scopedLogger *zerolog.Logger, +) error { + var sourceType string + if isCloudConnection { + sourceType = "cloud" + } else { + sourceType = "local" + } + + timer := prometheus.NewTimer(prometheus.ObserverFunc(func(v float64) { + metricConnectionLastSessionRequestDuration.WithLabelValues(sourceType, source).Set(v) + metricConnectionSessionRequestDuration.WithLabelValues(sourceType, source).Observe(v) + })) + defer timer.ObserveDuration() + + // If the message is from the cloud, we need to authenticate the session. + if isCloudConnection { + if err := authenticateSession(ctx, c, req); err != nil { + return err + } + } + session, err := newSession(SessionConfig{ - ICEServers: req.ICEServers, + ws: c, + IsCloud: isCloudConnection, LocalIP: req.IP, - IsCloud: true, + ICEServers: req.ICEServers, + Logger: scopedLogger, }) if err != nil { _ = wsjson.Write(context.Background(), c, gin.H{"error": err}) @@ -244,18 +473,40 @@ func handleSessionRequest(ctx context.Context, c *websocket.Conn, req WebRTCSess }() } - cloudLogger.Info("new session accepted") - cloudLogger.Tracef("new session accepted: %v", session) + cloudLogger.Info().Interface("session", session).Msg("new session accepted") + cloudLogger.Trace().Interface("session", session).Msg("new session accepted") currentSession = session - _ = wsjson.Write(context.Background(), c, gin.H{"sd": sd}) + _ = wsjson.Write(context.Background(), c, gin.H{"type": "answer", "data": sd}) return nil } func RunWebsocketClient() { for { + // If the cloud token is not set, we don't need to run the websocket client. + if config.CloudToken == "" { + time.Sleep(5 * time.Second) + continue + } + + // If the network is not up, well, we can't connect to the cloud. + if !networkState.IsOnline() { + cloudLogger.Warn().Msg("waiting for network to be online, will retry in 3 seconds") + time.Sleep(3 * time.Second) + continue + } + + // If the system time is not synchronized, the API request will fail anyway because the TLS handshake will fail. + if isTimeSyncNeeded() && !timeSync.IsSyncSuccess() { + cloudLogger.Warn().Msg("system time is not synced, will retry in 3 seconds") + time.Sleep(3 * time.Second) + continue + } + err := runWebsocketClient() if err != nil { - cloudLogger.Errorf("websocket client error: %v", err) + cloudLogger.Warn().Err(err).Msg("websocket client error") + metricCloudConnectionStatus.Set(0) + metricCloudConnectionFailureCount.Inc() time.Sleep(5 * time.Second) } } @@ -305,6 +556,11 @@ func rpcDeregisterDevice() error { return fmt.Errorf("failed to save configuration after deregistering: %w", err) } + cloudLogger.Info().Msg("device deregistered, disconnecting from cloud") + disconnectCloud(fmt.Errorf("device deregistered")) + + setCloudConnectionState(CloudConnectionStateNotConfigured) + return nil } diff --git a/cmd/main.go b/cmd/main.go index ab44ac9..2292bd9 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -1,9 +1,27 @@ package main import ( + "flag" + "fmt" + "os" + "github.com/jetkvm/kvm" ) func main() { + versionPtr := flag.Bool("version", false, "print version and exit") + versionJsonPtr := flag.Bool("version-json", false, "print version as json and exit") + flag.Parse() + + if *versionPtr || *versionJsonPtr { + versionData, err := kvm.GetVersionData(*versionJsonPtr) + if err != nil { + fmt.Printf("failed to get version data: %v\n", err) + os.Exit(1) + } + fmt.Println(string(versionData)) + return + } + kvm.Main() } diff --git a/config.go b/config.go index a88c492..8ad7dad 100644 --- a/config.go +++ b/config.go @@ -6,7 +6,11 @@ import ( "os" "sync" + "github.com/jetkvm/kvm/internal/logging" + "github.com/jetkvm/kvm/internal/network" "github.com/jetkvm/kvm/internal/usbgadget" + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promauto" ) type WakeOnLanDevice struct { @@ -14,27 +18,91 @@ type WakeOnLanDevice struct { MacAddress string `json:"macAddress"` } +// Constants for keyboard macro limits +const ( + MaxMacrosPerDevice = 25 + MaxStepsPerMacro = 10 + MaxKeysPerStep = 10 + MinStepDelay = 50 + MaxStepDelay = 2000 +) + +type KeyboardMacroStep struct { + Keys []string `json:"keys"` + Modifiers []string `json:"modifiers"` + Delay int `json:"delay"` +} + +func (s *KeyboardMacroStep) Validate() error { + if len(s.Keys) > MaxKeysPerStep { + return fmt.Errorf("too many keys in step (max %d)", MaxKeysPerStep) + } + + if s.Delay < MinStepDelay { + s.Delay = MinStepDelay + } else if s.Delay > MaxStepDelay { + s.Delay = MaxStepDelay + } + + return nil +} + +type KeyboardMacro struct { + ID string `json:"id"` + Name string `json:"name"` + Steps []KeyboardMacroStep `json:"steps"` + SortOrder int `json:"sortOrder,omitempty"` +} + +func (m *KeyboardMacro) Validate() error { + if m.Name == "" { + return fmt.Errorf("macro name cannot be empty") + } + + if len(m.Steps) == 0 { + return fmt.Errorf("macro must have at least one step") + } + + if len(m.Steps) > MaxStepsPerMacro { + return fmt.Errorf("too many steps in macro (max %d)", MaxStepsPerMacro) + } + + for i := range m.Steps { + if err := m.Steps[i].Validate(); err != nil { + return fmt.Errorf("invalid step %d: %w", i+1, err) + } + } + + return nil +} + type Config struct { - CloudURL string `json:"cloud_url"` - CloudAppURL string `json:"cloud_app_url"` - CloudToken string `json:"cloud_token"` - GoogleIdentity string `json:"google_identity"` - JigglerEnabled bool `json:"jiggler_enabled"` - JigglerConfig *JigglerConfig `json:"jiggler_config"` - AutoUpdateEnabled bool `json:"auto_update_enabled"` - IncludePreRelease bool `json:"include_pre_release"` - HashedPassword string `json:"hashed_password"` - LocalAuthToken string `json:"local_auth_token"` - LocalAuthMode string `json:"localAuthMode"` //TODO: fix it with migration - WakeOnLanDevices []WakeOnLanDevice `json:"wake_on_lan_devices"` - EdidString string `json:"hdmi_edid_string"` - ActiveExtension string `json:"active_extension"` - DisplayMaxBrightness int `json:"display_max_brightness"` - DisplayDimAfterSec int `json:"display_dim_after_sec"` - DisplayOffAfterSec int `json:"display_off_after_sec"` - TLSMode string `json:"tls_mode"` - UsbConfig *usbgadget.Config `json:"usb_config"` - UsbDevices *usbgadget.Devices `json:"usb_devices"` + CloudURL string `json:"cloud_url"` + CloudAppURL string `json:"cloud_app_url"` + CloudToken string `json:"cloud_token"` + GoogleIdentity string `json:"google_identity"` + JigglerEnabled bool `json:"jiggler_enabled"` + JigglerConfig *JigglerConfig `json:"jiggler_config"` + AutoUpdateEnabled bool `json:"auto_update_enabled"` + IncludePreRelease bool `json:"include_pre_release"` + HashedPassword string `json:"hashed_password"` + LocalAuthToken string `json:"local_auth_token"` + LocalAuthMode string `json:"localAuthMode"` //TODO: fix it with migration + LocalLoopbackOnly bool `json:"local_loopback_only"` + WakeOnLanDevices []WakeOnLanDevice `json:"wake_on_lan_devices"` + KeyboardMacros []KeyboardMacro `json:"keyboard_macros"` + KeyboardLayout string `json:"keyboard_layout"` + EdidString string `json:"hdmi_edid_string"` + ActiveExtension string `json:"active_extension"` + DisplayRotation string `json:"display_rotation"` + DisplayMaxBrightness int `json:"display_max_brightness"` + DisplayDimAfterSec int `json:"display_dim_after_sec"` + DisplayOffAfterSec int `json:"display_off_after_sec"` + TLSMode string `json:"tls_mode"` // options: "self-signed", "user-defined", "" + UsbConfig *usbgadget.Config `json:"usb_config"` + UsbDevices *usbgadget.Devices `json:"usb_devices"` + NetworkConfig *network.NetworkConfig `json:"network_config"` + DefaultLogLevel string `json:"default_log_level"` } const configPath = "/userdata/kvm_config.json" @@ -44,6 +112,9 @@ var defaultConfig = &Config{ CloudAppURL: "https://app.jetkvm.com", AutoUpdateEnabled: true, // Set a default value ActiveExtension: "", + KeyboardMacros: []KeyboardMacro{}, + DisplayRotation: "270", + KeyboardLayout: "en_US", DisplayMaxBrightness: 64, DisplayDimAfterSec: 120, // 2 minutes DisplayOffAfterSec: 1800, // 30 minutes @@ -66,6 +137,8 @@ var defaultConfig = &Config{ Keyboard: true, MassStorage: true, }, + NetworkConfig: &network.NetworkConfig{}, + DefaultLogLevel: "INFO", } var ( @@ -73,12 +146,27 @@ var ( configLock = &sync.Mutex{} ) +var ( + configSuccess = promauto.NewGauge( + prometheus.GaugeOpts{ + Name: "jetkvm_config_last_reload_successful", + Help: "The last configuration load succeeded", + }, + ) + configSuccessTime = promauto.NewGauge( + prometheus.GaugeOpts{ + Name: "jetkvm_config_last_reload_success_timestamp_seconds", + Help: "Timestamp of last successful config load", + }, + ) +) + func LoadConfig() { configLock.Lock() defer configLock.Unlock() if config != nil { - logger.Info("config already loaded, skipping") + logger.Debug().Msg("config already loaded, skipping") return } @@ -87,7 +175,9 @@ func LoadConfig() { file, err := os.Open(configPath) if err != nil { - logger.Debug("default config file doesn't exist, using default") + logger.Debug().Msg("default config file doesn't exist, using default") + configSuccess.Set(1.0) + configSuccessTime.SetToCurrentTime() return } defer file.Close() @@ -95,7 +185,8 @@ func LoadConfig() { // load and merge the default config with the user config loadedConfig := *defaultConfig if err := json.NewDecoder(file).Decode(&loadedConfig); err != nil { - logger.Errorf("config file JSON parsing failed, %v", err) + logger.Warn().Err(err).Msg("config file JSON parsing failed") + configSuccess.Set(0.0) return } @@ -108,13 +199,26 @@ func LoadConfig() { loadedConfig.UsbDevices = defaultConfig.UsbDevices } + if loadedConfig.NetworkConfig == nil { + loadedConfig.NetworkConfig = defaultConfig.NetworkConfig + } + config = &loadedConfig + + logging.GetRootLogger().UpdateLogLevel(config.DefaultLogLevel) + + configSuccess.Set(1.0) + configSuccessTime.SetToCurrentTime() + + logger.Info().Str("path", configPath).Msg("config loaded") } func SaveConfig() error { configLock.Lock() defer configLock.Unlock() + logger.Trace().Str("path", configPath).Msg("Saving config") + file, err := os.Create(configPath) if err != nil { return fmt.Errorf("failed to create config file: %w", err) diff --git a/dc_metrics.go b/dc_metrics.go new file mode 100644 index 0000000..692a7b5 --- /dev/null +++ b/dc_metrics.go @@ -0,0 +1,53 @@ +package kvm + +import ( + "sync" + + "github.com/prometheus/client_golang/prometheus" +) + +var ( + dcCurrentGauge = prometheus.NewGauge(prometheus.GaugeOpts{ + Name: "jetkvm_dc_current_amperes", + Help: "Current DC power consumption in amperes", + }) + + dcPowerGauge = prometheus.NewGauge(prometheus.GaugeOpts{ + Name: "jetkvm_dc_power_watts", + Help: "DC power consumption in watts", + }) + + dcVoltageGauge = prometheus.NewGauge(prometheus.GaugeOpts{ + Name: "jetkvm_dc_voltage_volts", + Help: "DC voltage in volts", + }) + + dcStateGauge = prometheus.NewGauge(prometheus.GaugeOpts{ + Name: "jetkvm_dc_power_state", + Help: "DC power state (1 = on, 0 = off)", + }) + + dcMetricsRegistered sync.Once +) + +// registerDCMetrics registers the DC power metrics with Prometheus (called once when DC control is mounted) +func registerDCMetrics() { + dcMetricsRegistered.Do(func() { + prometheus.MustRegister(dcCurrentGauge) + prometheus.MustRegister(dcPowerGauge) + prometheus.MustRegister(dcVoltageGauge) + prometheus.MustRegister(dcStateGauge) + }) +} + +// updateDCMetrics updates the Prometheus metrics with current DC power state values +func updateDCMetrics(state DCPowerState) { + dcCurrentGauge.Set(state.Current) + dcPowerGauge.Set(state.Power) + dcVoltageGauge.Set(state.Voltage) + if state.IsOn { + dcStateGauge.Set(1) + } else { + dcStateGauge.Set(0) + } +} diff --git a/dev_deploy.sh b/dev_deploy.sh index 7fbf29e..aac9acb 100755 --- a/dev_deploy.sh +++ b/dev_deploy.sh @@ -1,6 +1,21 @@ +#!/usr/bin/env bash +# # Exit immediately if a command exits with a non-zero status set -e +C_RST="$(tput sgr0)" +C_ERR="$(tput setaf 1)" +C_OK="$(tput setaf 2)" +C_WARN="$(tput setaf 3)" +C_INFO="$(tput setaf 5)" + +msg() { printf '%s%s%s\n' $2 "$1" $C_RST; } + +msg_info() { msg "$1" $C_INFO; } +msg_ok() { msg "$1" $C_OK; } +msg_err() { msg "$1" $C_ERR; } +msg_warn() { msg "$1" $C_WARN; } + # Function to display help message show_help() { echo "Usage: $0 [options] -r " @@ -10,19 +25,26 @@ show_help() { echo echo "Optional:" echo " -u, --user Remote username (default: root)" + echo " --run-go-tests Run go tests" + echo " --run-go-tests-only Run go tests and exit" echo " --skip-ui-build Skip frontend/UI build" + echo " -i, --install Build for release and install the app" echo " --help Display this help message" echo echo "Example:" echo " $0 -r 192.168.0.17" echo " $0 -r 192.168.0.17 -u admin" - exit 0 } # Default values REMOTE_USER="root" REMOTE_PATH="/userdata/jetkvm/bin" SKIP_UI_BUILD=false +RESET_USB_HID_DEVICE=false +LOG_TRACE_SCOPES="${LOG_TRACE_SCOPES:-jetkvm,cloud,websocket,native,jsonrpc}" +RUN_GO_TESTS=false +RUN_GO_TESTS_ONLY=false +INSTALL_APP=false # Parse command line arguments while [[ $# -gt 0 ]]; do @@ -39,6 +61,23 @@ while [[ $# -gt 0 ]]; do SKIP_UI_BUILD=true shift ;; + --reset-usb-hid) + RESET_USB_HID_DEVICE=true + shift + ;; + --run-go-tests) + RUN_GO_TESTS=true + shift + ;; + --run-go-tests-only) + RUN_GO_TESTS_ONLY=true + RUN_GO_TESTS=true + shift + ;; + -i|--install) + INSTALL_APP=true + shift + ;; --help) show_help exit 0 @@ -53,27 +92,89 @@ done # Verify required parameters if [ -z "$REMOTE_HOST" ]; then - echo "Error: Remote IP is a required parameter" + msg_err "Error: Remote IP is a required parameter" show_help + exit 1 fi # Build the development version on the host if [ "$SKIP_UI_BUILD" = false ]; then + msg_info "▶ Building frontend" make frontend fi -make build_dev -# Change directory to the binary output directory -cd bin +if [ "$RUN_GO_TESTS" = true ]; then + msg_info "▶ Building go tests" + make build_dev_test -# Kill any existing instances of the application -ssh "${REMOTE_USER}@${REMOTE_HOST}" "killall jetkvm_app_debug || true" + msg_info "▶ Copying device-tests.tar.gz to remote host" + ssh "${REMOTE_USER}@${REMOTE_HOST}" "cat > /tmp/device-tests.tar.gz" < device-tests.tar.gz -# Copy the binary to the remote host -cat jetkvm_app | ssh "${REMOTE_USER}@${REMOTE_HOST}" "cat > $REMOTE_PATH/jetkvm_app_debug" + msg_info "▶ Running go tests" + ssh "${REMOTE_USER}@${REMOTE_HOST}" ash << 'EOF' +set -e +TMP_DIR=$(mktemp -d) +cd ${TMP_DIR} +tar zxf /tmp/device-tests.tar.gz +./gotestsum --format=testdox \ + --jsonfile=/tmp/device-tests.json \ + --post-run-command 'sh -c "echo $TESTS_FAILED > /tmp/device-tests.failed"' \ + --raw-command -- ./run_all_tests -json -# Deploy and run the application on the remote host -ssh "${REMOTE_USER}@${REMOTE_HOST}" ash < /userdata/jetkvm/jetkvm_app.update" < bin/jetkvm_app + + # Reboot the device, the new app will be deployed by the startup process. + ssh "${REMOTE_USER}@${REMOTE_HOST}" "reboot" +else + msg_info "▶ Building development binary" + make build_dev + + # Kill any existing instances of the application + ssh "${REMOTE_USER}@${REMOTE_HOST}" "killall jetkvm_app_debug || true" + + # Copy the binary to the remote host + ssh "${REMOTE_USER}@${REMOTE_HOST}" "cat > ${REMOTE_PATH}/jetkvm_app_debug" < bin/jetkvm_app + + if [ "$RESET_USB_HID_DEVICE" = true ]; then + msg_info "▶ Resetting USB HID device" + msg_warn "The option has been deprecated and will be removed in a future version, as JetKVM will now reset USB gadget configuration when needed" + # Remove the old USB gadget configuration + ssh "${REMOTE_USER}@${REMOTE_HOST}" "rm -rf /sys/kernel/config/usb_gadget/jetkvm/configs/c.1/hid.usb*" + ssh "${REMOTE_USER}@${REMOTE_HOST}" "ls /sys/class/udc > /sys/kernel/config/usb_gadget/jetkvm/UDC" + fi + + # Deploy and run the application on the remote host + ssh "${REMOTE_USER}@${REMOTE_HOST}" ash << EOF set -e # Set the library path to include the directory where librockit.so is located @@ -84,13 +185,14 @@ killall jetkvm_app || true killall jetkvm_app_debug || true # Navigate to the directory where the binary will be stored -cd "$REMOTE_PATH" +cd "${REMOTE_PATH}" # Make the new binary executable chmod +x jetkvm_app_debug # Run the application in the background -PION_LOG_TRACE=jetkvm,cloud ./jetkvm_app_debug +PION_LOG_TRACE=${LOG_TRACE_SCOPES} ./jetkvm_app_debug | tee -a /tmp/jetkvm_app_debug.log EOF +fi echo "Deployment complete." diff --git a/display.go b/display.go index f4e8cf7..274bb8b 100644 --- a/display.go +++ b/display.go @@ -5,12 +5,18 @@ import ( "fmt" "os" "strconv" + "sync" "time" ) -var currentScreen = "ui_Boot_Screen" var backlightState = 0 // 0 - NORMAL, 1 - DIMMED, 2 - OFF +var ( + currentScreen = "ui_Boot_Screen" + displayedTexts = make(map[string]string) + screenStateLock = sync.Mutex{} +) + var ( dimTicker *time.Ticker offTicker *time.Ticker @@ -21,73 +27,210 @@ const ( backlightControlClass string = "/sys/class/backlight/backlight/brightness" ) +// do not call this function directly, use switchToScreenIfDifferent instead +// this function is not thread safe func switchToScreen(screen string) { _, err := CallCtrlAction("lv_scr_load", map[string]interface{}{"obj": screen}) if err != nil { - logger.Warnf("failed to switch to screen %s: %v", screen, err) + displayLogger.Warn().Err(err).Str("screen", screen).Msg("failed to switch to screen") return } currentScreen = screen } -var displayedTexts = make(map[string]string) +func lvObjSetState(objName string, state string) (*CtrlResponse, error) { + return CallCtrlAction("lv_obj_set_state", map[string]interface{}{"obj": objName, "state": state}) +} + +func lvObjAddFlag(objName string, flag string) (*CtrlResponse, error) { + return CallCtrlAction("lv_obj_add_flag", map[string]interface{}{"obj": objName, "flag": flag}) +} + +func lvObjClearFlag(objName string, flag string) (*CtrlResponse, error) { + return CallCtrlAction("lv_obj_clear_flag", map[string]interface{}{"obj": objName, "flag": flag}) +} + +func lvObjHide(objName string) (*CtrlResponse, error) { + return lvObjAddFlag(objName, "LV_OBJ_FLAG_HIDDEN") +} + +func lvObjShow(objName string) (*CtrlResponse, error) { + return lvObjClearFlag(objName, "LV_OBJ_FLAG_HIDDEN") +} + +func lvObjSetOpacity(objName string, opacity int) (*CtrlResponse, error) { // nolint:unused + return CallCtrlAction("lv_obj_set_style_opa_layered", map[string]interface{}{"obj": objName, "opa": opacity}) +} + +func lvObjFadeIn(objName string, duration uint32) (*CtrlResponse, error) { + return CallCtrlAction("lv_obj_fade_in", map[string]interface{}{"obj": objName, "time": duration}) +} + +func lvObjFadeOut(objName string, duration uint32) (*CtrlResponse, error) { + return CallCtrlAction("lv_obj_fade_out", map[string]interface{}{"obj": objName, "time": duration}) +} + +func lvLabelSetText(objName string, text string) (*CtrlResponse, error) { + return CallCtrlAction("lv_label_set_text", map[string]interface{}{"obj": objName, "text": text}) +} + +func lvImgSetSrc(objName string, src string) (*CtrlResponse, error) { + return CallCtrlAction("lv_img_set_src", map[string]interface{}{"obj": objName, "src": src}) +} + +func lvDispSetRotation(rotation string) (*CtrlResponse, error) { + return CallCtrlAction("lv_disp_set_rotation", map[string]interface{}{"rotation": rotation}) +} func updateLabelIfChanged(objName string, newText string) { + screenStateLock.Lock() + defer screenStateLock.Unlock() + if newText != "" && newText != displayedTexts[objName] { - _, _ = CallCtrlAction("lv_label_set_text", map[string]interface{}{"obj": objName, "text": newText}) + _, _ = lvLabelSetText(objName, newText) displayedTexts[objName] = newText } } func switchToScreenIfDifferent(screenName string) { - logger.Infof("switching screen from %s to %s", currentScreen, screenName) + screenStateLock.Lock() + defer screenStateLock.Unlock() + if currentScreen != screenName { + displayLogger.Info().Str("from", currentScreen).Str("to", screenName).Msg("switching screen") switchToScreen(screenName) } } +func clearDisplayState() { + screenStateLock.Lock() + defer screenStateLock.Unlock() + + displayedTexts = make(map[string]string) + currentScreen = "ui_Boot_Screen" +} + +var ( + cloudBlinkLock sync.Mutex = sync.Mutex{} + cloudBlinkStopped bool + cloudBlinkTicker *time.Ticker +) + func updateDisplay() { - updateLabelIfChanged("ui_Home_Content_Ip", networkState.IPv4) + updateLabelIfChanged("ui_Home_Content_Ip", networkState.IPv4String()) if usbState == "configured" { updateLabelIfChanged("ui_Home_Footer_Usb_Status_Label", "Connected") - _, _ = CallCtrlAction("lv_obj_set_state", map[string]interface{}{"obj": "ui_Home_Footer_Usb_Status_Label", "state": "LV_STATE_DEFAULT"}) + _, _ = lvObjSetState("ui_Home_Footer_Usb_Status_Label", "LV_STATE_DEFAULT") } else { updateLabelIfChanged("ui_Home_Footer_Usb_Status_Label", "Disconnected") - _, _ = CallCtrlAction("lv_obj_set_state", map[string]interface{}{"obj": "ui_Home_Footer_Usb_Status_Label", "state": "LV_STATE_USER_2"}) + _, _ = lvObjSetState("ui_Home_Footer_Usb_Status_Label", "LV_STATE_USER_2") } if lastVideoState.Ready { updateLabelIfChanged("ui_Home_Footer_Hdmi_Status_Label", "Connected") - _, _ = CallCtrlAction("lv_obj_set_state", map[string]interface{}{"obj": "ui_Home_Footer_Hdmi_Status_Label", "state": "LV_STATE_DEFAULT"}) + _, _ = lvObjSetState("ui_Home_Footer_Hdmi_Status_Label", "LV_STATE_DEFAULT") } else { updateLabelIfChanged("ui_Home_Footer_Hdmi_Status_Label", "Disconnected") - _, _ = CallCtrlAction("lv_obj_set_state", map[string]interface{}{"obj": "ui_Home_Footer_Hdmi_Status_Label", "state": "LV_STATE_USER_2"}) + _, _ = lvObjSetState("ui_Home_Footer_Hdmi_Status_Label", "LV_STATE_USER_2") } updateLabelIfChanged("ui_Home_Header_Cloud_Status_Label", fmt.Sprintf("%d active", actionSessions)) - if networkState.Up { + + if networkState.IsUp() { switchToScreenIfDifferent("ui_Home_Screen") } else { switchToScreenIfDifferent("ui_No_Network_Screen") } + + if cloudConnectionState == CloudConnectionStateNotConfigured { + _, _ = lvObjHide("ui_Home_Header_Cloud_Status_Icon") + } else { + _, _ = lvObjShow("ui_Home_Header_Cloud_Status_Icon") + } + + switch cloudConnectionState { + case CloudConnectionStateDisconnected: + _, _ = lvImgSetSrc("ui_Home_Header_Cloud_Status_Icon", "cloud_disconnected.png") + stopCloudBlink() + case CloudConnectionStateConnecting: + _, _ = lvImgSetSrc("ui_Home_Header_Cloud_Status_Icon", "cloud.png") + startCloudBlink() + case CloudConnectionStateConnected: + _, _ = lvImgSetSrc("ui_Home_Header_Cloud_Status_Icon", "cloud.png") + stopCloudBlink() + } } -var displayInited = false +func startCloudBlink() { + if cloudBlinkTicker == nil { + cloudBlinkTicker = time.NewTicker(2 * time.Second) + } else { + // do nothing if the blink isn't stopped + if cloudBlinkStopped { + cloudBlinkLock.Lock() + defer cloudBlinkLock.Unlock() + + cloudBlinkStopped = false + cloudBlinkTicker.Reset(2 * time.Second) + } + } + + go func() { + for range cloudBlinkTicker.C { + if cloudConnectionState != CloudConnectionStateConnecting { + continue + } + _, _ = lvObjFadeOut("ui_Home_Header_Cloud_Status_Icon", 1000) + time.Sleep(1000 * time.Millisecond) + _, _ = lvObjFadeIn("ui_Home_Header_Cloud_Status_Icon", 1000) + time.Sleep(1000 * time.Millisecond) + } + }() +} + +func stopCloudBlink() { + if cloudBlinkTicker != nil { + cloudBlinkTicker.Stop() + } + + cloudBlinkLock.Lock() + defer cloudBlinkLock.Unlock() + cloudBlinkStopped = true +} + +var ( + displayInited = false + displayUpdateLock = sync.Mutex{} + waitDisplayUpdate = sync.Mutex{} +) + +func requestDisplayUpdate(shouldWakeDisplay bool) { + displayUpdateLock.Lock() + defer displayUpdateLock.Unlock() -func requestDisplayUpdate() { if !displayInited { - logger.Info("display not inited, skipping updates") + displayLogger.Info().Msg("display not inited, skipping updates") return } go func() { - wakeDisplay(false) - logger.Info("display updating") + if shouldWakeDisplay { + wakeDisplay(false) + } + displayLogger.Debug().Msg("display updating") //TODO: only run once regardless how many pending updates updateDisplay() }() } +func waitCtrlAndRequestDisplayUpdate(shouldWakeDisplay bool) { + waitDisplayUpdate.Lock() + defer waitDisplayUpdate.Unlock() + + waitCtrlClientConnected() + requestDisplayUpdate(shouldWakeDisplay) +} + func updateStaticContents() { //contents that never change - updateLabelIfChanged("ui_Home_Content_Mac", networkState.MAC) + updateLabelIfChanged("ui_Home_Content_Mac", networkState.MACString()) systemVersion, appVersion, err := GetLocalVersion() if err == nil { updateLabelIfChanged("ui_About_Content_Operating_System_Version_ContentLabel", systemVersion.String()) @@ -118,7 +261,7 @@ func setDisplayBrightness(brightness int) error { return err } - logger.Infof("display: set brightness to %v", brightness) + displayLogger.Info().Int("brightness", brightness).Msg("set brightness") return nil } @@ -127,7 +270,7 @@ func setDisplayBrightness(brightness int) error { func tick_displayDim() { err := setDisplayBrightness(config.DisplayMaxBrightness / 2) if err != nil { - logger.Warnf("display: failed to dim display: %s", err) + displayLogger.Warn().Err(err).Msg("failed to dim display") } dimTicker.Stop() @@ -140,7 +283,7 @@ func tick_displayDim() { func tick_displayOff() { err := setDisplayBrightness(0) if err != nil { - logger.Warnf("display: failed to turn off display: %s", err) + displayLogger.Warn().Err(err).Msg("failed to turn off display") } offTicker.Stop() @@ -163,7 +306,7 @@ func wakeDisplay(force bool) { err := setDisplayBrightness(config.DisplayMaxBrightness) if err != nil { - logger.Warnf("display wake failed, %s", err) + displayLogger.Warn().Err(err).Msg("failed to wake display") } if config.DisplayDimAfterSec != 0 { @@ -183,7 +326,7 @@ func wakeDisplay(force bool) { func watchTsEvents() { ts, err := os.OpenFile(touchscreenDevice, os.O_RDONLY, 0666) if err != nil { - logger.Warnf("display: failed to open touchscreen device: %s", err) + displayLogger.Warn().Err(err).Msg("failed to open touchscreen device") return } @@ -196,7 +339,7 @@ func watchTsEvents() { for { _, err := ts.Read(buf) if err != nil { - logger.Warnf("display: failed to read from touchscreen device: %s", err) + displayLogger.Warn().Err(err).Msg("failed to read from touchscreen device") return } @@ -215,13 +358,21 @@ func startBacklightTickers() { return } - if dimTicker == nil && config.DisplayDimAfterSec != 0 { - logger.Info("display: dim_ticker has started") + // Stop existing tickers to prevent multiple active instances on repeated calls + if dimTicker != nil { + dimTicker.Stop() + } + + if offTicker != nil { + offTicker.Stop() + } + + if config.DisplayDimAfterSec != 0 { + displayLogger.Info().Msg("dim_ticker has started") dimTicker = time.NewTicker(time.Duration(config.DisplayDimAfterSec) * time.Second) - defer dimTicker.Stop() go func() { - for { //nolint:gosimple + for { //nolint:staticcheck select { case <-dimTicker.C: tick_displayDim() @@ -230,13 +381,12 @@ func startBacklightTickers() { }() } - if offTicker == nil && config.DisplayOffAfterSec != 0 { - logger.Info("display: off_ticker has started") + if config.DisplayOffAfterSec != 0 { + displayLogger.Info().Msg("off_ticker has started") offTicker = time.NewTicker(time.Duration(config.DisplayOffAfterSec) * time.Second) - defer offTicker.Stop() go func() { - for { //nolint:gosimple + for { //nolint:staticcheck select { case <-offTicker.C: tick_displayOff() @@ -246,19 +396,18 @@ func startBacklightTickers() { } } -func init() { - ensureConfigLoaded() - +func initDisplay() { go func() { waitCtrlClientConnected() - logger.Info("setting initial display contents") + displayLogger.Info().Msg("setting initial display contents") time.Sleep(500 * time.Millisecond) + _, _ = lvDispSetRotation(config.DisplayRotation) updateStaticContents() displayInited = true - logger.Info("display inited") + displayLogger.Info().Msg("display inited") startBacklightTickers() wakeDisplay(true) - requestDisplayUpdate() + requestDisplayUpdate(true) }() go watchTsEvents() diff --git a/fuse.go b/fuse.go index 29b11f7..19f144f 100644 --- a/fuse.go +++ b/fuse.go @@ -37,7 +37,7 @@ func (f *WebRTCStreamFile) Getattr(ctx context.Context, fh fs.FileHandle, out *f f.mu.Lock() defer f.mu.Unlock() out.Attr = f.Attr - out.Attr.Size = f.size + out.Size = f.size return fs.OK } @@ -103,7 +103,7 @@ func RunFuseServer() { var err error fuseServer, err = fs.Mount(fuseMountPoint, &FuseRoot{}, opts) if err != nil { - logger.Warnf("failed to mount fuse: %v", err) + logger.Warn().Err(err).Msg("failed to mount fuse") } fuseServer.Wait() } diff --git a/go.mod b/go.mod index a2bbed5..3e41071 100644 --- a/go.mod +++ b/go.mod @@ -1,86 +1,91 @@ module github.com/jetkvm/kvm -go 1.21.0 - -toolchain go1.21.1 +go 1.24.4 require ( - github.com/Masterminds/semver/v3 v3.3.0 - github.com/beevik/ntp v1.3.1 - github.com/coder/websocket v1.8.12 - github.com/coreos/go-oidc/v3 v3.11.0 - github.com/creack/pty v1.1.23 - github.com/gin-gonic/gin v1.9.1 + github.com/Masterminds/semver/v3 v3.4.0 + github.com/beevik/ntp v1.4.3 + github.com/coder/websocket v1.8.13 + github.com/coreos/go-oidc/v3 v3.14.1 + github.com/creack/pty v1.1.24 + github.com/fsnotify/fsnotify v1.9.0 + github.com/gin-contrib/logger v1.2.6 + github.com/gin-gonic/gin v1.10.1 + github.com/go-co-op/gocron/v2 v2.16.3 github.com/google/uuid v1.6.0 - github.com/gwatts/rootcerts v0.0.0-20240401182218-3ab9db955caf - github.com/hanwen/go-fuse/v2 v2.5.1 - github.com/hashicorp/go-envparse v0.1.0 - github.com/pion/logging v0.2.2 + github.com/guregu/null/v6 v6.0.0 + github.com/gwatts/rootcerts v0.0.0-20250601184604-370a9a75f341 + github.com/hanwen/go-fuse/v2 v2.8.0 + github.com/pion/logging v0.2.4 github.com/pion/mdns/v2 v2.0.7 - github.com/pion/webrtc/v4 v4.0.0 + github.com/pion/webrtc/v4 v4.1.3 github.com/pojntfx/go-nbd v0.3.2 - github.com/prometheus/client_golang v1.21.0 - github.com/prometheus/common v0.62.0 + github.com/prometheus/client_golang v1.22.0 + github.com/prometheus/common v0.65.0 + github.com/prometheus/procfs v0.16.1 github.com/psanford/httpreadat v0.1.0 - github.com/vishvananda/netlink v1.3.0 - go.bug.st/serial v1.6.2 - golang.org/x/crypto v0.31.0 - golang.org/x/net v0.33.0 + github.com/rs/zerolog v1.34.0 + github.com/sourcegraph/tf-dag v0.2.2-0.20250131204052-3e8ff1477b4f + github.com/stretchr/testify v1.10.0 + github.com/vishvananda/netlink v1.3.1 + go.bug.st/serial v1.6.4 + golang.org/x/crypto v0.40.0 + golang.org/x/net v0.41.0 + golang.org/x/sys v0.34.0 ) replace github.com/pojntfx/go-nbd v0.3.2 => github.com/chemhack/go-nbd v0.0.0-20241006125820-59e45f5b1e7b require ( github.com/beorn7/perks v1.0.1 // indirect - github.com/bytedance/sonic v1.11.6 // indirect - github.com/bytedance/sonic/loader v0.1.1 // indirect + github.com/bytedance/sonic v1.13.3 // indirect + github.com/bytedance/sonic/loader v0.2.4 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect - github.com/cloudwego/base64x v0.1.4 // indirect - github.com/cloudwego/iasm v0.2.0 // indirect - github.com/creack/goselect v0.1.2 // indirect - github.com/gabriel-vasile/mimetype v1.4.3 // indirect - github.com/gin-contrib/sse v0.1.0 // indirect - github.com/go-co-op/gocron/v2 v2.16.1 // indirect - github.com/go-jose/go-jose/v4 v4.0.2 // indirect + github.com/cloudwego/base64x v0.1.5 // indirect + github.com/creack/goselect v0.1.3 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/gabriel-vasile/mimetype v1.4.9 // indirect + github.com/gin-contrib/sse v1.1.0 // indirect + github.com/go-jose/go-jose/v4 v4.1.0 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect - github.com/go-playground/validator/v10 v10.20.0 // indirect - github.com/goccy/go-json v0.10.2 // indirect + github.com/go-playground/validator/v10 v10.26.0 // indirect + github.com/goccy/go-json v0.10.5 // indirect github.com/jonboulle/clockwork v0.5.0 // indirect github.com/json-iterator/go v1.1.12 // indirect - github.com/klauspost/compress v1.17.11 // indirect - github.com/klauspost/cpuid/v2 v2.2.7 // indirect + github.com/klauspost/cpuid/v2 v2.2.10 // indirect github.com/leodido/go-urn v1.4.0 // indirect + github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect - github.com/pelletier/go-toml/v2 v2.2.2 // indirect - github.com/pilebones/go-udev v0.9.0 // indirect - github.com/pion/datachannel v1.5.9 // indirect - github.com/pion/dtls/v3 v3.0.3 // indirect - github.com/pion/ice/v4 v4.0.2 // indirect - github.com/pion/interceptor v0.1.37 // indirect + github.com/pelletier/go-toml/v2 v2.2.4 // indirect + github.com/pilebones/go-udev v0.9.1 // indirect + github.com/pion/datachannel v1.5.10 // indirect + github.com/pion/dtls/v3 v3.0.6 // indirect + github.com/pion/ice/v4 v4.0.10 // indirect + github.com/pion/interceptor v0.1.40 // indirect github.com/pion/randutil v0.1.0 // indirect - github.com/pion/rtcp v1.2.14 // indirect - github.com/pion/rtp v1.8.9 // indirect - github.com/pion/sctp v1.8.33 // indirect - github.com/pion/sdp/v3 v3.0.9 // indirect - github.com/pion/srtp/v3 v3.0.4 // indirect + github.com/pion/rtcp v1.2.15 // indirect + github.com/pion/rtp v1.8.20 // indirect + github.com/pion/sctp v1.8.39 // indirect + github.com/pion/sdp/v3 v3.0.14 // indirect + github.com/pion/srtp/v3 v3.0.6 // indirect github.com/pion/stun/v3 v3.0.0 // indirect github.com/pion/transport/v3 v3.0.7 // indirect - github.com/pion/turn/v4 v4.0.0 // indirect - github.com/prometheus/client_model v0.6.1 // indirect - github.com/prometheus/procfs v0.15.1 // indirect + github.com/pion/turn/v4 v4.0.2 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/prometheus/client_model v0.6.2 // indirect github.com/robfig/cron/v3 v3.0.1 // indirect + github.com/rogpeppe/go-internal v1.14.1 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect - github.com/ugorji/go/codec v1.2.12 // indirect - github.com/vishvananda/netns v0.0.4 // indirect + github.com/ugorji/go/codec v1.3.0 // indirect + github.com/vishvananda/netns v0.0.5 // indirect github.com/wlynxg/anet v0.0.5 // indirect - golang.org/x/arch v0.8.0 // indirect - golang.org/x/oauth2 v0.24.0 // indirect - golang.org/x/sys v0.28.0 // indirect - golang.org/x/text v0.21.0 // indirect - google.golang.org/protobuf v1.36.1 // indirect + golang.org/x/arch v0.18.0 // indirect + golang.org/x/oauth2 v0.30.0 // indirect + golang.org/x/text v0.27.0 // indirect + google.golang.org/protobuf v1.36.6 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 134074b..6b75ad1 100644 --- a/go.sum +++ b/go.sum @@ -1,72 +1,78 @@ -github.com/Masterminds/semver/v3 v3.3.0 h1:B8LGeaivUe71a5qox1ICM/JLl0NqZSW5CHyL+hmvYS0= -github.com/Masterminds/semver/v3 v3.3.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= -github.com/beevik/ntp v1.3.1 h1:Y/srlT8L1yQr58kyPWFPZIxRL8ttx2SRIpVYJqZIlAM= -github.com/beevik/ntp v1.3.1/go.mod h1:fT6PylBq86Tsq23ZMEe47b7QQrZfYBFPnpzt0a9kJxw= +github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= +github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= +github.com/beevik/ntp v1.4.3 h1:PlbTvE5NNy4QHmA4Mg57n7mcFTmr1W1j3gcK7L1lqho= +github.com/beevik/ntp v1.4.3/go.mod h1:Unr8Zg+2dRn7d8bHFuehIMSvvUYssHMxW3Q5Nx4RW5Q= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= -github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0= -github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4= -github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM= +github.com/bytedance/sonic v1.13.3 h1:MS8gmaH16Gtirygw7jV91pDCN33NyMrPbN7qiYhEsF0= +github.com/bytedance/sonic v1.13.3/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4= github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= +github.com/bytedance/sonic/loader v0.2.4 h1:ZWCw4stuXUsn1/+zQDqeE7JKP+QO47tz7QCNan80NzY= +github.com/bytedance/sonic/loader v0.2.4/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/chemhack/go-nbd v0.0.0-20241006125820-59e45f5b1e7b h1:dSbDgy72Y1sjLPWLv7vs0fMFuhMBMViiT9PJZiZWZNs= github.com/chemhack/go-nbd v0.0.0-20241006125820-59e45f5b1e7b/go.mod h1:SehHnbi2e8NiSAKby42Itm8SIoS7b+wAprsfPH3qgYk= -github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y= -github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= -github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg= +github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4= +github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= -github.com/coder/websocket v1.8.12 h1:5bUXkEPPIbewrnkU8LTCLVaxi4N4J8ahufH2vlo4NAo= -github.com/coder/websocket v1.8.12/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs= -github.com/coreos/go-oidc/v3 v3.11.0 h1:Ia3MxdwpSw702YW0xgfmP1GVCMA9aEFWu12XUZ3/OtI= -github.com/coreos/go-oidc/v3 v3.11.0/go.mod h1:gE3LgjOgFoHi9a4ce4/tJczr0Ai2/BoDhf0r5lltWI0= -github.com/creack/goselect v0.1.2 h1:2DNy14+JPjRBgPzAd1thbQp4BSIihxcBf0IXhQXDRa0= -github.com/creack/goselect v0.1.2/go.mod h1:a/NhLweNvqIYMuxcMOuWY516Cimucms3DglDzQP3hKY= -github.com/creack/pty v1.1.23 h1:4M6+isWdcStXEf15G/RbrMPOQj1dZ7HPZCGwE4kOeP0= -github.com/creack/pty v1.1.23/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE= +github.com/coder/websocket v1.8.13 h1:f3QZdXy7uGVz+4uCJy2nTZyM0yTBj8yANEHhqlXZ9FE= +github.com/coder/websocket v1.8.13/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs= +github.com/coreos/go-oidc/v3 v3.14.1 h1:9ePWwfdwC4QKRlCXsJGou56adA/owXczOzwKdOumLqk= +github.com/coreos/go-oidc/v3 v3.14.1/go.mod h1:HaZ3szPaZ0e4r6ebqvsLWlk2Tn+aejfmrfah6hnSYEU= +github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/creack/goselect v0.1.3 h1:MaGNMclRo7P2Jl21hBpR1Cn33ITSbKP6E49RtfblLKc= +github.com/creack/goselect v0.1.3/go.mod h1:a/NhLweNvqIYMuxcMOuWY516Cimucms3DglDzQP3hKY= +github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s= +github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= -github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= -github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= -github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= -github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg= -github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU= -github.com/go-co-op/gocron/v2 v2.16.1 h1:ux/5zxVRveCaCuTtNI3DiOk581KC1KpJbpJFYUEVYwo= -github.com/go-co-op/gocron/v2 v2.16.1/go.mod h1:opexeOFy5BplhsKdA7bzY9zeYih8I8/WNJ4arTIFPVc= -github.com/go-jose/go-jose/v4 v4.0.2 h1:R3l3kkBds16bO7ZFAEEcofK0MkrAJt3jlJznWZG0nvk= -github.com/go-jose/go-jose/v4 v4.0.2/go.mod h1:WVf9LFMHh/QVrmqrOfqun0C45tMe3RoiKJMPvgWwLfY= +github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= +github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY= +github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok= +github.com/gin-contrib/logger v1.2.6 h1:EPolruKUTzNXMVBD9LuAFQmRjTs7AH7yKGuXgYqrKWc= +github.com/gin-contrib/logger v1.2.6/go.mod h1:7niPrd7F0Nscw/zvgz8RiGJxSdbKM2yfQNy8xCHcm64= +github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w= +github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM= +github.com/gin-gonic/gin v1.10.1 h1:T0ujvqyCSqRopADpgPgiTT63DUQVSfojyME59Ei63pQ= +github.com/gin-gonic/gin v1.10.1/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= +github.com/go-co-op/gocron/v2 v2.16.3 h1:kYqukZqBa8RC2+AFAHnunmKcs9GRTjwBo8WRF3I6cbI= +github.com/go-co-op/gocron/v2 v2.16.3/go.mod h1:aTf7/+5Jo2E+cyAqq625UQ6DzpkV96b22VHIUAt6l3c= +github.com/go-jose/go-jose/v4 v4.1.0 h1:cYSYxd3pw5zd2FSXk2vGdn9igQU2PS8MuxrCOCl0FdY= +github.com/go-jose/go-jose/v4 v4.1.0/go.mod h1:GG/vqmYm3Von2nYiB2vGTXzdoNKE5tix5tuc6iAd+sw= github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= -github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8= -github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= -github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= -github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= -github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/go-playground/validator/v10 v10.26.0 h1:SP05Nqhjcvz81uJaRfEV0YBSSSGMc/iMaVtFbr3Sw2k= +github.com/go-playground/validator/v10 v10.26.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo= +github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= +github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/gwatts/rootcerts v0.0.0-20240401182218-3ab9db955caf h1:JO6ISZIvEUitto5zjQ3/VEnDM5rPbqIFuOhS0U0ByeA= -github.com/gwatts/rootcerts v0.0.0-20240401182218-3ab9db955caf/go.mod h1:5Kt9XkWvkGi2OHOq0QsGxebHmhCcqJ8KCbNg/a6+n+g= -github.com/hanwen/go-fuse/v2 v2.5.1 h1:OQBE8zVemSocRxA4OaFJbjJ5hlpCmIWbGr7r0M4uoQQ= -github.com/hanwen/go-fuse/v2 v2.5.1/go.mod h1:xKwi1cF7nXAOBCXujD5ie0ZKsxc8GGSA1rlMJc+8IJs= -github.com/hashicorp/go-envparse v0.1.0 h1:bE++6bhIsNCPLvgDZkYqo3nA+/PFI51pkrHdmPSDFPY= -github.com/hashicorp/go-envparse v0.1.0/go.mod h1:OHheN1GoygLlAkTlXLXvAdnXdZxy8JUweQ1rAXx1xnc= +github.com/guregu/null/v6 v6.0.0 h1:N14VRS+4di81i1PXRiprbQJ9EM9gqBa0+KVMeS/QSjQ= +github.com/guregu/null/v6 v6.0.0/go.mod h1:hrMIhIfrOZeLPZhROSn149tpw2gHkidAqxoXNyeX3iQ= +github.com/gwatts/rootcerts v0.0.0-20250601184604-370a9a75f341 h1:zPrkLSKi7kKJoNJH4uUmsQ86+0/QqpwEns0NyNLwKv0= +github.com/gwatts/rootcerts v0.0.0-20250601184604-370a9a75f341/go.mod h1:5Kt9XkWvkGi2OHOq0QsGxebHmhCcqJ8KCbNg/a6+n+g= +github.com/hanwen/go-fuse/v2 v2.8.0 h1:wV8rG7rmCz8XHSOwBZhG5YcVqcYjkzivjmbaMafPlAs= +github.com/hanwen/go-fuse/v2 v2.8.0/go.mod h1:yE6D2PqWwm3CbYRxFXV9xUd8Md5d6NG0WBs5spCswmI= github.com/jonboulle/clockwork v0.5.0 h1:Hyh9A8u51kptdkR+cqRpT1EebBwTn1oK9YfGYbdFz6I= github.com/jonboulle/clockwork v0.5.0/go.mod h1:3mZlmanh0g2NDKO5TWZVJAfofYk64M7XN3SzBPjZF60= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= -github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= -github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= +github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= +github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= -github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM= -github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= +github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE= +github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= @@ -75,15 +81,19 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348/go.mod h1:B69LEHPfb2qLo0BaaOLcbitczOKLWTsrBG9LczfCD4k= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= +github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/moby/sys/mountinfo v0.6.2 h1:BzJjoreD5BMFNmD9Rus6gdd1pLuecOFPt8wC+Vygl78= -github.com/moby/sys/mountinfo v0.6.2/go.mod h1:IJb6JQeOklcdMU9F5xQ8ZALD+CUr5VlGpwtX+VE0rpI= +github.com/moby/sys/mountinfo v0.7.2 h1:1shs6aH5s4o5H2zQLn796ADW1wMrIwHsyJ2v9KouLrg= +github.com/moby/sys/mountinfo v0.7.2/go.mod h1:1YOa8w8Ih7uW0wALDUgT1dTTSBrZ+HiBLGws92L2RU4= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -91,104 +101,107 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= -github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= -github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= -github.com/pilebones/go-udev v0.9.0 h1:N1uEO/SxUwtIctc0WLU0t69JeBxIYEYnj8lT/Nabl9Q= -github.com/pilebones/go-udev v0.9.0/go.mod h1:T2eI2tUSK0hA2WS5QLjXJUfQkluZQu+18Cqvem3CaXI= -github.com/pion/datachannel v1.5.9 h1:LpIWAOYPyDrXtU+BW7X0Yt/vGtYxtXQ8ql7dFfYUVZA= -github.com/pion/datachannel v1.5.9/go.mod h1:kDUuk4CU4Uxp82NH4LQZbISULkX/HtzKa4P7ldf9izE= -github.com/pion/dtls/v3 v3.0.3 h1:j5ajZbQwff7Z8k3pE3S+rQ4STvKvXUdKsi/07ka+OWM= -github.com/pion/dtls/v3 v3.0.3/go.mod h1:weOTUyIV4z0bQaVzKe8kpaP17+us3yAuiQsEAG1STMU= -github.com/pion/ice/v4 v4.0.2 h1:1JhBRX8iQLi0+TfcavTjPjI6GO41MFn4CeTBX+Y9h5s= -github.com/pion/ice/v4 v4.0.2/go.mod h1:DCdqyzgtsDNYN6/3U8044j3U7qsJ9KFJC92VnOWHvXg= -github.com/pion/interceptor v0.1.37 h1:aRA8Zpab/wE7/c0O3fh1PqY0AJI3fCSEM5lRWJVorwI= -github.com/pion/interceptor v0.1.37/go.mod h1:JzxbJ4umVTlZAf+/utHzNesY8tmRkM2lVmkS82TTj8Y= -github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY= -github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms= +github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= +github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= +github.com/pilebones/go-udev v0.9.1 h1:uN72M1C1fgzhsVmBGEM8w9RD1JY4iVsPZpr+Z6rb3O8= +github.com/pilebones/go-udev v0.9.1/go.mod h1:Bgcl07crebF3JSeS4+nuaRvhWFdCeFoBhXXeAp93XNo= +github.com/pion/datachannel v1.5.10 h1:ly0Q26K1i6ZkGf42W7D4hQYR90pZwzFOjTq5AuCKk4o= +github.com/pion/datachannel v1.5.10/go.mod h1:p/jJfC9arb29W7WrxyKbepTU20CFgyx5oLo8Rs4Py/M= +github.com/pion/dtls/v3 v3.0.6 h1:7Hkd8WhAJNbRgq9RgdNh1aaWlZlGpYTzdqjy9x9sK2E= +github.com/pion/dtls/v3 v3.0.6/go.mod h1:iJxNQ3Uhn1NZWOMWlLxEEHAN5yX7GyPvvKw04v9bzYU= +github.com/pion/ice/v4 v4.0.10 h1:P59w1iauC/wPk9PdY8Vjl4fOFL5B+USq1+xbDcN6gT4= +github.com/pion/ice/v4 v4.0.10/go.mod h1:y3M18aPhIxLlcO/4dn9X8LzLLSma84cx6emMSu14FGw= +github.com/pion/interceptor v0.1.40 h1:e0BjnPcGpr2CFQgKhrQisBU7V3GXK6wrfYrGYaU6Jq4= +github.com/pion/interceptor v0.1.40/go.mod h1:Z6kqH7M/FYirg3frjGJ21VLSRJGBXB/KqaTIrdqnOic= +github.com/pion/logging v0.2.4 h1:tTew+7cmQ+Mc1pTBLKH2puKsOvhm32dROumOZ655zB8= +github.com/pion/logging v0.2.4/go.mod h1:DffhXTKYdNZU+KtJ5pyQDjvOAh/GsNSyv1lbkFbe3so= github.com/pion/mdns/v2 v2.0.7 h1:c9kM8ewCgjslaAmicYMFQIde2H9/lrZpjBkN8VwoVtM= github.com/pion/mdns/v2 v2.0.7/go.mod h1:vAdSYNAT0Jy3Ru0zl2YiW3Rm/fJCwIeM0nToenfOJKA= github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA= github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8= -github.com/pion/rtcp v1.2.14 h1:KCkGV3vJ+4DAJmvP0vaQShsb0xkRfWkO540Gy102KyE= -github.com/pion/rtcp v1.2.14/go.mod h1:sn6qjxvnwyAkkPzPULIbVqSKI5Dv54Rv7VG0kNxh9L4= -github.com/pion/rtp v1.8.9 h1:E2HX740TZKaqdcPmf4pw6ZZuG8u5RlMMt+l3dxeu6Wk= -github.com/pion/rtp v1.8.9/go.mod h1:pBGHaFt/yW7bf1jjWAoUjpSNoDnw98KTMg+jWWvziqU= -github.com/pion/sctp v1.8.33 h1:dSE4wX6uTJBcNm8+YlMg7lw1wqyKHggsP5uKbdj+NZw= -github.com/pion/sctp v1.8.33/go.mod h1:beTnqSzewI53KWoG3nqB282oDMGrhNxBdb+JZnkCwRM= -github.com/pion/sdp/v3 v3.0.9 h1:pX++dCHoHUwq43kuwf3PyJfHlwIj4hXA7Vrifiq0IJY= -github.com/pion/sdp/v3 v3.0.9/go.mod h1:B5xmvENq5IXJimIO4zfp6LAe1fD9N+kFv+V/1lOdz8M= -github.com/pion/srtp/v3 v3.0.4 h1:2Z6vDVxzrX3UHEgrUyIGM4rRouoC7v+NiF1IHtp9B5M= -github.com/pion/srtp/v3 v3.0.4/go.mod h1:1Jx3FwDoxpRaTh1oRV8A/6G1BnFL+QI82eK4ms8EEJQ= +github.com/pion/rtcp v1.2.15 h1:LZQi2JbdipLOj4eBjK4wlVoQWfrZbh3Q6eHtWtJBZBo= +github.com/pion/rtcp v1.2.15/go.mod h1:jlGuAjHMEXwMUHK78RgX0UmEJFV4zUKOFHR7OP+D3D0= +github.com/pion/rtp v1.8.20 h1:8zcyqohadZE8FCBeGdyEvHiclPIezcwRQH9zfapFyYI= +github.com/pion/rtp v1.8.20/go.mod h1:bAu2UFKScgzyFqvUKmbvzSdPr+NGbZtv6UB2hesqXBk= +github.com/pion/sctp v1.8.39 h1:PJma40vRHa3UTO3C4MyeJDQ+KIobVYRZQZ0Nt7SjQnE= +github.com/pion/sctp v1.8.39/go.mod h1:cNiLdchXra8fHQwmIoqw0MbLLMs+f7uQ+dGMG2gWebE= +github.com/pion/sdp/v3 v3.0.14 h1:1h7gBr9FhOWH5GjWWY5lcw/U85MtdcibTyt/o6RxRUI= +github.com/pion/sdp/v3 v3.0.14/go.mod h1:88GMahN5xnScv1hIMTqLdu/cOcUkj6a9ytbncwMCq2E= +github.com/pion/srtp/v3 v3.0.6 h1:E2gyj1f5X10sB/qILUGIkL4C2CqK269Xq167PbGCc/4= +github.com/pion/srtp/v3 v3.0.6/go.mod h1:BxvziG3v/armJHAaJ87euvkhHqWe9I7iiOy50K2QkhY= github.com/pion/stun/v3 v3.0.0 h1:4h1gwhWLWuZWOJIJR9s2ferRO+W3zA/b6ijOI6mKzUw= github.com/pion/stun/v3 v3.0.0/go.mod h1:HvCN8txt8mwi4FBvS3EmDghW6aQJ24T+y+1TKjB5jyU= github.com/pion/transport/v3 v3.0.7 h1:iRbMH05BzSNwhILHoBoAPxoB9xQgOaJk+591KC9P1o0= github.com/pion/transport/v3 v3.0.7/go.mod h1:YleKiTZ4vqNxVwh77Z0zytYi7rXHl7j6uPLGhhz9rwo= -github.com/pion/turn/v4 v4.0.0 h1:qxplo3Rxa9Yg1xXDxxH8xaqcyGUtbHYw4QSCvmFWvhM= -github.com/pion/turn/v4 v4.0.0/go.mod h1:MuPDkm15nYSklKpN8vWJ9W2M0PlyQZqYt1McGuxG7mA= -github.com/pion/webrtc/v4 v4.0.0 h1:x8ec7uJQPP3D1iI8ojPAiTOylPI7Fa7QgqZrhpLyqZ8= -github.com/pion/webrtc/v4 v4.0.0/go.mod h1:SfNn8CcFxR6OUVjLXVslAQ3a3994JhyE3Hw1jAuqEto= +github.com/pion/turn/v4 v4.0.2 h1:ZqgQ3+MjP32ug30xAbD6Mn+/K4Sxi3SdNOTFf+7mpps= +github.com/pion/turn/v4 v4.0.2/go.mod h1:pMMKP/ieNAG/fN5cZiN4SDuyKsXtNTr0ccN7IToA1zs= +github.com/pion/webrtc/v4 v4.1.3 h1:YZ67Boj9X/hk190jJZ8+HFGQ6DqSZ/fYP3sLAZv7c3c= +github.com/pion/webrtc/v4 v4.1.3/go.mod h1:rsq+zQ82ryfR9vbb0L1umPJ6Ogq7zm8mcn9fcGnxomM= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/prometheus/client_golang v1.21.0 h1:DIsaGmiaBkSangBgMtWdNfxbMNdku5IK6iNhrEqWvdA= -github.com/prometheus/client_golang v1.21.0/go.mod h1:U9NM32ykUErtVBxdvD3zfi+EuFkkaBvMb09mIfe0Zgg= -github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= -github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= -github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io= -github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I= -github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= -github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= +github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q= +github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0= +github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= +github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= +github.com/prometheus/common v0.65.0 h1:QDwzd+G1twt//Kwj/Ww6E9FQq1iVMmODnILtW1t2VzE= +github.com/prometheus/common v0.65.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8= +github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= +github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= github.com/psanford/httpreadat v0.1.0 h1:VleW1HS2zO7/4c7c7zNl33fO6oYACSagjJIyMIwZLUE= github.com/psanford/httpreadat v0.1.0/go.mod h1:Zg7P+TlBm3bYbyHTKv/EdtSJZn3qwbPwpfZ/I9GKCRE= github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= -github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= -github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= +github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY= +github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ= +github.com/sourcegraph/tf-dag v0.2.2-0.20250131204052-3e8ff1477b4f h1:VgoRCP1efSCEZIcF2THLQ46+pIBzzgNiaUBe9wEDwYU= +github.com/sourcegraph/tf-dag v0.2.2-0.20250131204052-3e8ff1477b4f/go.mod h1:pzro7BGorij2WgrjEammtrkbo3+xldxo+KaGLGUiD+Q= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= -github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= -github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= -github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= -github.com/vishvananda/netlink v1.3.0 h1:X7l42GfcV4S6E4vHTsw48qbrV+9PVojNfIhZcwQdrZk= -github.com/vishvananda/netlink v1.3.0/go.mod h1:i6NetklAujEcC6fK0JPjT8qSwWyO0HLn4UKG+hGqeJs= -github.com/vishvananda/netns v0.0.4 h1:Oeaw1EM2JMxD51g9uhtC0D7erkIjgmj8+JZc26m1YX8= -github.com/vishvananda/netns v0.0.4/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM= +github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA= +github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4= +github.com/vishvananda/netlink v1.3.1 h1:3AEMt62VKqz90r0tmNhog0r/PpWKmrEShJU0wJW6bV0= +github.com/vishvananda/netlink v1.3.1/go.mod h1:ARtKouGSTGchR8aMwmkzC0qiNPrrWO5JS/XMVl45+b4= +github.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zdEY= +github.com/vishvananda/netns v0.0.5/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM= github.com/wlynxg/anet v0.0.5 h1:J3VJGi1gvo0JwZ/P1/Yc/8p63SoW98B5dHkYDmpgvvU= github.com/wlynxg/anet v0.0.5/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA= -go.bug.st/serial v1.6.2 h1:kn9LRX3sdm+WxWKufMlIRndwGfPWsH1/9lCWXQCasq8= -go.bug.st/serial v1.6.2/go.mod h1:UABfsluHAiaNI+La2iESysd9Vetq7VRdpxvjx7CmmOE= -golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= -golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc= -golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= -golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= -golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= -golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= -golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= -golang.org/x/oauth2 v0.24.0 h1:KTBBxWqUa0ykRPLtV69rRto9TLXcqYkeswu48x/gvNE= -golang.org/x/oauth2 v0.24.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= -golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +go.bug.st/serial v1.6.4 h1:7FmqNPgVp3pu2Jz5PoPtbZ9jJO5gnEnZIvnI1lzve8A= +go.bug.st/serial v1.6.4/go.mod h1:nofMJxTeNVny/m6+KaafC6vJGj3miwQZ6vW4BZUGJPI= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +golang.org/x/arch v0.18.0 h1:WN9poc33zL4AzGxqf8VtpKUnGvMi8O9lhNyBMF/85qc= +golang.org/x/arch v0.18.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk= +golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM= +golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY= +golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw= +golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA= +golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= +golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= -golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= -golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= -google.golang.org/protobuf v1.36.1 h1:yBPeRvTftaleIgM3PZ/WBIZ7XM/eEYAaEyCwvyjq/gk= -google.golang.org/protobuf v1.36.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA= +golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4= +golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU= +google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= +google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= @@ -196,4 +209,3 @@ gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= -rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= diff --git a/hw.go b/hw.go index 03e9d4b..20d88eb 100644 --- a/hw.go +++ b/hw.go @@ -4,6 +4,7 @@ import ( "fmt" "os" "regexp" + "strings" "sync" "time" ) @@ -42,7 +43,7 @@ func GetDeviceID() string { deviceIDOnce.Do(func() { serial, err := extractSerialNumber() if err != nil { - logger.Warn("unknown serial number, the program likely not running on RV1106") + logger.Warn().Msg("unknown serial number, the program likely not running on RV1106") deviceID = "unknown_device_id" } else { deviceID = serial @@ -51,10 +52,19 @@ func GetDeviceID() string { return deviceID } +func GetDefaultHostname() string { + deviceId := GetDeviceID() + if deviceId == "unknown_device_id" { + return "jetkvm" + } + + return fmt.Sprintf("jetkvm-%s", strings.ToLower(deviceId)) +} + func runWatchdog() { file, err := os.OpenFile("/dev/watchdog", os.O_WRONLY, 0) if err != nil { - logger.Warnf("unable to open /dev/watchdog: %v, skipping watchdog reset", err) + watchdogLogger.Warn().Err(err).Msg("unable to open /dev/watchdog, skipping watchdog reset") return } defer file.Close() @@ -65,13 +75,13 @@ func runWatchdog() { case <-ticker.C: _, err = file.Write([]byte{0}) if err != nil { - logger.Errorf("error writing to /dev/watchdog, system may reboot: %v", err) + watchdogLogger.Warn().Err(err).Msg("error writing to /dev/watchdog, system may reboot") } case <-appCtx.Done(): //disarm watchdog with magic value _, err := file.Write([]byte("V")) if err != nil { - logger.Errorf("failed to disarm watchdog, system may reboot: %v", err) + watchdogLogger.Warn().Err(err).Msg("failed to disarm watchdog, system may reboot") } return } diff --git a/internal/confparser/confparser.go b/internal/confparser/confparser.go new file mode 100644 index 0000000..5ccd1cb --- /dev/null +++ b/internal/confparser/confparser.go @@ -0,0 +1,386 @@ +package confparser + +import ( + "fmt" + "net" + "net/url" + "reflect" + "slices" + "strconv" + "strings" + + "github.com/guregu/null/v6" + "golang.org/x/net/idna" +) + +type FieldConfig struct { + Name string + Required bool + RequiredIf map[string]interface{} + OneOf []string + ValidateTypes []string + Defaults interface{} + IsEmpty bool + CurrentValue interface{} + TypeString string + Delegated bool + shouldUpdateValue bool +} + +func SetDefaultsAndValidate(config interface{}) error { + return setDefaultsAndValidate(config, true) +} + +func setDefaultsAndValidate(config interface{}, isRoot bool) error { + // first we need to check if the config is a pointer + if reflect.TypeOf(config).Kind() != reflect.Ptr { + return fmt.Errorf("config is not a pointer") + } + + // now iterate over the lease struct and set the values + configType := reflect.TypeOf(config).Elem() + configValue := reflect.ValueOf(config).Elem() + + fields := make(map[string]FieldConfig) + + for i := 0; i < configType.NumField(); i++ { + field := configType.Field(i) + fieldValue := configValue.Field(i) + + defaultValue := field.Tag.Get("default") + + fieldType := field.Type.String() + + fieldConfig := FieldConfig{ + Name: field.Name, + OneOf: splitString(field.Tag.Get("one_of")), + ValidateTypes: splitString(field.Tag.Get("validate_type")), + RequiredIf: make(map[string]interface{}), + CurrentValue: fieldValue.Interface(), + IsEmpty: false, + TypeString: fieldType, + } + + // check if the field is required + required := field.Tag.Get("required") + if required != "" { + requiredBool, _ := strconv.ParseBool(required) + fieldConfig.Required = requiredBool + } + + var canUseOneOff = false + + // use switch to get the type + switch fieldValue.Interface().(type) { + case string, null.String: + if defaultValue != "" { + fieldConfig.Defaults = defaultValue + } + canUseOneOff = true + case []string: + if defaultValue != "" { + fieldConfig.Defaults = strings.Split(defaultValue, ",") + } + canUseOneOff = true + case int, null.Int: + if defaultValue != "" { + defaultValueInt, err := strconv.Atoi(defaultValue) + if err != nil { + return fmt.Errorf("invalid default value for field `%s`: %s", field.Name, defaultValue) + } + + fieldConfig.Defaults = defaultValueInt + } + case bool, null.Bool: + if defaultValue != "" { + defaultValueBool, err := strconv.ParseBool(defaultValue) + if err != nil { + return fmt.Errorf("invalid default value for field `%s`: %s", field.Name, defaultValue) + } + + fieldConfig.Defaults = defaultValueBool + } + default: + if defaultValue != "" { + return fmt.Errorf("field `%s` cannot use default value: unsupported type: %s", field.Name, fieldType) + } + + // check if it's a pointer + if fieldValue.Kind() == reflect.Ptr { + // check if the pointer is nil + if fieldValue.IsNil() { + fieldConfig.IsEmpty = true + } else { + fieldConfig.CurrentValue = fieldValue.Elem().Addr() + fieldConfig.Delegated = true + } + } else { + fieldConfig.Delegated = true + } + } + + // now check if the field is nullable interface + switch fieldValue.Interface().(type) { + case null.String: + if fieldValue.Interface().(null.String).IsZero() { + fieldConfig.IsEmpty = true + } + case null.Int: + if fieldValue.Interface().(null.Int).IsZero() { + fieldConfig.IsEmpty = true + } + case null.Bool: + if fieldValue.Interface().(null.Bool).IsZero() { + fieldConfig.IsEmpty = true + } + case []string: + if len(fieldValue.Interface().([]string)) == 0 { + fieldConfig.IsEmpty = true + } + } + + // now check if the field has required_if + requiredIf := field.Tag.Get("required_if") + if requiredIf != "" { + requiredIfParts := strings.Split(requiredIf, ",") + for _, part := range requiredIfParts { + partVal := strings.SplitN(part, "=", 2) + if len(partVal) != 2 { + return fmt.Errorf("invalid required_if for field `%s`: %s", field.Name, requiredIf) + } + + fieldConfig.RequiredIf[partVal[0]] = partVal[1] + } + } + + // check if the field can use one_of + if !canUseOneOff && len(fieldConfig.OneOf) > 0 { + return fmt.Errorf("field `%s` cannot use one_of: unsupported type: %s", field.Name, fieldType) + } + + fields[field.Name] = fieldConfig + } + + if err := validateFields(config, fields); err != nil { + return err + } + + return nil +} + +func validateFields(config interface{}, fields map[string]FieldConfig) error { + // now we can start to validate the fields + for _, fieldConfig := range fields { + if err := fieldConfig.validate(fields); err != nil { + return err + } + + fieldConfig.populate(config) + } + + return nil +} + +func (f *FieldConfig) validate(fields map[string]FieldConfig) error { + var required bool + var err error + + if required, err = f.validateRequired(fields); err != nil { + return err + } + + // check if the field needs to be updated and set defaults if needed + if err := f.checkIfFieldNeedsUpdate(); err != nil { + return err + } + + // then we can check if the field is one_of + if err := f.validateOneOf(); err != nil { + return err + } + + // and validate the type + if err := f.validateField(); err != nil { + return err + } + + // if the field is delegated, we need to validate the nested field + // but before that, let's check if the field is required + if required && f.Delegated { + if err := setDefaultsAndValidate(f.CurrentValue.(reflect.Value).Interface(), false); err != nil { + return err + } + } + + return nil +} + +func (f *FieldConfig) populate(config interface{}) { + // update the field if it's not empty + if !f.shouldUpdateValue { + return + } + + reflect.ValueOf(config).Elem().FieldByName(f.Name).Set(reflect.ValueOf(f.CurrentValue)) +} + +func (f *FieldConfig) checkIfFieldNeedsUpdate() error { + // populate the field if it's empty and has a default value + if f.IsEmpty && f.Defaults != nil { + switch f.CurrentValue.(type) { + case null.String: + f.CurrentValue = null.StringFrom(f.Defaults.(string)) + case null.Int: + f.CurrentValue = null.IntFrom(int64(f.Defaults.(int))) + case null.Bool: + f.CurrentValue = null.BoolFrom(f.Defaults.(bool)) + case string: + f.CurrentValue = f.Defaults.(string) + case int: + f.CurrentValue = f.Defaults.(int) + case bool: + f.CurrentValue = f.Defaults.(bool) + case []string: + f.CurrentValue = f.Defaults.([]string) + default: + return fmt.Errorf("field `%s` cannot use default value: unsupported type: %s", f.Name, f.TypeString) + } + + f.shouldUpdateValue = true + } + + return nil +} + +func (f *FieldConfig) validateRequired(fields map[string]FieldConfig) (bool, error) { + var required = f.Required + + // if the field is not required, we need to check if it's required_if + if !required && len(f.RequiredIf) > 0 { + for key, value := range f.RequiredIf { + // check if the field's result matches the required_if + // right now we only support string and int + requiredField, ok := fields[key] + if !ok { + return required, fmt.Errorf("required_if field `%s` not found", key) + } + + switch requiredField.CurrentValue.(type) { + case string: + if requiredField.CurrentValue.(string) == value.(string) { + required = true + } + case int: + if requiredField.CurrentValue.(int) == value.(int) { + required = true + } + case null.String: + if !requiredField.CurrentValue.(null.String).IsZero() && + requiredField.CurrentValue.(null.String).String == value.(string) { + required = true + } + case null.Int: + if !requiredField.CurrentValue.(null.Int).IsZero() && + requiredField.CurrentValue.(null.Int).Int64 == value.(int64) { + required = true + } + } + + // if the field is required, we can break the loop + // because we only need one of the required_if fields to be true + if required { + break + } + } + } + + if required && f.IsEmpty { + return false, fmt.Errorf("field `%s` is required", f.Name) + } + + return required, nil +} + +func checkIfSliceContains(slice []string, one_of []string) bool { + for _, oneOf := range one_of { + if slices.Contains(slice, oneOf) { + return true + } + } + + return false +} + +func (f *FieldConfig) validateOneOf() error { + if len(f.OneOf) == 0 { + return nil + } + + var val []string + switch f.CurrentValue.(type) { + case string: + val = []string{f.CurrentValue.(string)} + case null.String: + val = []string{f.CurrentValue.(null.String).String} + case []string: + // let's validate the value here + val = f.CurrentValue.([]string) + default: + return fmt.Errorf("field `%s` cannot use one_of: unsupported type: %s", f.Name, f.TypeString) + } + + if !checkIfSliceContains(val, f.OneOf) { + return fmt.Errorf( + "field `%s` is not one of the allowed values: %s, current value: %s", + f.Name, + strings.Join(f.OneOf, ", "), + strings.Join(val, ", "), + ) + } + + return nil +} + +func (f *FieldConfig) validateField() error { + if len(f.ValidateTypes) == 0 || f.IsEmpty { + return nil + } + + val, err := toString(f.CurrentValue) + if err != nil { + return fmt.Errorf("field `%s` cannot use validate_type: %s", f.Name, err) + } + + if val == "" { + return nil + } + + for _, validateType := range f.ValidateTypes { + switch validateType { + case "ipv4": + if net.ParseIP(val).To4() == nil { + return fmt.Errorf("field `%s` is not a valid IPv4 address: %s", f.Name, val) + } + case "ipv6": + if net.ParseIP(val).To16() == nil { + return fmt.Errorf("field `%s` is not a valid IPv6 address: %s", f.Name, val) + } + case "hwaddr": + if _, err := net.ParseMAC(val); err != nil { + return fmt.Errorf("field `%s` is not a valid MAC address: %s", f.Name, val) + } + case "hostname": + if _, err := idna.Lookup.ToASCII(val); err != nil { + return fmt.Errorf("field `%s` is not a valid hostname: %s", f.Name, val) + } + case "proxy": + if url, err := url.Parse(val); err != nil || (url.Scheme != "http" && url.Scheme != "https") || url.Host == "" { + return fmt.Errorf("field `%s` is not a valid HTTP proxy URL: %s", f.Name, val) + } + default: + return fmt.Errorf("field `%s` cannot use validate_type: unsupported validator: %s", f.Name, validateType) + } + } + + return nil +} diff --git a/internal/confparser/confparser_test.go b/internal/confparser/confparser_test.go new file mode 100644 index 0000000..e14a1ea --- /dev/null +++ b/internal/confparser/confparser_test.go @@ -0,0 +1,117 @@ +package confparser + +import ( + "net" + "testing" + "time" + + "github.com/guregu/null/v6" +) + +type testIPv6Address struct { //nolint:unused + Address net.IP `json:"address"` + Prefix net.IPNet `json:"prefix"` + ValidLifetime *time.Time `json:"valid_lifetime"` + PreferredLifetime *time.Time `json:"preferred_lifetime"` + Scope int `json:"scope"` +} + +type testIPv4StaticConfig struct { + Address null.String `json:"address" validate_type:"ipv4" required:"true"` + Netmask null.String `json:"netmask" validate_type:"ipv4" required:"true"` + Gateway null.String `json:"gateway" validate_type:"ipv4" required:"true"` + DNS []string `json:"dns" validate_type:"ipv4" required:"true"` +} + +type testIPv6StaticConfig struct { + Address null.String `json:"address" validate_type:"ipv6" required:"true"` + Prefix null.String `json:"prefix" validate_type:"ipv6" required:"true"` + Gateway null.String `json:"gateway" validate_type:"ipv6" required:"true"` + DNS []string `json:"dns" validate_type:"ipv6" required:"true"` +} +type testNetworkConfig struct { + Hostname null.String `json:"hostname,omitempty"` + Domain null.String `json:"domain,omitempty"` + + IPv4Mode null.String `json:"ipv4_mode" one_of:"dhcp,static,disabled" default:"dhcp"` + IPv4Static *testIPv4StaticConfig `json:"ipv4_static,omitempty" required_if:"IPv4Mode=static"` + + IPv6Mode null.String `json:"ipv6_mode" one_of:"slaac,dhcpv6,slaac_and_dhcpv6,static,link_local,disabled" default:"slaac"` + IPv6Static *testIPv6StaticConfig `json:"ipv6_static,omitempty" required_if:"IPv6Mode=static"` + + LLDPMode null.String `json:"lldp_mode,omitempty" one_of:"disabled,basic,all" default:"basic"` + LLDPTxTLVs []string `json:"lldp_tx_tlvs,omitempty" one_of:"chassis,port,system,vlan" default:"chassis,port,system,vlan"` + MDNSMode null.String `json:"mdns_mode,omitempty" one_of:"disabled,auto,ipv4_only,ipv6_only" default:"auto"` + TimeSyncMode null.String `json:"time_sync_mode,omitempty" one_of:"ntp_only,ntp_and_http,http_only,custom" default:"ntp_and_http"` + TimeSyncOrdering []string `json:"time_sync_ordering,omitempty" one_of:"http,ntp,ntp_dhcp,ntp_user_provided,http_user_provided" default:"ntp,http"` + TimeSyncDisableFallback null.Bool `json:"time_sync_disable_fallback,omitempty" default:"false"` + TimeSyncParallel null.Int `json:"time_sync_parallel,omitempty" default:"4"` + TimeSyncNTPServers []string `json:"time_sync_ntp_servers,omitempty" validate_type:"ipv4_or_ipv6" required_if:"TimeSyncOrdering=ntp_user_provided"` + TimeSyncHTTPUrls []string `json:"time_sync_http_urls,omitempty" validate_type:"url" required_if:"TimeSyncOrdering=http_user_provided"` +} + +func TestValidateConfig(t *testing.T) { + config := &testNetworkConfig{} + + err := SetDefaultsAndValidate(config) + if err != nil { + t.Fatalf("expected no error, got %v", err) + } +} + +func TestValidateIPv4StaticConfigNetmaskRequiredIfStatic(t *testing.T) { + config := &testNetworkConfig{ + IPv4Static: &testIPv4StaticConfig{ + Address: null.StringFrom("192.168.1.1"), + Gateway: null.StringFrom("192.168.1.1"), + }, + IPv4Mode: null.StringFrom("static"), + } + + err := SetDefaultsAndValidate(config) + if err == nil { + t.Fatalf("expected error, got nil") + } +} + +func TestValidateIPv4StaticConfigNetmaskNotRequiredIfStatic(t *testing.T) { + config := &testNetworkConfig{ + IPv4Static: &testIPv4StaticConfig{ + Address: null.StringFrom("192.168.1.1"), + Gateway: null.StringFrom("192.168.1.1"), + }, + } + + err := SetDefaultsAndValidate(config) + if err != nil { + t.Fatalf("expected no error, got %v", err) + } +} + +func TestValidateIPv4StaticConfigRequiredIf(t *testing.T) { + config := &testNetworkConfig{ + IPv4Mode: null.StringFrom("static"), + } + + err := SetDefaultsAndValidate(config) + if err == nil { + t.Fatalf("expected error, got nil") + } +} + +func TestValidateIPv4StaticConfigValidateType(t *testing.T) { + config := &testNetworkConfig{ + IPv4Static: &testIPv4StaticConfig{ + Address: null.StringFrom("X"), + Netmask: null.StringFrom("255.255.255.0"), + Gateway: null.StringFrom("192.168.1.1"), + DNS: []string{"8.8.8.8", "8.8.4.4"}, + }, + IPv4Mode: null.StringFrom("static"), + } + + err := SetDefaultsAndValidate(config) + if err == nil { + t.Fatalf("expected error, got nil") + } +} diff --git a/internal/confparser/utils.go b/internal/confparser/utils.go new file mode 100644 index 0000000..a46871e --- /dev/null +++ b/internal/confparser/utils.go @@ -0,0 +1,28 @@ +package confparser + +import ( + "fmt" + "reflect" + "strings" + + "github.com/guregu/null/v6" +) + +func splitString(s string) []string { + if s == "" { + return []string{} + } + + return strings.Split(s, ",") +} + +func toString(v interface{}) (string, error) { + switch v := v.(type) { + case string: + return v, nil + case null.String: + return v.String, nil + } + + return "", fmt.Errorf("unsupported type: %s", reflect.TypeOf(v)) +} diff --git a/internal/logging/logger.go b/internal/logging/logger.go new file mode 100644 index 0000000..39156ec --- /dev/null +++ b/internal/logging/logger.go @@ -0,0 +1,197 @@ +package logging + +import ( + "fmt" + "io" + "os" + "strings" + "sync" + "time" + + "github.com/rs/zerolog" +) + +type Logger struct { + l *zerolog.Logger + scopeLoggers map[string]*zerolog.Logger + scopeLevels map[string]zerolog.Level + scopeLevelMutex sync.Mutex + + defaultLogLevelFromEnv zerolog.Level + defaultLogLevelFromConfig zerolog.Level + defaultLogLevel zerolog.Level +} + +const ( + defaultLogLevel = zerolog.ErrorLevel +) + +type logOutput struct { + mu *sync.Mutex +} + +func (w *logOutput) Write(p []byte) (n int, err error) { + w.mu.Lock() + defer w.mu.Unlock() + + // TODO: write to file or syslog + if sseServer != nil { + // use a goroutine to avoid blocking the Write method + go func() { + sseServer.Message <- string(p) + }() + } + return len(p), nil +} + +var ( + consoleLogOutput io.Writer = zerolog.ConsoleWriter{ + Out: os.Stdout, + TimeFormat: time.RFC3339, + PartsOrder: []string{"time", "level", "scope", "component", "message"}, + FieldsExclude: []string{"scope", "component"}, + FormatPartValueByName: func(value interface{}, name string) string { + val := fmt.Sprintf("%s", value) + if name == "component" { + if value == nil { + return "-" + } + } + return val + }, + } + fileLogOutput io.Writer = &logOutput{mu: &sync.Mutex{}} + defaultLogOutput = zerolog.MultiLevelWriter(consoleLogOutput, fileLogOutput) + + zerologLevels = map[string]zerolog.Level{ + "DISABLE": zerolog.Disabled, + "NOLEVEL": zerolog.NoLevel, + "PANIC": zerolog.PanicLevel, + "FATAL": zerolog.FatalLevel, + "ERROR": zerolog.ErrorLevel, + "WARN": zerolog.WarnLevel, + "INFO": zerolog.InfoLevel, + "DEBUG": zerolog.DebugLevel, + "TRACE": zerolog.TraceLevel, + } +) + +func NewLogger(zerologLogger zerolog.Logger) *Logger { + return &Logger{ + l: &zerologLogger, + scopeLoggers: make(map[string]*zerolog.Logger), + scopeLevels: make(map[string]zerolog.Level), + scopeLevelMutex: sync.Mutex{}, + defaultLogLevelFromEnv: -2, + defaultLogLevelFromConfig: -2, + defaultLogLevel: defaultLogLevel, + } +} + +func (l *Logger) updateLogLevel() { + l.scopeLevelMutex.Lock() + defer l.scopeLevelMutex.Unlock() + + l.scopeLevels = make(map[string]zerolog.Level) + + finalDefaultLogLevel := l.defaultLogLevel + + for name, level := range zerologLevels { + env := os.Getenv(fmt.Sprintf("JETKVM_LOG_%s", name)) + + if env == "" { + env = os.Getenv(fmt.Sprintf("PION_LOG_%s", name)) + } + + if env == "" { + env = os.Getenv(fmt.Sprintf("PIONS_LOG_%s", name)) + } + + if env == "" { + continue + } + + if strings.ToLower(env) == "all" { + l.defaultLogLevelFromEnv = level + + if finalDefaultLogLevel > level { + finalDefaultLogLevel = level + } + + continue + } + + scopes := strings.Split(strings.ToLower(env), ",") + for _, scope := range scopes { + l.scopeLevels[scope] = level + } + } + + l.defaultLogLevel = finalDefaultLogLevel +} + +func (l *Logger) getScopeLoggerLevel(scope string) zerolog.Level { + if l.scopeLevels == nil { + l.updateLogLevel() + } + + var scopeLevel zerolog.Level + if l.defaultLogLevelFromConfig != -2 { + scopeLevel = l.defaultLogLevelFromConfig + } + if l.defaultLogLevelFromEnv != -2 { + scopeLevel = l.defaultLogLevelFromEnv + } + + // if the scope is not in the map, use the default level from the root logger + if level, ok := l.scopeLevels[scope]; ok { + scopeLevel = level + } + + return scopeLevel +} + +func (l *Logger) newScopeLogger(scope string) zerolog.Logger { + scopeLevel := l.getScopeLoggerLevel(scope) + logger := l.l.Level(scopeLevel).With().Str("component", scope).Logger() + + return logger +} + +func (l *Logger) getLogger(scope string) *zerolog.Logger { + logger, ok := l.scopeLoggers[scope] + if !ok || logger == nil { + scopeLogger := l.newScopeLogger(scope) + l.scopeLoggers[scope] = &scopeLogger + } + + return l.scopeLoggers[scope] +} + +func (l *Logger) UpdateLogLevel(configDefaultLogLevel string) { + needUpdate := false + + if configDefaultLogLevel != "" { + if logLevel, ok := zerologLevels[configDefaultLogLevel]; ok { + l.defaultLogLevelFromConfig = logLevel + } else { + l.l.Warn().Str("logLevel", configDefaultLogLevel).Msg("invalid defaultLogLevel from config, using ERROR") + } + + if l.defaultLogLevelFromConfig != l.defaultLogLevel { + needUpdate = true + } + } + + l.updateLogLevel() + + if needUpdate { + for scope, logger := range l.scopeLoggers { + currentLevel := logger.GetLevel() + targetLevel := l.getScopeLoggerLevel(scope) + if currentLevel != targetLevel { + *logger = l.newScopeLogger(scope) + } + } + } +} diff --git a/internal/logging/pion.go b/internal/logging/pion.go new file mode 100644 index 0000000..453b8bc --- /dev/null +++ b/internal/logging/pion.go @@ -0,0 +1,63 @@ +package logging + +import ( + "github.com/pion/logging" + "github.com/rs/zerolog" +) + +type pionLogger struct { + logger *zerolog.Logger +} + +// Print all messages except trace. +func (c pionLogger) Trace(msg string) { + c.logger.Trace().Msg(msg) +} +func (c pionLogger) Tracef(format string, args ...interface{}) { + c.logger.Trace().Msgf(format, args...) +} + +func (c pionLogger) Debug(msg string) { + c.logger.Debug().Msg(msg) +} +func (c pionLogger) Debugf(format string, args ...interface{}) { + c.logger.Debug().Msgf(format, args...) +} +func (c pionLogger) Info(msg string) { + c.logger.Info().Msg(msg) +} +func (c pionLogger) Infof(format string, args ...interface{}) { + c.logger.Info().Msgf(format, args...) +} +func (c pionLogger) Warn(msg string) { + c.logger.Warn().Msg(msg) +} +func (c pionLogger) Warnf(format string, args ...interface{}) { + c.logger.Warn().Msgf(format, args...) +} +func (c pionLogger) Error(msg string) { + c.logger.Error().Msg(msg) +} +func (c pionLogger) Errorf(format string, args ...interface{}) { + c.logger.Error().Msgf(format, args...) +} + +// customLoggerFactory satisfies the interface logging.LoggerFactory +// This allows us to create different loggers per subsystem. So we can +// add custom behavior. +type pionLoggerFactory struct{} + +func (c pionLoggerFactory) NewLogger(subsystem string) logging.LeveledLogger { + logger := rootLogger.getLogger(subsystem).With(). + Str("scope", "pion"). + Str("component", subsystem). + Logger() + + return pionLogger{logger: &logger} +} + +var defaultLoggerFactory = &pionLoggerFactory{} + +func GetPionDefaultLoggerFactory() logging.LoggerFactory { + return defaultLoggerFactory +} diff --git a/internal/logging/root.go b/internal/logging/root.go new file mode 100644 index 0000000..397ca64 --- /dev/null +++ b/internal/logging/root.go @@ -0,0 +1,20 @@ +package logging + +import "github.com/rs/zerolog" + +var ( + rootZerologLogger = zerolog.New(defaultLogOutput).With(). + Str("scope", "jetkvm"). + Timestamp(). + Stack(). + Logger() + rootLogger = NewLogger(rootZerologLogger) +) + +func GetRootLogger() *Logger { + return rootLogger +} + +func GetSubsystemLogger(subsystem string) *zerolog.Logger { + return rootLogger.getLogger(subsystem) +} diff --git a/internal/logging/sse.go b/internal/logging/sse.go new file mode 100644 index 0000000..05e6e9e --- /dev/null +++ b/internal/logging/sse.go @@ -0,0 +1,137 @@ +package logging + +import ( + "embed" + "io" + "net/http" + + "github.com/gin-gonic/gin" + "github.com/rs/zerolog" +) + +//go:embed sse.html +var sseHTML embed.FS + +type sseEvent struct { + Message chan string + NewClients chan chan string + ClosedClients chan chan string + TotalClients map[chan string]bool +} + +// New event messages are broadcast to all registered client connection channels +type sseClientChan chan string + +var ( + sseServer *sseEvent + sseLogger *zerolog.Logger +) + +func init() { + sseServer = newSseServer() + sseLogger = GetSubsystemLogger("sse") +} + +// Initialize event and Start procnteessing requests +func newSseServer() (event *sseEvent) { + event = &sseEvent{ + Message: make(chan string), + NewClients: make(chan chan string), + ClosedClients: make(chan chan string), + TotalClients: make(map[chan string]bool), + } + + go event.listen() + + return +} + +// It Listens all incoming requests from clients. +// Handles addition and removal of clients and broadcast messages to clients. +func (stream *sseEvent) listen() { + for { + select { + // Add new available client + case client := <-stream.NewClients: + stream.TotalClients[client] = true + sseLogger.Info(). + Int("total_clients", len(stream.TotalClients)). + Msg("new client connected") + + // Remove closed client + case client := <-stream.ClosedClients: + delete(stream.TotalClients, client) + close(client) + sseLogger.Info().Int("total_clients", len(stream.TotalClients)).Msg("client disconnected") + + // Broadcast message to client + case eventMsg := <-stream.Message: + for clientMessageChan := range stream.TotalClients { + select { + case clientMessageChan <- eventMsg: + // Message sent successfully + default: + // Failed to send, dropping message + } + } + } + } +} + +func (stream *sseEvent) serveHTTP() gin.HandlerFunc { + return func(c *gin.Context) { + clientChan := make(sseClientChan) + stream.NewClients <- clientChan + + go func() { + <-c.Writer.CloseNotify() + + for range clientChan { + } + + stream.ClosedClients <- clientChan + }() + + c.Set("clientChan", clientChan) + c.Next() + } +} + +func sseHeadersMiddleware() gin.HandlerFunc { + return func(c *gin.Context) { + if c.Request.Method == "GET" && c.NegotiateFormat(gin.MIMEHTML) == gin.MIMEHTML { + c.FileFromFS("/sse.html", http.FS(sseHTML)) + c.Status(http.StatusOK) + c.Abort() + return + } + + c.Writer.Header().Set("Content-Type", "text/event-stream") + c.Writer.Header().Set("Cache-Control", "no-cache") + c.Writer.Header().Set("Connection", "keep-alive") + c.Writer.Header().Set("Transfer-Encoding", "chunked") + c.Writer.Header().Set("Access-Control-Allow-Origin", "*") + c.Next() + } +} + +func AttachSSEHandler(router *gin.RouterGroup) { + router.StaticFS("/log-stream", http.FS(sseHTML)) + router.GET("/log-stream", sseHeadersMiddleware(), sseServer.serveHTTP(), func(c *gin.Context) { + v, ok := c.Get("clientChan") + if !ok { + return + } + clientChan, ok := v.(sseClientChan) + if !ok { + return + } + c.Stream(func(w io.Writer) bool { + if msg, ok := <-clientChan; ok { + c.SSEvent("message", msg) + return true + } + return false + }) + }) +} diff --git a/internal/logging/sse.html b/internal/logging/sse.html new file mode 100644 index 0000000..192b464 --- /dev/null +++ b/internal/logging/sse.html @@ -0,0 +1,319 @@ + + + + + + Server Sent Event + + + + +
+ +
+ +
+
+ + + + + \ No newline at end of file diff --git a/internal/logging/utils.go b/internal/logging/utils.go new file mode 100644 index 0000000..e622d96 --- /dev/null +++ b/internal/logging/utils.go @@ -0,0 +1,32 @@ +package logging + +import ( + "fmt" + "os" + + "github.com/rs/zerolog" +) + +var defaultLogger = zerolog.New(os.Stdout).Level(zerolog.InfoLevel) + +func GetDefaultLogger() *zerolog.Logger { + return &defaultLogger +} + +func ErrorfL(l *zerolog.Logger, format string, err error, args ...interface{}) error { + // TODO: move rootLogger to logging package + if l == nil { + l = &defaultLogger + } + + l.Error().Err(err).Msgf(format, args...) + + if err == nil { + return fmt.Errorf(format, args...) + } + + err_msg := err.Error() + ": %v" + err_args := append(args, err) + + return fmt.Errorf(err_msg, err_args...) +} diff --git a/internal/mdns/mdns.go b/internal/mdns/mdns.go new file mode 100644 index 0000000..b882b93 --- /dev/null +++ b/internal/mdns/mdns.go @@ -0,0 +1,190 @@ +package mdns + +import ( + "fmt" + "net" + "reflect" + "strings" + "sync" + + "github.com/jetkvm/kvm/internal/logging" + pion_mdns "github.com/pion/mdns/v2" + "github.com/rs/zerolog" + "golang.org/x/net/ipv4" + "golang.org/x/net/ipv6" +) + +type MDNS struct { + conn *pion_mdns.Conn + lock sync.Mutex + l *zerolog.Logger + + localNames []string + listenOptions *MDNSListenOptions +} + +type MDNSListenOptions struct { + IPv4 bool + IPv6 bool +} + +type MDNSOptions struct { + Logger *zerolog.Logger + LocalNames []string + ListenOptions *MDNSListenOptions +} + +const ( + DefaultAddressIPv4 = pion_mdns.DefaultAddressIPv4 + DefaultAddressIPv6 = pion_mdns.DefaultAddressIPv6 +) + +func NewMDNS(opts *MDNSOptions) (*MDNS, error) { + if opts.Logger == nil { + opts.Logger = logging.GetDefaultLogger() + } + + if opts.ListenOptions == nil { + opts.ListenOptions = &MDNSListenOptions{ + IPv4: true, + IPv6: true, + } + } + + return &MDNS{ + l: opts.Logger, + lock: sync.Mutex{}, + localNames: opts.LocalNames, + listenOptions: opts.ListenOptions, + }, nil +} + +func (m *MDNS) start(allowRestart bool) error { + m.lock.Lock() + defer m.lock.Unlock() + + if m.conn != nil { + if !allowRestart { + return fmt.Errorf("mDNS server already running") + } + + m.conn.Close() + } + + if m.listenOptions == nil { + return fmt.Errorf("listen options not set") + } + + if !m.listenOptions.IPv4 && !m.listenOptions.IPv6 { + m.l.Info().Msg("mDNS server disabled") + return nil + } + + var ( + addr4, addr6 *net.UDPAddr + l4, l6 *net.UDPConn + p4 *ipv4.PacketConn + p6 *ipv6.PacketConn + err error + ) + + if m.listenOptions.IPv4 { + addr4, err = net.ResolveUDPAddr("udp4", DefaultAddressIPv4) + if err != nil { + return err + } + + l4, err = net.ListenUDP("udp4", addr4) + if err != nil { + return err + } + + p4 = ipv4.NewPacketConn(l4) + } + + if m.listenOptions.IPv6 { + addr6, err = net.ResolveUDPAddr("udp6", DefaultAddressIPv6) + if err != nil { + return err + } + + l6, err = net.ListenUDP("udp6", addr6) + if err != nil { + return err + } + + p6 = ipv6.NewPacketConn(l6) + } + + scopeLogger := m.l.With(). + Interface("local_names", m.localNames). + Bool("ipv4", m.listenOptions.IPv4). + Bool("ipv6", m.listenOptions.IPv6). + Logger() + + newLocalNames := make([]string, len(m.localNames)) + for i, name := range m.localNames { + newLocalNames[i] = strings.TrimRight(strings.ToLower(name), ".") + if !strings.HasSuffix(newLocalNames[i], ".local") { + newLocalNames[i] = newLocalNames[i] + ".local" + } + } + + mDNSConn, err := pion_mdns.Server(p4, p6, &pion_mdns.Config{ + LocalNames: newLocalNames, + LoggerFactory: logging.GetPionDefaultLoggerFactory(), + }) + + if err != nil { + scopeLogger.Warn().Err(err).Msg("failed to start mDNS server") + return err + } + + m.conn = mDNSConn + scopeLogger.Info().Msg("mDNS server started") + + return nil +} + +func (m *MDNS) Start() error { + return m.start(false) +} + +func (m *MDNS) Restart() error { + return m.start(true) +} + +func (m *MDNS) Stop() error { + m.lock.Lock() + defer m.lock.Unlock() + + if m.conn == nil { + return nil + } + + return m.conn.Close() +} + +func (m *MDNS) SetLocalNames(localNames []string, always bool) error { + if reflect.DeepEqual(m.localNames, localNames) && !always { + return nil + } + + m.localNames = localNames + _ = m.Restart() + + return nil +} + +func (m *MDNS) SetListenOptions(listenOptions *MDNSListenOptions) error { + if m.listenOptions != nil && + m.listenOptions.IPv4 == listenOptions.IPv4 && + m.listenOptions.IPv6 == listenOptions.IPv6 { + return nil + } + + m.listenOptions = listenOptions + _ = m.Restart() + + return nil +} diff --git a/internal/mdns/utils.go b/internal/mdns/utils.go new file mode 100644 index 0000000..7565eee --- /dev/null +++ b/internal/mdns/utils.go @@ -0,0 +1 @@ +package mdns diff --git a/internal/network/config.go b/internal/network/config.go new file mode 100644 index 0000000..8a28d51 --- /dev/null +++ b/internal/network/config.go @@ -0,0 +1,127 @@ +package network + +import ( + "fmt" + "net" + "net/http" + "net/url" + "time" + + "github.com/guregu/null/v6" + "github.com/jetkvm/kvm/internal/mdns" + "golang.org/x/net/idna" +) + +type IPv6Address struct { + Address net.IP `json:"address"` + Prefix net.IPNet `json:"prefix"` + ValidLifetime *time.Time `json:"valid_lifetime"` + PreferredLifetime *time.Time `json:"preferred_lifetime"` + Scope int `json:"scope"` +} + +type IPv4StaticConfig struct { + Address null.String `json:"address,omitempty" validate_type:"ipv4" required:"true"` + Netmask null.String `json:"netmask,omitempty" validate_type:"ipv4" required:"true"` + Gateway null.String `json:"gateway,omitempty" validate_type:"ipv4" required:"true"` + DNS []string `json:"dns,omitempty" validate_type:"ipv4" required:"true"` +} + +type IPv6StaticConfig struct { + Address null.String `json:"address,omitempty" validate_type:"ipv6" required:"true"` + Prefix null.String `json:"prefix,omitempty" validate_type:"ipv6" required:"true"` + Gateway null.String `json:"gateway,omitempty" validate_type:"ipv6" required:"true"` + DNS []string `json:"dns,omitempty" validate_type:"ipv6" required:"true"` +} +type NetworkConfig struct { + Hostname null.String `json:"hostname,omitempty" validate_type:"hostname"` + HTTPProxy null.String `json:"http_proxy,omitempty" validate_type:"proxy"` + Domain null.String `json:"domain,omitempty" validate_type:"hostname"` + + IPv4Mode null.String `json:"ipv4_mode,omitempty" one_of:"dhcp,static,disabled" default:"dhcp"` + IPv4Static *IPv4StaticConfig `json:"ipv4_static,omitempty" required_if:"IPv4Mode=static"` + + IPv6Mode null.String `json:"ipv6_mode,omitempty" one_of:"slaac,dhcpv6,slaac_and_dhcpv6,static,link_local,disabled" default:"slaac"` + IPv6Static *IPv6StaticConfig `json:"ipv6_static,omitempty" required_if:"IPv6Mode=static"` + + LLDPMode null.String `json:"lldp_mode,omitempty" one_of:"disabled,basic,all" default:"basic"` + LLDPTxTLVs []string `json:"lldp_tx_tlvs,omitempty" one_of:"chassis,port,system,vlan" default:"chassis,port,system,vlan"` + MDNSMode null.String `json:"mdns_mode,omitempty" one_of:"disabled,auto,ipv4_only,ipv6_only" default:"auto"` + TimeSyncMode null.String `json:"time_sync_mode,omitempty" one_of:"ntp_only,ntp_and_http,http_only,custom" default:"ntp_and_http"` + TimeSyncOrdering []string `json:"time_sync_ordering,omitempty" one_of:"http,ntp,ntp_dhcp,ntp_user_provided,http_user_provided" default:"ntp,http"` + TimeSyncDisableFallback null.Bool `json:"time_sync_disable_fallback,omitempty" default:"false"` + TimeSyncParallel null.Int `json:"time_sync_parallel,omitempty" default:"4"` + TimeSyncNTPServers []string `json:"time_sync_ntp_servers,omitempty" validate_type:"ipv4_or_ipv6" required_if:"TimeSyncOrdering=ntp_user_provided"` + TimeSyncHTTPUrls []string `json:"time_sync_http_urls,omitempty" validate_type:"url" required_if:"TimeSyncOrdering=http_user_provided"` +} + +func (c *NetworkConfig) GetMDNSMode() *mdns.MDNSListenOptions { + mode := c.MDNSMode.String + listenOptions := &mdns.MDNSListenOptions{ + IPv4: true, + IPv6: true, + } + + switch mode { + case "ipv4_only": + listenOptions.IPv6 = false + case "ipv6_only": + listenOptions.IPv4 = false + case "disabled": + listenOptions.IPv4 = false + listenOptions.IPv6 = false + } + + return listenOptions +} + +func (s *NetworkConfig) GetTransportProxyFunc() func(*http.Request) (*url.URL, error) { + return func(*http.Request) (*url.URL, error) { + if s.HTTPProxy.String == "" { + return nil, nil + } else { + proxyUrl, _ := url.Parse(s.HTTPProxy.String) + return proxyUrl, nil + } + } +} + +func (s *NetworkInterfaceState) GetHostname() string { + hostname := ToValidHostname(s.config.Hostname.String) + + if hostname == "" { + return s.defaultHostname + } + + return hostname +} + +func ToValidDomain(domain string) string { + ascii, err := idna.Lookup.ToASCII(domain) + if err != nil { + return "" + } + + return ascii +} + +func (s *NetworkInterfaceState) GetDomain() string { + domain := ToValidDomain(s.config.Domain.String) + + if domain == "" { + lease := s.dhcpClient.GetLease() + if lease != nil && lease.Domain != "" { + domain = ToValidDomain(lease.Domain) + } + } + + if domain == "" { + return "local" + } + + return domain +} + +func (s *NetworkInterfaceState) GetFQDN() string { + return fmt.Sprintf("%s.%s", s.GetHostname(), s.GetDomain()) +} diff --git a/internal/network/dhcp.go b/internal/network/dhcp.go new file mode 100644 index 0000000..9e173cc --- /dev/null +++ b/internal/network/dhcp.go @@ -0,0 +1,11 @@ +package network + +type DhcpTargetState int + +const ( + DhcpTargetStateDoNothing DhcpTargetState = iota + DhcpTargetStateStart + DhcpTargetStateStop + DhcpTargetStateRenew + DhcpTargetStateRelease +) diff --git a/internal/network/hostname.go b/internal/network/hostname.go new file mode 100644 index 0000000..d75255c --- /dev/null +++ b/internal/network/hostname.go @@ -0,0 +1,137 @@ +package network + +import ( + "fmt" + "io" + "os" + "os/exec" + "strings" + "sync" + + "golang.org/x/net/idna" +) + +const ( + hostnamePath = "/etc/hostname" + hostsPath = "/etc/hosts" +) + +var ( + hostnameLock sync.Mutex = sync.Mutex{} +) + +func updateEtcHosts(hostname string, fqdn string) error { + // update /etc/hosts + hostsFile, err := os.OpenFile(hostsPath, os.O_RDWR|os.O_SYNC, os.ModeExclusive) + if err != nil { + return fmt.Errorf("failed to open %s: %w", hostsPath, err) + } + defer hostsFile.Close() + + // read all lines + if _, err := hostsFile.Seek(0, io.SeekStart); err != nil { + return fmt.Errorf("failed to seek %s: %w", hostsPath, err) + } + + lines, err := io.ReadAll(hostsFile) + if err != nil { + return fmt.Errorf("failed to read %s: %w", hostsPath, err) + } + + newLines := []string{} + hostLine := fmt.Sprintf("127.0.1.1\t%s %s", hostname, fqdn) + hostLineExists := false + + for _, line := range strings.Split(string(lines), "\n") { + if strings.HasPrefix(line, "127.0.1.1") { + hostLineExists = true + line = hostLine + } + newLines = append(newLines, line) + } + + if !hostLineExists { + newLines = append(newLines, hostLine) + } + + if err := hostsFile.Truncate(0); err != nil { + return fmt.Errorf("failed to truncate %s: %w", hostsPath, err) + } + + if _, err := hostsFile.Seek(0, io.SeekStart); err != nil { + return fmt.Errorf("failed to seek %s: %w", hostsPath, err) + } + + if _, err := hostsFile.Write([]byte(strings.Join(newLines, "\n"))); err != nil { + return fmt.Errorf("failed to write %s: %w", hostsPath, err) + } + + return nil +} + +func ToValidHostname(hostname string) string { + ascii, err := idna.Lookup.ToASCII(hostname) + if err != nil { + return "" + } + return ascii +} + +func SetHostname(hostname string, fqdn string) error { + hostnameLock.Lock() + defer hostnameLock.Unlock() + + hostname = ToValidHostname(strings.TrimSpace(hostname)) + fqdn = ToValidHostname(strings.TrimSpace(fqdn)) + + if hostname == "" { + return fmt.Errorf("invalid hostname: %s", hostname) + } + + if fqdn == "" { + fqdn = hostname + } + + // update /etc/hostname + if err := os.WriteFile(hostnamePath, []byte(hostname), 0644); err != nil { + return fmt.Errorf("failed to write %s: %w", hostnamePath, err) + } + + // update /etc/hosts + if err := updateEtcHosts(hostname, fqdn); err != nil { + return fmt.Errorf("failed to update /etc/hosts: %w", err) + } + + // run hostname + if err := exec.Command("hostname", "-F", hostnamePath).Run(); err != nil { + return fmt.Errorf("failed to run hostname: %w", err) + } + + return nil +} + +func (s *NetworkInterfaceState) setHostnameIfNotSame() error { + hostname := s.GetHostname() + currentHostname, _ := os.Hostname() + + fqdn := fmt.Sprintf("%s.%s", hostname, s.GetDomain()) + + if currentHostname == hostname && s.currentFqdn == fqdn && s.currentHostname == hostname { + return nil + } + + scopedLogger := s.l.With().Str("hostname", hostname).Str("fqdn", fqdn).Logger() + + err := SetHostname(hostname, fqdn) + if err != nil { + scopedLogger.Error().Err(err).Msg("failed to set hostname") + return err + } + + s.currentHostname = hostname + s.currentFqdn = fqdn + + scopedLogger.Info().Msg("hostname set") + + return nil +} diff --git a/internal/network/netif.go b/internal/network/netif.go new file mode 100644 index 0000000..5a8dab6 --- /dev/null +++ b/internal/network/netif.go @@ -0,0 +1,388 @@ +package network + +import ( + "fmt" + "net" + "sync" + + "github.com/jetkvm/kvm/internal/confparser" + "github.com/jetkvm/kvm/internal/logging" + "github.com/jetkvm/kvm/internal/udhcpc" + "github.com/rs/zerolog" + + "github.com/vishvananda/netlink" +) + +type NetworkInterfaceState struct { + interfaceName string + interfaceUp bool + ipv4Addr *net.IP + ipv4Addresses []string + ipv6Addr *net.IP + ipv6Addresses []IPv6Address + ipv6LinkLocal *net.IP + ntpAddresses []*net.IP + macAddr *net.HardwareAddr + + l *zerolog.Logger + stateLock sync.Mutex + + config *NetworkConfig + dhcpClient *udhcpc.DHCPClient + + defaultHostname string + currentHostname string + currentFqdn string + + onStateChange func(state *NetworkInterfaceState) + onInitialCheck func(state *NetworkInterfaceState) + cbConfigChange func(config *NetworkConfig) + + checked bool +} + +type NetworkInterfaceOptions struct { + InterfaceName string + DhcpPidFile string + Logger *zerolog.Logger + DefaultHostname string + OnStateChange func(state *NetworkInterfaceState) + OnInitialCheck func(state *NetworkInterfaceState) + OnDhcpLeaseChange func(lease *udhcpc.Lease) + OnConfigChange func(config *NetworkConfig) + NetworkConfig *NetworkConfig +} + +func NewNetworkInterfaceState(opts *NetworkInterfaceOptions) (*NetworkInterfaceState, error) { + if opts.NetworkConfig == nil { + return nil, fmt.Errorf("NetworkConfig can not be nil") + } + + if opts.DefaultHostname == "" { + opts.DefaultHostname = "jetkvm" + } + + err := confparser.SetDefaultsAndValidate(opts.NetworkConfig) + if err != nil { + return nil, err + } + + l := opts.Logger + s := &NetworkInterfaceState{ + interfaceName: opts.InterfaceName, + defaultHostname: opts.DefaultHostname, + stateLock: sync.Mutex{}, + l: l, + onStateChange: opts.OnStateChange, + onInitialCheck: opts.OnInitialCheck, + cbConfigChange: opts.OnConfigChange, + config: opts.NetworkConfig, + ntpAddresses: make([]*net.IP, 0), + } + + // create the dhcp client + dhcpClient := udhcpc.NewDHCPClient(&udhcpc.DHCPClientOptions{ + InterfaceName: opts.InterfaceName, + PidFile: opts.DhcpPidFile, + Logger: l, + OnLeaseChange: func(lease *udhcpc.Lease) { + _, err := s.update() + if err != nil { + opts.Logger.Error().Err(err).Msg("failed to update network state") + return + } + _ = s.updateNtpServersFromLease(lease) + _ = s.setHostnameIfNotSame() + + opts.OnDhcpLeaseChange(lease) + }, + }) + + s.dhcpClient = dhcpClient + + return s, nil +} + +func (s *NetworkInterfaceState) IsUp() bool { + return s.interfaceUp +} + +func (s *NetworkInterfaceState) HasIPAssigned() bool { + return s.ipv4Addr != nil || s.ipv6Addr != nil +} + +func (s *NetworkInterfaceState) IsOnline() bool { + return s.IsUp() && s.HasIPAssigned() +} + +func (s *NetworkInterfaceState) IPv4() *net.IP { + return s.ipv4Addr +} + +func (s *NetworkInterfaceState) IPv4String() string { + if s.ipv4Addr == nil { + return "..." + } + return s.ipv4Addr.String() +} + +func (s *NetworkInterfaceState) IPv6() *net.IP { + return s.ipv6Addr +} + +func (s *NetworkInterfaceState) IPv6String() string { + if s.ipv6Addr == nil { + return "..." + } + return s.ipv6Addr.String() +} + +func (s *NetworkInterfaceState) NtpAddresses() []*net.IP { + return s.ntpAddresses +} + +func (s *NetworkInterfaceState) NtpAddressesString() []string { + ntpServers := []string{} + + if s != nil { + s.l.Debug().Any("s", s).Msg("getting NTP address strings") + + if len(s.ntpAddresses) > 0 { + for _, server := range s.ntpAddresses { + s.l.Debug().IPAddr("server", *server).Msg("converting NTP address") + ntpServers = append(ntpServers, server.String()) + } + } + } + + return ntpServers +} + +func (s *NetworkInterfaceState) MAC() *net.HardwareAddr { + return s.macAddr +} + +func (s *NetworkInterfaceState) MACString() string { + if s.macAddr == nil { + return "" + } + return s.macAddr.String() +} + +func (s *NetworkInterfaceState) update() (DhcpTargetState, error) { + s.stateLock.Lock() + defer s.stateLock.Unlock() + + dhcpTargetState := DhcpTargetStateDoNothing + + iface, err := netlink.LinkByName(s.interfaceName) + if err != nil { + s.l.Error().Err(err).Msg("failed to get interface") + return dhcpTargetState, err + } + + // detect if the interface status changed + var changed bool + attrs := iface.Attrs() + state := attrs.OperState + newInterfaceUp := state == netlink.OperUp + + // check if the interface is coming up + interfaceGoingUp := !s.interfaceUp && newInterfaceUp + interfaceGoingDown := s.interfaceUp && !newInterfaceUp + + if s.interfaceUp != newInterfaceUp { + s.interfaceUp = newInterfaceUp + changed = true + } + + if changed { + if interfaceGoingUp { + s.l.Info().Msg("interface state transitioned to up") + dhcpTargetState = DhcpTargetStateRenew + } else if interfaceGoingDown { + s.l.Info().Msg("interface state transitioned to down") + } + } + + // set the mac address + s.macAddr = &attrs.HardwareAddr + + // get the ip addresses + addrs, err := netlinkAddrs(iface) + if err != nil { + return dhcpTargetState, logging.ErrorfL(s.l, "failed to get ip addresses", err) + } + + var ( + ipv4Addresses = make([]net.IP, 0) + ipv4AddressesString = make([]string, 0) + ipv6Addresses = make([]IPv6Address, 0) + // ipv6AddressesString = make([]string, 0) + ipv6LinkLocal *net.IP + ) + + for _, addr := range addrs { + if addr.IP.To4() != nil { + scopedLogger := s.l.With().Str("ipv4", addr.IP.String()).Logger() + if interfaceGoingDown { + // remove all IPv4 addresses from the interface. + scopedLogger.Info().Msg("state transitioned to down, removing IPv4 address") + err := netlink.AddrDel(iface, &addr) + if err != nil { + scopedLogger.Warn().Err(err).Msg("failed to delete address") + } + // notify the DHCP client to release the lease + dhcpTargetState = DhcpTargetStateRelease + continue + } + ipv4Addresses = append(ipv4Addresses, addr.IP) + ipv4AddressesString = append(ipv4AddressesString, addr.IPNet.String()) + } else if addr.IP.To16() != nil { + scopedLogger := s.l.With().Str("ipv6", addr.IP.String()).Logger() + // check if it's a link local address + if addr.IP.IsLinkLocalUnicast() { + ipv6LinkLocal = &addr.IP + continue + } + + if !addr.IP.IsGlobalUnicast() { + scopedLogger.Trace().Msg("not a global unicast address, skipping") + continue + } + + if interfaceGoingDown { + scopedLogger.Info().Msg("state transitioned to down, removing IPv6 address") + err := netlink.AddrDel(iface, &addr) + if err != nil { + scopedLogger.Warn().Err(err).Msg("failed to delete address") + } + continue + } + ipv6Addresses = append(ipv6Addresses, IPv6Address{ + Address: addr.IP, + Prefix: *addr.IPNet, + ValidLifetime: lifetimeToTime(addr.ValidLft), + PreferredLifetime: lifetimeToTime(addr.PreferedLft), + Scope: addr.Scope, + }) + // ipv6AddressesString = append(ipv6AddressesString, addr.IPNet.String()) + } + } + + if len(ipv4Addresses) > 0 { + // compare the addresses to see if there's a change + if s.ipv4Addr == nil || s.ipv4Addr.String() != ipv4Addresses[0].String() { + scopedLogger := s.l.With().Str("ipv4", ipv4Addresses[0].String()).Logger() + if s.ipv4Addr != nil { + scopedLogger.Info(). + Str("old_ipv4", s.ipv4Addr.String()). + Msg("IPv4 address changed") + } else { + scopedLogger.Info().Msg("IPv4 address found") + } + s.ipv4Addr = &ipv4Addresses[0] + changed = true + } + } + s.ipv4Addresses = ipv4AddressesString + + if ipv6LinkLocal != nil { + if s.ipv6LinkLocal == nil || s.ipv6LinkLocal.String() != ipv6LinkLocal.String() { + scopedLogger := s.l.With().Str("ipv6", ipv6LinkLocal.String()).Logger() + if s.ipv6LinkLocal != nil { + scopedLogger.Info(). + Str("old_ipv6", s.ipv6LinkLocal.String()). + Msg("IPv6 link local address changed") + } else { + scopedLogger.Info().Msg("IPv6 link local address found") + } + s.ipv6LinkLocal = ipv6LinkLocal + changed = true + } + } + s.ipv6Addresses = ipv6Addresses + + if len(ipv6Addresses) > 0 { + // compare the addresses to see if there's a change + if s.ipv6Addr == nil || s.ipv6Addr.String() != ipv6Addresses[0].Address.String() { + scopedLogger := s.l.With().Str("ipv6", ipv6Addresses[0].Address.String()).Logger() + if s.ipv6Addr != nil { + scopedLogger.Info(). + Str("old_ipv6", s.ipv6Addr.String()). + Msg("IPv6 address changed") + } else { + scopedLogger.Info().Msg("IPv6 address found") + } + s.ipv6Addr = &ipv6Addresses[0].Address + changed = true + } + } + + // if it's the initial check, we'll set changed to false + initialCheck := !s.checked + if initialCheck { + s.checked = true + changed = false + if dhcpTargetState == DhcpTargetStateRenew { + // it's the initial check, we'll start the DHCP client + // dhcpTargetState = DhcpTargetStateStart + // TODO: manage DHCP client start/stop + dhcpTargetState = DhcpTargetStateDoNothing + } + } + + if initialCheck { + s.onInitialCheck(s) + } else if changed { + s.onStateChange(s) + } + + return dhcpTargetState, nil +} + +func (s *NetworkInterfaceState) updateNtpServersFromLease(lease *udhcpc.Lease) error { + if lease != nil && len(lease.NTPServers) > 0 { + s.l.Info().Msg("lease found, updating DHCP NTP addresses") + s.ntpAddresses = make([]*net.IP, 0, len(lease.NTPServers)) + + for _, ntpServer := range lease.NTPServers { + if ntpServer != nil { + s.l.Info().IPAddr("ntp_server", ntpServer).Msg("NTP server found in lease") + s.ntpAddresses = append(s.ntpAddresses, &ntpServer) + } + } + } else { + s.l.Info().Msg("no NTP servers found in lease") + s.ntpAddresses = make([]*net.IP, 0, len(s.config.TimeSyncNTPServers)) + } + + return nil +} + +func (s *NetworkInterfaceState) CheckAndUpdateDhcp() error { + dhcpTargetState, err := s.update() + if err != nil { + return logging.ErrorfL(s.l, "failed to update network state", err) + } + + switch dhcpTargetState { + case DhcpTargetStateRenew: + s.l.Info().Msg("renewing DHCP lease") + _ = s.dhcpClient.Renew() + case DhcpTargetStateRelease: + s.l.Info().Msg("releasing DHCP lease") + _ = s.dhcpClient.Release() + case DhcpTargetStateStart: + s.l.Warn().Msg("dhcpTargetStateStart not implemented") + case DhcpTargetStateStop: + s.l.Warn().Msg("dhcpTargetStateStop not implemented") + } + + return nil +} + +func (s *NetworkInterfaceState) onConfigChange(config *NetworkConfig) { + _ = s.setHostnameIfNotSame() + s.cbConfigChange(config) +} diff --git a/internal/network/netif_linux.go b/internal/network/netif_linux.go new file mode 100644 index 0000000..ec057f1 --- /dev/null +++ b/internal/network/netif_linux.go @@ -0,0 +1,58 @@ +//go:build linux + +package network + +import ( + "time" + + "github.com/vishvananda/netlink" + "github.com/vishvananda/netlink/nl" +) + +func (s *NetworkInterfaceState) HandleLinkUpdate(update netlink.LinkUpdate) { + if update.Link.Attrs().Name == s.interfaceName { + s.l.Info().Interface("update", update).Msg("interface link update received") + _ = s.CheckAndUpdateDhcp() + } +} + +func (s *NetworkInterfaceState) Run() error { + updates := make(chan netlink.LinkUpdate) + done := make(chan struct{}) + + if err := netlink.LinkSubscribe(updates, done); err != nil { + s.l.Warn().Err(err).Msg("failed to subscribe to link updates") + return err + } + + _ = s.setHostnameIfNotSame() + + // run the dhcp client + go s.dhcpClient.Run() // nolint:errcheck + + if err := s.CheckAndUpdateDhcp(); err != nil { + return err + } + + go func() { + ticker := time.NewTicker(1 * time.Second) + defer ticker.Stop() + + for { + select { + case update := <-updates: + s.HandleLinkUpdate(update) + case <-ticker.C: + _ = s.CheckAndUpdateDhcp() + case <-done: + return + } + } + }() + + return nil +} + +func netlinkAddrs(iface netlink.Link) ([]netlink.Addr, error) { + return netlink.AddrList(iface, nl.FAMILY_ALL) +} diff --git a/internal/network/netif_notlinux.go b/internal/network/netif_notlinux.go new file mode 100644 index 0000000..d101630 --- /dev/null +++ b/internal/network/netif_notlinux.go @@ -0,0 +1,21 @@ +//go:build !linux + +package network + +import ( + "fmt" + + "github.com/vishvananda/netlink" +) + +func (s *NetworkInterfaceState) HandleLinkUpdate() error { + return fmt.Errorf("not implemented") +} + +func (s *NetworkInterfaceState) Run() error { + return fmt.Errorf("not implemented") +} + +func netlinkAddrs(iface netlink.Link) ([]netlink.Addr, error) { + return nil, fmt.Errorf("not implemented") +} diff --git a/internal/network/rpc.go b/internal/network/rpc.go new file mode 100644 index 0000000..32f34f5 --- /dev/null +++ b/internal/network/rpc.go @@ -0,0 +1,126 @@ +package network + +import ( + "fmt" + "time" + + "github.com/jetkvm/kvm/internal/confparser" + "github.com/jetkvm/kvm/internal/udhcpc" +) + +type RpcIPv6Address struct { + Address string `json:"address"` + ValidLifetime *time.Time `json:"valid_lifetime,omitempty"` + PreferredLifetime *time.Time `json:"preferred_lifetime,omitempty"` + Scope int `json:"scope"` +} + +type RpcNetworkState struct { + InterfaceName string `json:"interface_name"` + MacAddress string `json:"mac_address"` + IPv4 string `json:"ipv4,omitempty"` + IPv6 string `json:"ipv6,omitempty"` + IPv6LinkLocal string `json:"ipv6_link_local,omitempty"` + IPv4Addresses []string `json:"ipv4_addresses,omitempty"` + IPv6Addresses []RpcIPv6Address `json:"ipv6_addresses,omitempty"` + DHCPLease *udhcpc.Lease `json:"dhcp_lease,omitempty"` +} + +type RpcNetworkSettings struct { + NetworkConfig +} + +func (s *NetworkInterfaceState) MacAddress() string { + if s.macAddr == nil { + return "" + } + + return s.macAddr.String() +} + +func (s *NetworkInterfaceState) IPv4Address() string { + if s.ipv4Addr == nil { + return "" + } + + return s.ipv4Addr.String() +} + +func (s *NetworkInterfaceState) IPv6Address() string { + if s.ipv6Addr == nil { + return "" + } + + return s.ipv6Addr.String() +} + +func (s *NetworkInterfaceState) IPv6LinkLocalAddress() string { + if s.ipv6LinkLocal == nil { + return "" + } + + return s.ipv6LinkLocal.String() +} + +func (s *NetworkInterfaceState) RpcGetNetworkState() RpcNetworkState { + ipv6Addresses := make([]RpcIPv6Address, 0) + + if s.ipv6Addresses != nil { + for _, addr := range s.ipv6Addresses { + ipv6Addresses = append(ipv6Addresses, RpcIPv6Address{ + Address: addr.Prefix.String(), + ValidLifetime: addr.ValidLifetime, + PreferredLifetime: addr.PreferredLifetime, + Scope: addr.Scope, + }) + } + } + + return RpcNetworkState{ + InterfaceName: s.interfaceName, + MacAddress: s.MacAddress(), + IPv4: s.IPv4Address(), + IPv6: s.IPv6Address(), + IPv6LinkLocal: s.IPv6LinkLocalAddress(), + IPv4Addresses: s.ipv4Addresses, + IPv6Addresses: ipv6Addresses, + DHCPLease: s.dhcpClient.GetLease(), + } +} + +func (s *NetworkInterfaceState) RpcGetNetworkSettings() RpcNetworkSettings { + if s.config == nil { + return RpcNetworkSettings{} + } + + return RpcNetworkSettings{ + NetworkConfig: *s.config, + } +} + +func (s *NetworkInterfaceState) RpcSetNetworkSettings(settings RpcNetworkSettings) error { + currentSettings := s.config + + err := confparser.SetDefaultsAndValidate(&settings.NetworkConfig) + if err != nil { + return err + } + + if IsSame(currentSettings, settings.NetworkConfig) { + // no changes, do nothing + return nil + } + + s.config = &settings.NetworkConfig + s.onConfigChange(s.config) + + return nil +} + +func (s *NetworkInterfaceState) RpcRenewDHCPLease() error { + if s.dhcpClient == nil { + return fmt.Errorf("dhcp client not initialized") + } + + return s.dhcpClient.Renew() +} diff --git a/internal/network/utils.go b/internal/network/utils.go new file mode 100644 index 0000000..6d64332 --- /dev/null +++ b/internal/network/utils.go @@ -0,0 +1,26 @@ +package network + +import ( + "encoding/json" + "time" +) + +func lifetimeToTime(lifetime int) *time.Time { + if lifetime == 0 { + return nil + } + t := time.Now().Add(time.Duration(lifetime) * time.Second) + return &t +} + +func IsSame(a, b interface{}) bool { + aJSON, err := json.Marshal(a) + if err != nil { + return false + } + bJSON, err := json.Marshal(b) + if err != nil { + return false + } + return string(aJSON) == string(bJSON) +} diff --git a/internal/timesync/http.go b/internal/timesync/http.go new file mode 100644 index 0000000..4375e2a --- /dev/null +++ b/internal/timesync/http.go @@ -0,0 +1,151 @@ +package timesync + +import ( + "context" + "errors" + "math/rand" + "net/http" + "net/url" + "strconv" + "time" +) + +var defaultHTTPUrls = []string{ + "http://www.gstatic.com/generate_204", + "http://cp.cloudflare.com/", + "http://edge-http.microsoft.com/captiveportal/generate_204", + // Firefox, Apple, and Microsoft have inconsistent results, so we don't use it + // "http://detectportal.firefox.com/", + // "http://www.apple.com/library/test/success.html", + // "http://www.msftconnecttest.com/connecttest.txt", +} + +func (t *TimeSync) queryAllHttpTime(httpUrls []string) (now *time.Time) { + chunkSize := int(t.networkConfig.TimeSyncParallel.ValueOr(4)) + t.l.Info().Strs("httpUrls", httpUrls).Int("chunkSize", chunkSize).Msg("querying HTTP URLs") + + // shuffle the http urls to avoid always querying the same servers + rand.Shuffle(len(httpUrls), func(i, j int) { httpUrls[i], httpUrls[j] = httpUrls[j], httpUrls[i] }) + + for i := 0; i < len(httpUrls); i += chunkSize { + chunk := httpUrls[i:min(i+chunkSize, len(httpUrls))] + results := t.queryMultipleHttp(chunk, timeSyncTimeout) + if results != nil { + return results + } + } + + return nil +} + +func (t *TimeSync) queryMultipleHttp(urls []string, timeout time.Duration) (now *time.Time) { + results := make(chan *time.Time, len(urls)) + + ctx, cancel := context.WithTimeout(context.Background(), timeout) + defer cancel() + + for _, url := range urls { + go func(url string) { + scopedLogger := t.l.With(). + Str("http_url", url). + Logger() + + metricHttpRequestCount.WithLabelValues(url).Inc() + metricHttpTotalRequestCount.Inc() + + startTime := time.Now() + now, response, err := queryHttpTime( + ctx, + url, + timeout, + t.networkConfig.GetTransportProxyFunc(), + ) + duration := time.Since(startTime) + + metricHttpServerLastRTT.WithLabelValues(url).Set(float64(duration.Milliseconds())) + metricHttpServerRttHistogram.WithLabelValues(url).Observe(float64(duration.Milliseconds())) + + status := 0 + if response != nil { + status = response.StatusCode + } + metricHttpServerInfo.WithLabelValues( + url, + strconv.Itoa(status), + ).Set(1) + + if err == nil { + metricHttpTotalSuccessCount.Inc() + metricHttpSuccessCount.WithLabelValues(url).Inc() + + requestId := response.Header.Get("X-Request-Id") + if requestId != "" { + requestId = response.Header.Get("X-Msedge-Ref") + } + if requestId == "" { + requestId = response.Header.Get("Cf-Ray") + } + scopedLogger.Info(). + Str("time", now.Format(time.RFC3339)). + Int("status", status). + Str("request_id", requestId). + Str("time_taken", duration.String()). + Msg("HTTP server returned time") + + cancel() + results <- now + } else if errors.Is(err, context.Canceled) { + metricHttpCancelCount.WithLabelValues(url).Inc() + metricHttpTotalCancelCount.Inc() + results <- nil + } else { + scopedLogger.Warn(). + Str("error", err.Error()). + Int("status", status). + Msg("failed to query HTTP server") + results <- nil + } + }(url) + } + + for range urls { + result := <-results + if result == nil { + continue + } + now = result + return + } + + return +} + +func queryHttpTime( + ctx context.Context, + url string, + timeout time.Duration, + proxyFunc func(*http.Request) (*url.URL, error), +) (now *time.Time, response *http.Response, err error) { + transport := http.DefaultTransport.(*http.Transport).Clone() + transport.Proxy = proxyFunc + + client := http.Client{ + Transport: transport, + Timeout: timeout, + } + + req, err := http.NewRequestWithContext(ctx, "GET", url, nil) + if err != nil { + return nil, nil, err + } + resp, err := client.Do(req) + if err != nil { + return nil, nil, err + } + dateStr := resp.Header.Get("Date") + parsedTime, err := time.Parse(time.RFC1123, dateStr) + if err != nil { + return nil, nil, err + } + return &parsedTime, resp, nil +} diff --git a/internal/timesync/metrics.go b/internal/timesync/metrics.go new file mode 100644 index 0000000..1c27c8b --- /dev/null +++ b/internal/timesync/metrics.go @@ -0,0 +1,148 @@ +package timesync + +import ( + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promauto" +) + +var ( + metricTimeSyncStatus = promauto.NewGauge( + prometheus.GaugeOpts{ + Name: "jetkvm_timesync_status", + Help: "The status of the timesync, 1 if successful, 0 if not", + }, + ) + metricTimeSyncCount = promauto.NewCounter( + prometheus.CounterOpts{ + Name: "jetkvm_timesync_total", + Help: "The number of times the timesync has been run", + }, + ) + metricTimeSyncSuccessCount = promauto.NewCounter( + prometheus.CounterOpts{ + Name: "jetkvm_timesync_success_total", + Help: "The number of times the timesync has been successful", + }, + ) + metricRTCUpdateCount = promauto.NewCounter( //nolint:unused + prometheus.CounterOpts{ + Name: "jetkvm_timesync_rtc_update_total", + Help: "The number of times the RTC has been updated", + }, + ) + metricNtpTotalSuccessCount = promauto.NewCounter( + prometheus.CounterOpts{ + Name: "jetkvm_timesync_ntp_total_success_total", + Help: "The total number of successful NTP requests", + }, + ) + metricNtpTotalRequestCount = promauto.NewCounter( + prometheus.CounterOpts{ + Name: "jetkvm_timesync_ntp_total_request_total", + Help: "The total number of NTP requests sent", + }, + ) + metricNtpSuccessCount = promauto.NewCounterVec( + prometheus.CounterOpts{ + Name: "jetkvm_timesync_ntp_success_total", + Help: "The number of successful NTP requests", + }, + []string{"url"}, + ) + metricNtpRequestCount = promauto.NewCounterVec( + prometheus.CounterOpts{ + Name: "jetkvm_timesync_ntp_request_total", + Help: "The number of NTP requests sent to the server", + }, + []string{"url"}, + ) + metricNtpServerLastRTT = promauto.NewGaugeVec( + prometheus.GaugeOpts{ + Name: "jetkvm_timesync_ntp_server_last_rtt", + Help: "The last RTT of the NTP server in milliseconds", + }, + []string{"url"}, + ) + metricNtpServerRttHistogram = promauto.NewHistogramVec( + prometheus.HistogramOpts{ + Name: "jetkvm_timesync_ntp_server_rtt", + Help: "The histogram of the RTT of the NTP server in milliseconds", + Buckets: []float64{ + 10, 25, 50, 100, 200, 300, 500, 1000, + }, + }, + []string{"url"}, + ) + + metricNtpServerInfo = promauto.NewGaugeVec( + prometheus.GaugeOpts{ + Name: "jetkvm_timesync_ntp_server_info", + Help: "The info of the NTP server", + }, + []string{"url", "reference", "stratum", "precision"}, + ) + + metricHttpTotalSuccessCount = promauto.NewCounter( + prometheus.CounterOpts{ + Name: "jetkvm_timesync_http_total_success_total", + Help: "The total number of successful HTTP requests", + }, + ) + metricHttpTotalRequestCount = promauto.NewCounter( + prometheus.CounterOpts{ + Name: "jetkvm_timesync_http_total_request_total", + Help: "The total number of HTTP requests sent", + }, + ) + metricHttpTotalCancelCount = promauto.NewCounter( + prometheus.CounterOpts{ + Name: "jetkvm_timesync_http_total_cancel_total", + Help: "The total number of HTTP requests cancelled", + }, + ) + metricHttpSuccessCount = promauto.NewCounterVec( + prometheus.CounterOpts{ + Name: "jetkvm_timesync_http_success_total", + Help: "The number of successful HTTP requests", + }, + []string{"url"}, + ) + metricHttpRequestCount = promauto.NewCounterVec( + prometheus.CounterOpts{ + Name: "jetkvm_timesync_http_request_total", + Help: "The number of HTTP requests sent to the server", + }, + []string{"url"}, + ) + metricHttpCancelCount = promauto.NewCounterVec( + prometheus.CounterOpts{ + Name: "jetkvm_timesync_http_cancel_total", + Help: "The number of HTTP requests cancelled", + }, + []string{"url"}, + ) + metricHttpServerLastRTT = promauto.NewGaugeVec( + prometheus.GaugeOpts{ + Name: "jetkvm_timesync_http_server_last_rtt", + Help: "The last RTT of the HTTP server in milliseconds", + }, + []string{"url"}, + ) + metricHttpServerRttHistogram = promauto.NewHistogramVec( + prometheus.HistogramOpts{ + Name: "jetkvm_timesync_http_server_rtt", + Help: "The histogram of the RTT of the HTTP server in milliseconds", + Buckets: []float64{ + 10, 25, 50, 100, 200, 300, 500, 1000, + }, + }, + []string{"url"}, + ) + metricHttpServerInfo = promauto.NewGaugeVec( + prometheus.GaugeOpts{ + Name: "jetkvm_timesync_http_server_info", + Help: "The info of the HTTP server", + }, + []string{"url", "http_code"}, + ) +) diff --git a/internal/timesync/ntp.go b/internal/timesync/ntp.go new file mode 100644 index 0000000..c32de2a --- /dev/null +++ b/internal/timesync/ntp.go @@ -0,0 +1,140 @@ +package timesync + +import ( + "context" + "math/rand/v2" + "strconv" + "time" + + "github.com/beevik/ntp" +) + +var defaultNTPServers = []string{ + "time.apple.com", + "time.aws.com", + "time.windows.com", + "time.google.com", + "162.159.200.123", // time.cloudflare.com IPv4 + "2606:4700:f1::123", // time.cloudflare.com IPv6 + "0.pool.ntp.org", + "1.pool.ntp.org", + "2.pool.ntp.org", + "3.pool.ntp.org", +} + +func (t *TimeSync) queryNetworkTime(ntpServers []string) (now *time.Time, offset *time.Duration) { + chunkSize := int(t.networkConfig.TimeSyncParallel.ValueOr(4)) + t.l.Info().Strs("servers", ntpServers).Int("chunkSize", chunkSize).Msg("querying NTP servers") + + // shuffle the ntp servers to avoid always querying the same servers + rand.Shuffle(len(ntpServers), func(i, j int) { ntpServers[i], ntpServers[j] = ntpServers[j], ntpServers[i] }) + + for i := 0; i < len(ntpServers); i += chunkSize { + chunk := ntpServers[i:min(i+chunkSize, len(ntpServers))] + now, offset := t.queryMultipleNTP(chunk, timeSyncTimeout) + if now != nil { + return now, offset + } + } + + return nil, nil +} + +type ntpResult struct { + now *time.Time + offset *time.Duration +} + +func (t *TimeSync) queryMultipleNTP(servers []string, timeout time.Duration) (now *time.Time, offset *time.Duration) { + results := make(chan *ntpResult, len(servers)) + + _, cancel := context.WithTimeout(context.Background(), timeout) + defer cancel() + + for _, server := range servers { + go func(server string) { + scopedLogger := t.l.With(). + Str("server", server). + Logger() + + // increase request count + metricNtpTotalRequestCount.Inc() + metricNtpRequestCount.WithLabelValues(server).Inc() + + // query the server + now, response, err := queryNtpServer(server, timeout) + if err != nil { + scopedLogger.Warn(). + Str("error", err.Error()). + Msg("failed to query NTP server") + results <- nil + return + } + + if response.IsKissOfDeath() { + scopedLogger.Warn(). + Str("kiss_code", response.KissCode). + Msg("ignoring NTP server kiss of death") + results <- nil + return + } + + rtt := float64(response.RTT.Milliseconds()) + + // set the last RTT + metricNtpServerLastRTT.WithLabelValues( + server, + ).Set(rtt) + + // set the RTT histogram + metricNtpServerRttHistogram.WithLabelValues( + server, + ).Observe(rtt) + + // set the server info + metricNtpServerInfo.WithLabelValues( + server, + response.ReferenceString(), + strconv.Itoa(int(response.Stratum)), + strconv.Itoa(int(response.Precision)), + ).Set(1) + + // increase success count + metricNtpTotalSuccessCount.Inc() + metricNtpSuccessCount.WithLabelValues(server).Inc() + + scopedLogger.Info(). + Str("time", now.Format(time.RFC3339)). + Str("reference", response.ReferenceString()). + Float64("rtt", rtt). + Str("clockOffset", response.ClockOffset.String()). + Uint8("stratum", response.Stratum). + Msg("NTP server returned time") + + cancel() + + results <- &ntpResult{ + now: now, + offset: &response.ClockOffset, + } + }(server) + } + + for range servers { + result := <-results + if result == nil { + continue + } + now, offset = result.now, result.offset + return + } + return +} + +func queryNtpServer(server string, timeout time.Duration) (now *time.Time, response *ntp.Response, err error) { + resp, err := ntp.QueryWithOptions(server, ntp.QueryOptions{Timeout: timeout}) + if err != nil { + return nil, nil, err + } + return &resp.Time, resp, nil +} diff --git a/internal/timesync/rtc.go b/internal/timesync/rtc.go new file mode 100644 index 0000000..92ee485 --- /dev/null +++ b/internal/timesync/rtc.go @@ -0,0 +1,26 @@ +package timesync + +import ( + "fmt" + "os" +) + +var ( + rtcDeviceSearchPaths = []string{ + "/dev/rtc", + "/dev/rtc0", + "/dev/rtc1", + "/dev/misc/rtc", + "/dev/misc/rtc0", + "/dev/misc/rtc1", + } +) + +func getRtcDevicePath() (string, error) { + for _, path := range rtcDeviceSearchPaths { + if _, err := os.Stat(path); err == nil { + return path, nil + } + } + return "", fmt.Errorf("rtc device not found") +} diff --git a/internal/timesync/rtc_linux.go b/internal/timesync/rtc_linux.go new file mode 100644 index 0000000..27e4ec7 --- /dev/null +++ b/internal/timesync/rtc_linux.go @@ -0,0 +1,105 @@ +//go:build linux + +package timesync + +import ( + "fmt" + "os" + "time" + + "golang.org/x/sys/unix" +) + +func TimetoRtcTime(t time.Time) unix.RTCTime { + return unix.RTCTime{ + Sec: int32(t.Second()), + Min: int32(t.Minute()), + Hour: int32(t.Hour()), + Mday: int32(t.Day()), + Mon: int32(t.Month() - 1), + Year: int32(t.Year() - 1900), + Wday: int32(0), + Yday: int32(0), + Isdst: int32(0), + } +} + +func RtcTimetoTime(t unix.RTCTime) time.Time { + return time.Date( + int(t.Year)+1900, + time.Month(t.Mon+1), + int(t.Mday), + int(t.Hour), + int(t.Min), + int(t.Sec), + 0, + time.UTC, + ) +} + +func (t *TimeSync) getRtcDevice() (*os.File, error) { + if t.rtcDevice == nil { + file, err := os.OpenFile(t.rtcDevicePath, os.O_RDWR, 0666) + if err != nil { + return nil, err + } + t.rtcDevice = file + } + return t.rtcDevice, nil +} + +func (t *TimeSync) getRtcDeviceFd() (int, error) { + device, err := t.getRtcDevice() + if err != nil { + return 0, err + } + return int(device.Fd()), nil +} + +// Read implements Read for the Linux RTC +func (t *TimeSync) readRtcTime() (time.Time, error) { + fd, err := t.getRtcDeviceFd() + if err != nil { + return time.Time{}, fmt.Errorf("failed to get RTC device fd: %w", err) + } + + rtcTime, err := unix.IoctlGetRTCTime(fd) + if err != nil { + return time.Time{}, fmt.Errorf("failed to get RTC time: %w", err) + } + + date := RtcTimetoTime(*rtcTime) + + return date, nil +} + +// Set implements Set for the Linux RTC +// ... +// It might be not accurate as the time consumed by the system call is not taken into account +// but it's good enough for our purposes +func (t *TimeSync) setRtcTime(tu time.Time) error { + rt := TimetoRtcTime(tu) + + fd, err := t.getRtcDeviceFd() + if err != nil { + return fmt.Errorf("failed to get RTC device fd: %w", err) + } + + currentRtcTime, err := t.readRtcTime() + if err != nil { + return fmt.Errorf("failed to read RTC time: %w", err) + } + + t.l.Info(). + Interface("rtc_time", tu). + Str("offset", tu.Sub(currentRtcTime).String()). + Msg("set rtc time") + + if err := unix.IoctlSetRTCTime(fd, &rt); err != nil { + return fmt.Errorf("failed to set RTC time: %w", err) + } + + metricRTCUpdateCount.Inc() + + return nil +} diff --git a/internal/timesync/rtc_notlinux.go b/internal/timesync/rtc_notlinux.go new file mode 100644 index 0000000..e3c1b20 --- /dev/null +++ b/internal/timesync/rtc_notlinux.go @@ -0,0 +1,16 @@ +//go:build !linux + +package timesync + +import ( + "errors" + "time" +) + +func (t *TimeSync) readRtcTime() (time.Time, error) { + return time.Now(), nil +} + +func (t *TimeSync) setRtcTime(tu time.Time) error { + return errors.New("not supported") +} diff --git a/internal/timesync/timesync.go b/internal/timesync/timesync.go new file mode 100644 index 0000000..db1c96e --- /dev/null +++ b/internal/timesync/timesync.go @@ -0,0 +1,259 @@ +package timesync + +import ( + "fmt" + "os" + "os/exec" + "sync" + "time" + + "github.com/jetkvm/kvm/internal/network" + "github.com/rs/zerolog" +) + +const ( + timeSyncRetryStep = 5 * time.Second + timeSyncRetryMaxInt = 1 * time.Minute + timeSyncWaitNetChkInt = 100 * time.Millisecond + timeSyncWaitNetUpInt = 3 * time.Second + timeSyncInterval = 1 * time.Hour + timeSyncTimeout = 2 * time.Second +) + +var ( + timeSyncRetryInterval = 0 * time.Second +) + +type TimeSync struct { + syncLock *sync.Mutex + l *zerolog.Logger + + networkConfig *network.NetworkConfig + dhcpNtpAddresses []string + + rtcDevicePath string + rtcDevice *os.File //nolint:unused + rtcLock *sync.Mutex + + syncSuccess bool + + preCheckFunc func() (bool, error) +} + +type TimeSyncOptions struct { + PreCheckFunc func() (bool, error) + Logger *zerolog.Logger + NetworkConfig *network.NetworkConfig +} + +type SyncMode struct { + Ntp bool + Http bool + Ordering []string + NtpUseFallback bool + HttpUseFallback bool +} + +func NewTimeSync(opts *TimeSyncOptions) *TimeSync { + rtcDevice, err := getRtcDevicePath() + if err != nil { + opts.Logger.Error().Err(err).Msg("failed to get RTC device path") + } else { + opts.Logger.Info().Str("path", rtcDevice).Msg("RTC device found") + } + + t := &TimeSync{ + syncLock: &sync.Mutex{}, + l: opts.Logger, + dhcpNtpAddresses: []string{}, + rtcDevicePath: rtcDevice, + rtcLock: &sync.Mutex{}, + preCheckFunc: opts.PreCheckFunc, + networkConfig: opts.NetworkConfig, + } + + if t.rtcDevicePath != "" { + rtcTime, _ := t.readRtcTime() + t.l.Info().Interface("rtc_time", rtcTime).Msg("read RTC time") + } + + return t +} + +func (t *TimeSync) SetDhcpNtpAddresses(addresses []string) { + t.dhcpNtpAddresses = addresses +} + +func (t *TimeSync) getSyncMode() SyncMode { + syncMode := SyncMode{ + Ntp: true, + Http: true, + Ordering: []string{"ntp_dhcp", "ntp", "http"}, + NtpUseFallback: true, + HttpUseFallback: true, + } + + if t.networkConfig != nil { + switch t.networkConfig.TimeSyncMode.String { + case "ntp_only": + syncMode.Http = false + case "http_only": + syncMode.Ntp = false + } + + if t.networkConfig.TimeSyncDisableFallback.Bool { + syncMode.NtpUseFallback = false + syncMode.HttpUseFallback = false + } + + var syncOrdering = t.networkConfig.TimeSyncOrdering + if len(syncOrdering) > 0 { + syncMode.Ordering = syncOrdering + } + } + + t.l.Debug().Strs("Ordering", syncMode.Ordering).Bool("Ntp", syncMode.Ntp).Bool("Http", syncMode.Http).Bool("NtpUseFallback", syncMode.NtpUseFallback).Bool("HttpUseFallback", syncMode.HttpUseFallback).Msg("sync mode") + + return syncMode +} +func (t *TimeSync) doTimeSync() { + metricTimeSyncStatus.Set(0) + for { + if ok, err := t.preCheckFunc(); !ok { + if err != nil { + t.l.Error().Err(err).Msg("pre-check failed") + } + time.Sleep(timeSyncWaitNetChkInt) + continue + } + + t.l.Info().Msg("syncing system time") + start := time.Now() + err := t.Sync() + if err != nil { + t.l.Error().Str("error", err.Error()).Msg("failed to sync system time") + + // retry after a delay + timeSyncRetryInterval += timeSyncRetryStep + time.Sleep(timeSyncRetryInterval) + // reset the retry interval if it exceeds the max interval + if timeSyncRetryInterval > timeSyncRetryMaxInt { + timeSyncRetryInterval = 0 + } + + continue + } + t.syncSuccess = true + t.l.Info().Str("now", time.Now().Format(time.RFC3339)). + Str("time_taken", time.Since(start).String()). + Msg("time sync successful") + + metricTimeSyncStatus.Set(1) + + time.Sleep(timeSyncInterval) // after the first sync is done + } +} + +func (t *TimeSync) Sync() error { + var ( + now *time.Time + offset *time.Duration + ) + + metricTimeSyncCount.Inc() + + syncMode := t.getSyncMode() + +Orders: + for _, mode := range syncMode.Ordering { + switch mode { + case "ntp_user_provided": + if syncMode.Ntp { + t.l.Info().Msg("using NTP custom servers") + now, offset = t.queryNetworkTime(t.networkConfig.TimeSyncNTPServers) + if now != nil { + t.l.Info().Str("source", "NTP").Time("now", *now).Msg("time obtained") + break Orders + } + } + case "ntp_dhcp": + if syncMode.Ntp { + t.l.Info().Msg("using NTP servers from DHCP") + now, offset = t.queryNetworkTime(t.dhcpNtpAddresses) + if now != nil { + t.l.Info().Str("source", "NTP DHCP").Time("now", *now).Msg("time obtained") + break Orders + } + } + case "ntp": + if syncMode.Ntp && syncMode.NtpUseFallback { + t.l.Info().Msg("using NTP fallback") + now, offset = t.queryNetworkTime(defaultNTPServers) + if now != nil { + t.l.Info().Str("source", "NTP fallback").Time("now", *now).Msg("time obtained") + break Orders + } + } + case "http_user_provided": + if syncMode.Http { + t.l.Info().Msg("using HTTP custom URLs") + now = t.queryAllHttpTime(t.networkConfig.TimeSyncHTTPUrls) + if now != nil { + t.l.Info().Str("source", "HTTP").Time("now", *now).Msg("time obtained") + break Orders + } + } + case "http": + if syncMode.Http && syncMode.HttpUseFallback { + t.l.Info().Msg("using HTTP fallback") + now = t.queryAllHttpTime(defaultHTTPUrls) + if now != nil { + t.l.Info().Str("source", "HTTP fallback").Time("now", *now).Msg("time obtained") + break Orders + } + } + default: + t.l.Warn().Str("mode", mode).Msg("unknown time sync mode, skipping") + } + } + + if now == nil { + return fmt.Errorf("failed to get time from any source") + } + + if offset != nil { + newNow := time.Now().Add(*offset) + now = &newNow + } + + err := t.setSystemTime(*now) + if err != nil { + return fmt.Errorf("failed to set system time: %w", err) + } + + metricTimeSyncSuccessCount.Inc() + + return nil +} + +func (t *TimeSync) IsSyncSuccess() bool { + return t.syncSuccess +} + +func (t *TimeSync) Start() { + go t.doTimeSync() +} + +func (t *TimeSync) setSystemTime(now time.Time) error { + nowStr := now.Format("2006-01-02 15:04:05") + output, err := exec.Command("date", "-s", nowStr).CombinedOutput() + if err != nil { + return fmt.Errorf("failed to run date -s: %w, %s", err, string(output)) + } + + if t.rtcDevicePath != "" { + return t.setRtcTime(now) + } + + return nil +} diff --git a/internal/udhcpc/options.go b/internal/udhcpc/options.go new file mode 100644 index 0000000..10c9f75 --- /dev/null +++ b/internal/udhcpc/options.go @@ -0,0 +1,12 @@ +package udhcpc + +func (u *DHCPClient) GetNtpServers() []string { + if u.lease == nil { + return nil + } + servers := make([]string, len(u.lease.NTPServers)) + for i, server := range u.lease.NTPServers { + servers[i] = server.String() + } + return servers +} diff --git a/internal/udhcpc/parser.go b/internal/udhcpc/parser.go new file mode 100644 index 0000000..66c3ba2 --- /dev/null +++ b/internal/udhcpc/parser.go @@ -0,0 +1,186 @@ +package udhcpc + +import ( + "bufio" + "encoding/json" + "fmt" + "net" + "os" + "reflect" + "strconv" + "strings" + "time" +) + +type Lease struct { + // from https://udhcp.busybox.net/README.udhcpc + IPAddress net.IP `env:"ip" json:"ip"` // The obtained IP + Netmask net.IP `env:"subnet" json:"netmask"` // The assigned subnet mask + Broadcast net.IP `env:"broadcast" json:"broadcast"` // The broadcast address for this network + TTL int `env:"ipttl" json:"ttl,omitempty"` // The TTL to use for this network + MTU int `env:"mtu" json:"mtu,omitempty"` // The MTU to use for this network + HostName string `env:"hostname" json:"hostname,omitempty"` // The assigned hostname + Domain string `env:"domain" json:"domain,omitempty"` // The domain name of the network + BootPNextServer net.IP `env:"siaddr" json:"bootp_next_server,omitempty"` // The bootp next server option + BootPServerName string `env:"sname" json:"bootp_server_name,omitempty"` // The bootp server name option + BootPFile string `env:"boot_file" json:"bootp_file,omitempty"` // The bootp boot file option + Timezone string `env:"timezone" json:"timezone,omitempty"` // Offset in seconds from UTC + Routers []net.IP `env:"router" json:"routers,omitempty"` // A list of routers + DNS []net.IP `env:"dns" json:"dns_servers,omitempty"` // A list of DNS servers + NTPServers []net.IP `env:"ntpsrv" json:"ntp_servers,omitempty"` // A list of NTP servers + LPRServers []net.IP `env:"lprsvr" json:"lpr_servers,omitempty"` // A list of LPR servers + TimeServers []net.IP `env:"timesvr" json:"_time_servers,omitempty"` // A list of time servers (obsolete) + IEN116NameServers []net.IP `env:"namesvr" json:"_name_servers,omitempty"` // A list of IEN 116 name servers (obsolete) + LogServers []net.IP `env:"logsvr" json:"_log_servers,omitempty"` // A list of MIT-LCS UDP log servers (obsolete) + CookieServers []net.IP `env:"cookiesvr" json:"_cookie_servers,omitempty"` // A list of RFC 865 cookie servers (obsolete) + WINSServers []net.IP `env:"wins" json:"_wins_servers,omitempty"` // A list of WINS servers + SwapServer net.IP `env:"swapsvr" json:"_swap_server,omitempty"` // The IP address of the client's swap server + BootSize int `env:"bootsize" json:"bootsize,omitempty"` // The length in 512 octect blocks of the bootfile + RootPath string `env:"rootpath" json:"root_path,omitempty"` // The path name of the client's root disk + LeaseTime time.Duration `env:"lease" json:"lease,omitempty"` // The lease time, in seconds + DHCPType string `env:"dhcptype" json:"dhcp_type,omitempty"` // DHCP message type (safely ignored) + ServerID string `env:"serverid" json:"server_id,omitempty"` // The IP of the server + Message string `env:"message" json:"reason,omitempty"` // Reason for a DHCPNAK + TFTPServerName string `env:"tftp" json:"tftp,omitempty"` // The TFTP server name + BootFileName string `env:"bootfile" json:"bootfile,omitempty"` // The boot file name + Uptime time.Duration `env:"uptime" json:"uptime,omitempty"` // The uptime of the device when the lease was obtained, in seconds + LeaseExpiry *time.Time `json:"lease_expiry,omitempty"` // The expiry time of the lease + isEmpty map[string]bool +} + +func (l *Lease) setIsEmpty(m map[string]bool) { + l.isEmpty = m +} + +func (l *Lease) IsEmpty(key string) bool { + return l.isEmpty[key] +} + +func (l *Lease) ToJSON() string { + json, err := json.Marshal(l) + if err != nil { + return "" + } + return string(json) +} + +func (l *Lease) SetLeaseExpiry() (time.Time, error) { + if l.Uptime == 0 || l.LeaseTime == 0 { + return time.Time{}, fmt.Errorf("uptime or lease time isn't set") + } + + // get the uptime of the device + + file, err := os.Open("/proc/uptime") + if err != nil { + return time.Time{}, fmt.Errorf("failed to open uptime file: %w", err) + } + defer file.Close() + + var uptime time.Duration + + scanner := bufio.NewScanner(file) + for scanner.Scan() { + text := scanner.Text() + parts := strings.Split(text, " ") + uptime, err = time.ParseDuration(parts[0] + "s") + + if err != nil { + return time.Time{}, fmt.Errorf("failed to parse uptime: %w", err) + } + } + + relativeLeaseRemaining := (l.Uptime + l.LeaseTime) - uptime + leaseExpiry := time.Now().Add(relativeLeaseRemaining) + + l.LeaseExpiry = &leaseExpiry + + return leaseExpiry, nil +} + +func UnmarshalDHCPCLease(lease *Lease, str string) error { + // parse the lease file as a map + data := make(map[string]string) + for _, line := range strings.Split(str, "\n") { + line = strings.TrimSpace(line) + // skip empty lines and comments + if line == "" || strings.HasPrefix(line, "#") { + continue + } + + parts := strings.SplitN(line, "=", 2) + if len(parts) != 2 { + continue + } + + key := strings.TrimSpace(parts[0]) + value := strings.TrimSpace(parts[1]) + + data[key] = value + } + + // now iterate over the lease struct and set the values + leaseType := reflect.TypeOf(lease).Elem() + leaseValue := reflect.ValueOf(lease).Elem() + + valuesParsed := make(map[string]bool) + + for i := 0; i < leaseType.NumField(); i++ { + field := leaseValue.Field(i) + + // get the env tag + key := leaseType.Field(i).Tag.Get("env") + if key == "" { + continue + } + + valuesParsed[key] = false + + // get the value from the data map + value, ok := data[key] + if !ok || value == "" { + continue + } + + switch field.Interface().(type) { + case string: + field.SetString(value) + case int: + val, err := strconv.Atoi(value) + if err != nil { + continue + } + field.SetInt(int64(val)) + case time.Duration: + val, err := time.ParseDuration(value + "s") + if err != nil { + continue + } + field.Set(reflect.ValueOf(val)) + case net.IP: + ip := net.ParseIP(value) + if ip == nil { + continue + } + field.Set(reflect.ValueOf(ip)) + case []net.IP: + val := make([]net.IP, 0) + for _, ipStr := range strings.Fields(value) { + ip := net.ParseIP(ipStr) + if ip == nil { + continue + } + val = append(val, ip) + } + field.Set(reflect.ValueOf(val)) + default: + return fmt.Errorf("unsupported field `%s` type: %s", key, field.Type().String()) + } + + valuesParsed[key] = true + } + + lease.setIsEmpty(valuesParsed) + + return nil +} diff --git a/internal/udhcpc/parser_test.go b/internal/udhcpc/parser_test.go new file mode 100644 index 0000000..423ab53 --- /dev/null +++ b/internal/udhcpc/parser_test.go @@ -0,0 +1,74 @@ +package udhcpc + +import ( + "testing" + "time" +) + +func TestUnmarshalDHCPCLease(t *testing.T) { + lease := &Lease{} + err := UnmarshalDHCPCLease(lease, ` +# generated @ Mon Jan 4 19:31:53 UTC 2021 +# 19:31:53 up 0 min, 0 users, load average: 0.72, 0.14, 0.04 +# the date might be inaccurate if the clock is not set +ip=192.168.0.240 +siaddr=192.168.0.1 +sname= +boot_file= +subnet=255.255.255.0 +timezone= +router=192.168.0.1 +timesvr= +namesvr= +dns=172.19.53.2 +logsvr= +cookiesvr= +lprsvr= +hostname= +bootsize= +domain= +swapsvr= +rootpath= +ipttl= +mtu= +broadcast= +ntpsrv=162.159.200.123 +wins= +lease=172800 +dhcptype= +serverid=192.168.0.1 +message= +tftp= +bootfile= + `) + if lease.IPAddress.String() != "192.168.0.240" { + t.Fatalf("expected ip to be 192.168.0.240, got %s", lease.IPAddress.String()) + } + if lease.Netmask.String() != "255.255.255.0" { + t.Fatalf("expected netmask to be 255.255.255.0, got %s", lease.Netmask.String()) + } + if len(lease.Routers) != 1 { + t.Fatalf("expected 1 router, got %d", len(lease.Routers)) + } + if lease.Routers[0].String() != "192.168.0.1" { + t.Fatalf("expected router to be 192.168.0.1, got %s", lease.Routers[0].String()) + } + if len(lease.NTPServers) != 1 { + t.Fatalf("expected 1 timeserver, got %d", len(lease.NTPServers)) + } + if lease.NTPServers[0].String() != "162.159.200.123" { + t.Fatalf("expected timeserver to be 162.159.200.123, got %s", lease.NTPServers[0].String()) + } + if len(lease.DNS) != 1 { + t.Fatalf("expected 1 dns, got %d", len(lease.DNS)) + } + if lease.DNS[0].String() != "172.19.53.2" { + t.Fatalf("expected dns to be 172.19.53.2, got %s", lease.DNS[0].String()) + } + if lease.LeaseTime != 172800*time.Second { + t.Fatalf("expected lease time to be 172800 seconds, got %d", lease.LeaseTime) + } + if err != nil { + t.Fatal(err) + } +} diff --git a/internal/udhcpc/proc.go b/internal/udhcpc/proc.go new file mode 100644 index 0000000..69c2ab9 --- /dev/null +++ b/internal/udhcpc/proc.go @@ -0,0 +1,212 @@ +package udhcpc + +import ( + "bytes" + "errors" + "fmt" + "io" + "os" + "path/filepath" + "strconv" + "strings" + "syscall" +) + +func readFileNoStat(filename string) ([]byte, error) { + const maxBufferSize = 1024 * 1024 + + f, err := os.Open(filename) + if err != nil { + return nil, err + } + defer f.Close() + + reader := io.LimitReader(f, maxBufferSize) + return io.ReadAll(reader) +} + +func toCmdline(path string) ([]string, error) { + data, err := readFileNoStat(path) + if err != nil { + return nil, err + } + + if len(data) < 1 { + return []string{}, nil + } + + return strings.Split(string(bytes.TrimRight(data, "\x00")), "\x00"), nil +} + +func (p *DHCPClient) findUdhcpcProcess() (int, error) { + // read procfs for udhcpc processes + // we do not use procfs.AllProcs() because we want to avoid the overhead of reading the entire procfs + processes, err := os.ReadDir("/proc") + if err != nil { + return 0, err + } + + // iterate over the processes + for _, d := range processes { + // check if file is numeric + pid, err := strconv.Atoi(d.Name()) + if err != nil { + continue + } + + // check if it's a directory + if !d.IsDir() { + continue + } + + cmdline, err := toCmdline(filepath.Join("/proc", d.Name(), "cmdline")) + if err != nil { + continue + } + + if len(cmdline) < 1 { + continue + } + + if cmdline[0] != "udhcpc" { + continue + } + + cmdlineText := strings.Join(cmdline, " ") + + // check if it's a udhcpc process + if strings.Contains(cmdlineText, fmt.Sprintf("-i %s", p.InterfaceName)) { + p.logger.Debug(). + Str("pid", d.Name()). + Interface("cmdline", cmdline). + Msg("found udhcpc process") + return pid, nil + } + } + + return 0, errors.New("udhcpc process not found") +} + +func (c *DHCPClient) getProcessPid() (int, error) { + var pid int + if c.pidFile != "" { + // try to read the pid file + pidHandle, err := os.ReadFile(c.pidFile) + if err != nil { + c.logger.Warn().Err(err). + Str("pidFile", c.pidFile).Msg("failed to read udhcpc pid file") + } + + // if it exists, try to read the pid + if pidHandle != nil { + pidFromFile, err := strconv.Atoi(string(pidHandle)) + if err != nil { + c.logger.Warn().Err(err). + Str("pidFile", c.pidFile).Msg("failed to convert pid file to int") + } + pid = pidFromFile + } + } + + // if the pid is 0, try to find the pid using procfs + if pid == 0 { + newPid, err := c.findUdhcpcProcess() + if err != nil { + return 0, err + } + pid = newPid + } + + return pid, nil +} + +func (c *DHCPClient) getProcess() *os.Process { + pid, err := c.getProcessPid() + if err != nil { + return nil + } + + process, err := os.FindProcess(pid) + if err != nil { + c.logger.Warn().Err(err). + Int("pid", pid).Msg("failed to find process") + return nil + } + + return process +} + +func (c *DHCPClient) GetProcess() *os.Process { + if c.process == nil { + process := c.getProcess() + if process == nil { + return nil + } + c.process = process + } + + err := c.process.Signal(syscall.Signal(0)) + if err != nil && errors.Is(err, os.ErrProcessDone) { + oldPid := c.process.Pid + + c.process = nil + c.process = c.getProcess() + if c.process == nil { + c.logger.Error().Msg("failed to find new udhcpc process") + return nil + } + c.logger.Warn(). + Int("oldPid", oldPid). + Int("newPid", c.process.Pid). + Msg("udhcpc process pid changed") + } else if err != nil { + c.logger.Warn().Err(err). + Int("pid", c.process.Pid).Msg("udhcpc process is not running") + } + + return c.process +} + +func (c *DHCPClient) KillProcess() error { + process := c.GetProcess() + if process == nil { + return nil + } + + return process.Kill() +} + +func (c *DHCPClient) ReleaseProcess() error { + process := c.GetProcess() + if process == nil { + return nil + } + + return process.Release() +} + +func (c *DHCPClient) signalProcess(sig syscall.Signal) error { + process := c.GetProcess() + if process == nil { + return nil + } + + s := process.Signal(sig) + if s != nil { + c.logger.Warn().Err(s). + Int("pid", process.Pid). + Str("signal", sig.String()). + Msg("failed to signal udhcpc process") + return s + } + + return nil +} + +func (c *DHCPClient) Renew() error { + return c.signalProcess(syscall.SIGUSR1) +} + +func (c *DHCPClient) Release() error { + return c.signalProcess(syscall.SIGUSR2) +} diff --git a/internal/udhcpc/udhcpc.go b/internal/udhcpc/udhcpc.go new file mode 100644 index 0000000..128ea66 --- /dev/null +++ b/internal/udhcpc/udhcpc.go @@ -0,0 +1,198 @@ +package udhcpc + +import ( + "errors" + "fmt" + "os" + "path/filepath" + "reflect" + "time" + + "github.com/fsnotify/fsnotify" + "github.com/rs/zerolog" +) + +const ( + DHCPLeaseFile = "/run/udhcpc.%s.info" + DHCPPidFile = "/run/udhcpc.%s.pid" +) + +type DHCPClient struct { + InterfaceName string + leaseFile string + pidFile string + lease *Lease + logger *zerolog.Logger + process *os.Process + onLeaseChange func(lease *Lease) +} + +type DHCPClientOptions struct { + InterfaceName string + PidFile string + Logger *zerolog.Logger + OnLeaseChange func(lease *Lease) +} + +var defaultLogger = zerolog.New(os.Stdout).Level(zerolog.InfoLevel) + +func NewDHCPClient(options *DHCPClientOptions) *DHCPClient { + if options.Logger == nil { + options.Logger = &defaultLogger + } + + l := options.Logger.With().Str("interface", options.InterfaceName).Logger() + return &DHCPClient{ + InterfaceName: options.InterfaceName, + logger: &l, + leaseFile: fmt.Sprintf(DHCPLeaseFile, options.InterfaceName), + pidFile: options.PidFile, + onLeaseChange: options.OnLeaseChange, + } +} + +func (c *DHCPClient) getWatchPaths() []string { + watchPaths := make(map[string]interface{}) + watchPaths[filepath.Dir(c.leaseFile)] = nil + + if c.pidFile != "" { + watchPaths[filepath.Dir(c.pidFile)] = nil + } + + paths := make([]string, 0) + for path := range watchPaths { + paths = append(paths, path) + } + return paths +} + +// Run starts the DHCP client and watches the lease file for changes. +// this isn't a blocking call, and the lease file is reloaded when a change is detected. +func (c *DHCPClient) Run() error { + err := c.loadLeaseFile() + if err != nil && !errors.Is(err, os.ErrNotExist) { + return err + } + + watcher, err := fsnotify.NewWatcher() + if err != nil { + return err + } + defer watcher.Close() + + go func() { + for { + select { + case event, ok := <-watcher.Events: + if !ok { + continue + } + if !event.Has(fsnotify.Write) && !event.Has(fsnotify.Create) { + continue + } + + if event.Name == c.leaseFile { + c.logger.Debug(). + Str("event", event.Op.String()). + Str("path", event.Name). + Msg("udhcpc lease file updated, reloading lease") + _ = c.loadLeaseFile() + } + case err, ok := <-watcher.Errors: + if !ok { + return + } + c.logger.Error().Err(err).Msg("error watching lease file") + } + } + }() + + for _, path := range c.getWatchPaths() { + err = watcher.Add(path) + if err != nil { + c.logger.Error(). + Err(err). + Str("path", path). + Msg("failed to watch directory") + return err + } + } + + // TODO: update udhcpc pid file + // we'll comment this out for now because the pid might change + // process := c.GetProcess() + // if process == nil { + // c.logger.Error().Msg("udhcpc process not found") + // } + + // block the goroutine until the lease file is updated + <-make(chan struct{}) + + return nil +} + +func (c *DHCPClient) loadLeaseFile() error { + file, err := os.ReadFile(c.leaseFile) + if err != nil { + return err + } + + data := string(file) + if data == "" { + c.logger.Debug().Msg("udhcpc lease file is empty") + return nil + } + + lease := &Lease{} + err = UnmarshalDHCPCLease(lease, string(file)) + if err != nil { + return err + } + + isFirstLoad := c.lease == nil + + // Skip processing if lease hasn't changed to avoid unnecessary wake-ups. + if reflect.DeepEqual(c.lease, lease) { + return nil + } + + c.lease = lease + + if lease.IPAddress == nil { + c.logger.Info(). + Interface("lease", lease). + Str("data", string(file)). + Msg("udhcpc lease cleared") + return nil + } + + msg := "udhcpc lease updated" + if isFirstLoad { + msg = "udhcpc lease loaded" + } + + leaseExpiry, err := lease.SetLeaseExpiry() + if err != nil { + c.logger.Error().Err(err).Msg("failed to get dhcp lease expiry") + } else { + expiresIn := time.Until(leaseExpiry) + c.logger.Info(). + Interface("expiry", leaseExpiry). + Str("expiresIn", expiresIn.String()). + Msg("current dhcp lease expiry time calculated") + } + + c.onLeaseChange(lease) + + c.logger.Info(). + Str("ip", lease.IPAddress.String()). + Str("leaseTime", lease.LeaseTime.String()). + Interface("data", lease). + Msg(msg) + + return nil +} + +func (c *DHCPClient) GetLease() *Lease { + return c.lease +} diff --git a/internal/usbgadget/changeset.go b/internal/usbgadget/changeset.go new file mode 100644 index 0000000..57f5d7d --- /dev/null +++ b/internal/usbgadget/changeset.go @@ -0,0 +1,436 @@ +package usbgadget + +import ( + "bytes" + "fmt" + "os" + "path/filepath" + "reflect" + "time" + + "github.com/prometheus/procfs" + "github.com/sourcegraph/tf-dag/dag" +) + +// it's a minimalistic implementation of ansible's file module with some modifications +// to make it more suitable for our use case +// https://docs.ansible.com/ansible/latest/modules/file_module.html + +// we use this to check if the files in the gadget config are in the expected state +// and to update them if they are not in the expected state + +type FileState uint8 +type ChangeState uint8 +type FileChangeResolvedAction uint8 + +type ApplyFunc func(c *ChangeSet, changes []*FileChange) error + +const ( + FileStateUnknown FileState = iota + FileStateAbsent + FileStateDirectory + FileStateFile + FileStateFileContentMatch + FileStateFileWrite // update file content without checking + FileStateMounted + FileStateMountedConfigFS + FileStateSymlink + FileStateSymlinkInOrderConfigFS // configfs is a shithole, so we need to check if the symlinks are created in the correct order + FileStateSymlinkNotInOrderConfigFS + FileStateTouch +) + +var FileStateString = map[FileState]string{ + FileStateUnknown: "UNKNOWN", + FileStateAbsent: "ABSENT", + FileStateDirectory: "DIRECTORY", + FileStateFile: "FILE", + FileStateFileContentMatch: "FILE_CONTENT_MATCH", + FileStateFileWrite: "FILE_WRITE", + FileStateMounted: "MOUNTED", + FileStateMountedConfigFS: "CONFIGFS_MOUNTED", + FileStateSymlink: "SYMLINK", + FileStateSymlinkInOrderConfigFS: "SYMLINK_IN_ORDER_CONFIGFS", + FileStateTouch: "TOUCH", +} + +const ( + ChangeStateUnknown ChangeState = iota + ChangeStateRequired + ChangeStateNotChanged + ChangeStateChanged + ChangeStateError +) + +const ( + FileChangeResolvedActionUnknown FileChangeResolvedAction = iota + FileChangeResolvedActionDoNothing + FileChangeResolvedActionRemove + FileChangeResolvedActionCreateFile + FileChangeResolvedActionWriteFile + FileChangeResolvedActionUpdateFile + FileChangeResolvedActionAppendFile + FileChangeResolvedActionCreateSymlink + FileChangeResolvedActionRecreateSymlink + FileChangeResolvedActionCreateDirectoryAndSymlinks + FileChangeResolvedActionReorderSymlinks + FileChangeResolvedActionCreateDirectory + FileChangeResolvedActionRemoveDirectory + FileChangeResolvedActionTouch + FileChangeResolvedActionMountConfigFS +) + +var FileChangeResolvedActionString = map[FileChangeResolvedAction]string{ + FileChangeResolvedActionUnknown: "UNKNOWN", + FileChangeResolvedActionDoNothing: "DO_NOTHING", + FileChangeResolvedActionRemove: "REMOVE", + FileChangeResolvedActionCreateFile: "FILE_CREATE", + FileChangeResolvedActionWriteFile: "FILE_WRITE", + FileChangeResolvedActionUpdateFile: "FILE_UPDATE", + FileChangeResolvedActionAppendFile: "FILE_APPEND", + FileChangeResolvedActionCreateSymlink: "SYMLINK_CREATE", + FileChangeResolvedActionRecreateSymlink: "SYMLINK_RECREATE", + FileChangeResolvedActionCreateDirectoryAndSymlinks: "DIR_CREATE_AND_SYMLINKS", + FileChangeResolvedActionReorderSymlinks: "SYMLINK_REORDER", + FileChangeResolvedActionCreateDirectory: "DIR_CREATE", + FileChangeResolvedActionRemoveDirectory: "DIR_REMOVE", + FileChangeResolvedActionTouch: "TOUCH", + FileChangeResolvedActionMountConfigFS: "CONFIGFS_MOUNT", +} + +type ChangeSet struct { + Changes []FileChange +} + +type RequestedFileChange struct { + Component string + Key string + Path string // will be used as Key if Key is empty + ParamSymlinks []symlink + ExpectedState FileState + ExpectedContent []byte + DependsOn []string + BeforeChange []string // if the file is going to be changed, apply the change first + Description string + IgnoreErrors bool + When string // only apply the change if when meets the condition +} + +type FileChange struct { + RequestedFileChange + ActualState FileState + ActualContent []byte + resolvedDeps []string + checked bool + changed ChangeState + action FileChangeResolvedAction +} + +func (f *RequestedFileChange) String() string { + var s string + switch f.ExpectedState { + case FileStateDirectory: + s = fmt.Sprintf("dir: %s", f.Path) + case FileStateFile: + s = fmt.Sprintf("file: %s", f.Path) + case FileStateSymlink: + s = fmt.Sprintf("symlink: %s -> %s", f.Path, f.ExpectedContent) + case FileStateSymlinkInOrderConfigFS: + s = fmt.Sprintf("symlink_in_order_configfs: %s -> %s", f.Path, f.ExpectedContent) + case FileStateSymlinkNotInOrderConfigFS: + s = fmt.Sprintf("symlink_not_in_order_configfs: %s -> %s", f.Path, f.ExpectedContent) + case FileStateAbsent: + s = fmt.Sprintf("absent: %s", f.Path) + case FileStateFileContentMatch: + s = fmt.Sprintf("file: %s with content [%s]", f.Path, f.ExpectedContent) + case FileStateFileWrite: + s = fmt.Sprintf("write: %s with content [%s]", f.Path, f.ExpectedContent) + case FileStateMountedConfigFS: + s = fmt.Sprintf("configfs: %s", f.Path) + case FileStateTouch: + s = fmt.Sprintf("touch: %s", f.Path) + case FileStateUnknown: + s = fmt.Sprintf("unknown change for %s", f.Path) + default: + s = fmt.Sprintf("unknown expected state %d for %s", f.ExpectedState, f.Path) + } + + if len(f.Description) > 0 { + s += fmt.Sprintf(" (%s)", f.Description) + } + + return s +} + +func (f *RequestedFileChange) IsSame(other *RequestedFileChange) bool { + return f.Path == other.Path && + f.ExpectedState == other.ExpectedState && + reflect.DeepEqual(f.ExpectedContent, other.ExpectedContent) && + reflect.DeepEqual(f.DependsOn, other.DependsOn) && + f.IgnoreErrors == other.IgnoreErrors +} + +func (fc *FileChange) checkIfDirIsMountPoint() error { + // check if the file is a mount point + mounts, err := procfs.GetMounts() + if err != nil { + return fmt.Errorf("failed to get mounts") + } + + for _, mount := range mounts { + if mount.MountPoint == fc.Path { + fc.ActualState = FileStateMounted + fc.ActualContent = []byte(mount.Source) + + if mount.FSType == "configfs" { + fc.ActualState = FileStateMountedConfigFS + } + + return nil + } + } + + return nil +} + +// GetActualState returns the actual state of the file at the given path. +func (fc *FileChange) getActualState() error { + l := defaultLogger.With().Str("path", fc.Path).Logger() + + fi, err := os.Lstat(fc.Path) + if err != nil { + if os.IsNotExist(err) { + fc.ActualState = FileStateAbsent + } else { + l.Warn().Err(err).Msg("failed to stat file") + fc.ActualState = FileStateUnknown + } + return nil + } + + // check if the file is a symlink + if fi.Mode()&os.ModeSymlink == os.ModeSymlink { + fc.ActualState = FileStateSymlink + // get the target of the symlink + target, err := os.Readlink(fc.Path) + if err != nil { + l.Warn().Err(err).Msg("failed to read symlink") + return fmt.Errorf("failed to read symlink") + } + // check if the target is a relative path + if !filepath.IsAbs(target) { + // make it absolute + target, err = filepath.Abs(filepath.Join(filepath.Dir(fc.Path), target)) + if err != nil { + l.Warn().Err(err).Msg("failed to make symlink target absolute") + return fmt.Errorf("failed to make symlink target absolute") + } + } + fc.ActualContent = []byte(target) + return nil + } + + if fi.IsDir() { + fc.ActualState = FileStateDirectory + + switch fc.ExpectedState { + case FileStateMountedConfigFS: + err := fc.checkIfDirIsMountPoint() + if err != nil { + l.Warn().Err(err).Msg("failed to check if dir is mount point") + return err + } + case FileStateSymlinkInOrderConfigFS: + state, err := checkIfSymlinksInOrder(fc, &l) + if err != nil { + l.Warn().Err(err).Msg("failed to check if symlinks are in order") + return err + } + fc.ActualState = state + } + return nil + } + + if fi.Mode()&os.ModeDevice == os.ModeDevice { + l.Info().Msg("file is a device") + return nil + } + + // check if the file is a regular file + if fi.Mode().IsRegular() { + fc.ActualState = FileStateFile + // get the content of the file + content, err := os.ReadFile(fc.Path) + if err != nil { + l.Warn().Err(err).Msg("failed to read file") + return fmt.Errorf("failed to read file") + } + fc.ActualContent = content + return nil + } + + l.Warn().Interface("file_info", fi.Mode()).Bool("is_dir", fi.IsDir()).Msg("unknown file type") + + return fmt.Errorf("unknown file type") +} + +func (fc *FileChange) ResetActionResolution() { + fc.checked = false + fc.action = FileChangeResolvedActionUnknown + fc.changed = ChangeStateUnknown +} + +func (fc *FileChange) Action() FileChangeResolvedAction { + if !fc.checked { + fc.action = fc.getFileChangeResolvedAction() + fc.checked = true + } + + return fc.action +} + +func (fc *FileChange) getFileChangeResolvedAction() FileChangeResolvedAction { + l := defaultLogger.With().Str("path", fc.Path).Logger() + + // some actions are not needed to be checked + switch fc.ExpectedState { + case FileStateFileWrite: + return FileChangeResolvedActionWriteFile + case FileStateTouch: + return FileChangeResolvedActionTouch + } + + // get the actual state of the file + err := fc.getActualState() + if err != nil { + return FileChangeResolvedActionDoNothing + } + + baseName := filepath.Base(fc.Path) + + switch fc.ExpectedState { + case FileStateDirectory: + // if the file is already a directory, do nothing + if fc.ActualState == FileStateDirectory { + return FileChangeResolvedActionDoNothing + } + return FileChangeResolvedActionCreateDirectory + case FileStateFile: + // if the file is already a file, do nothing + if fc.ActualState == FileStateFile { + return FileChangeResolvedActionDoNothing + } + return FileChangeResolvedActionCreateFile + case FileStateFileContentMatch: + // if the file is already a file with the expected content, do nothing + if fc.ActualState == FileStateFile { + looserMatch := baseName == "inquiry_string" + if compareFileContent(fc.ActualContent, fc.ExpectedContent, looserMatch) { + return FileChangeResolvedActionDoNothing + } + // TODO: move this to somewhere else + // this is a workaround for the fact that the file is not updated if it has no content + if baseName == "file" && + bytes.Equal(fc.ActualContent, []byte{}) && + bytes.Equal(fc.ExpectedContent, []byte{0x0a}) { + return FileChangeResolvedActionDoNothing + } + return FileChangeResolvedActionUpdateFile + } + return FileChangeResolvedActionCreateFile + case FileStateSymlink: + // if the file is already a symlink, check if the target is the same + if fc.ActualState == FileStateSymlink { + if reflect.DeepEqual(fc.ActualContent, fc.ExpectedContent) { + return FileChangeResolvedActionDoNothing + } + return FileChangeResolvedActionRecreateSymlink + } + return FileChangeResolvedActionCreateSymlink + case FileStateSymlinkInOrderConfigFS: + // if the file is already a symlink, check if the target is the same + if fc.ActualState == FileStateSymlinkInOrderConfigFS { + return FileChangeResolvedActionDoNothing + } + return FileChangeResolvedActionReorderSymlinks + case FileStateAbsent: + if fc.ActualState == FileStateAbsent { + return FileChangeResolvedActionDoNothing + } + return FileChangeResolvedActionRemove + case FileStateMountedConfigFS: + if fc.ActualState == FileStateMountedConfigFS { + return FileChangeResolvedActionDoNothing + } + return FileChangeResolvedActionMountConfigFS + default: + l.Warn().Interface("file_change", FileStateString[fc.ExpectedState]).Msg("unknown expected state") + return FileChangeResolvedActionDoNothing + } +} + +func (c *ChangeSet) AddFileChangeStruct(r RequestedFileChange) { + fc := FileChange{ + RequestedFileChange: r, + } + c.Changes = append(c.Changes, fc) +} + +func (c *ChangeSet) AddFileChange(component string, path string, expectedState FileState, expectedContent []byte, dependsOn []string, description string) { + c.AddFileChangeStruct(RequestedFileChange{ + Component: component, + Path: path, + ExpectedState: expectedState, + ExpectedContent: expectedContent, + DependsOn: dependsOn, + Description: description, + }) +} + +func (c *ChangeSet) ApplyChanges() error { + r := ChangeSetResolver{ + changeset: c, + g: &dag.AcyclicGraph{}, + l: defaultLogger, + } + + return r.Apply() +} + +func (c *ChangeSet) applyChange(change *FileChange) error { + switch change.Action() { + case FileChangeResolvedActionWriteFile: + return os.WriteFile(change.Path, change.ExpectedContent, 0644) + case FileChangeResolvedActionUpdateFile: + return os.WriteFile(change.Path, change.ExpectedContent, 0644) + case FileChangeResolvedActionCreateFile: + return os.WriteFile(change.Path, change.ExpectedContent, 0644) + case FileChangeResolvedActionCreateSymlink: + return os.Symlink(string(change.ExpectedContent), change.Path) + case FileChangeResolvedActionRecreateSymlink: + if err := os.Remove(change.Path); err != nil { + return fmt.Errorf("failed to remove symlink: %w", err) + } + return os.Symlink(string(change.ExpectedContent), change.Path) + case FileChangeResolvedActionReorderSymlinks: + return recreateSymlinks(change, nil) + case FileChangeResolvedActionCreateDirectory: + return os.MkdirAll(change.Path, 0755) + case FileChangeResolvedActionRemove: + return os.Remove(change.Path) + case FileChangeResolvedActionRemoveDirectory: + return os.RemoveAll(change.Path) + case FileChangeResolvedActionTouch: + return os.Chtimes(change.Path, time.Now(), time.Now()) + case FileChangeResolvedActionMountConfigFS: + return mountConfigFS(change.Path) + case FileChangeResolvedActionDoNothing: + return nil + default: + return fmt.Errorf("unknown action: %d", change.Action()) + } +} + +func (c *ChangeSet) Apply() error { + return c.ApplyChanges() +} diff --git a/internal/usbgadget/changeset_arm_test.go b/internal/usbgadget/changeset_arm_test.go new file mode 100644 index 0000000..8c0abd5 --- /dev/null +++ b/internal/usbgadget/changeset_arm_test.go @@ -0,0 +1,115 @@ +//go:build arm && linux + +package usbgadget + +import ( + "os" + "strings" + "testing" + + "github.com/stretchr/testify/assert" +) + +var ( + usbConfig = &Config{ + VendorId: "0x1d6b", //The Linux Foundation + ProductId: "0x0104", //Multifunction Composite Gadget + SerialNumber: "", + Manufacturer: "JetKVM", + Product: "USB Emulation Device", + strictMode: true, + } + usbDevices = &Devices{ + AbsoluteMouse: true, + RelativeMouse: true, + Keyboard: true, + MassStorage: true, + } + usbGadgetName = "jetkvm" + usbGadget *UsbGadget +) + +var oldAbsoluteMouseCombinedReportDesc = []byte{ + 0x05, 0x01, // Usage Page (Generic Desktop Ctrls) + 0x09, 0x02, // Usage (Mouse) + 0xA1, 0x01, // Collection (Application) + + // Report ID 1: Absolute Mouse Movement + 0x85, 0x01, // Report ID (1) + 0x09, 0x01, // Usage (Pointer) + 0xA1, 0x00, // Collection (Physical) + 0x05, 0x09, // Usage Page (Button) + 0x19, 0x01, // Usage Minimum (0x01) + 0x29, 0x03, // Usage Maximum (0x03) + 0x15, 0x00, // Logical Minimum (0) + 0x25, 0x01, // Logical Maximum (1) + 0x75, 0x01, // Report Size (1) + 0x95, 0x03, // Report Count (3) + 0x81, 0x02, // Input (Data, Var, Abs) + 0x95, 0x01, // Report Count (1) + 0x75, 0x05, // Report Size (5) + 0x81, 0x03, // Input (Cnst, Var, Abs) + 0x05, 0x01, // Usage Page (Generic Desktop Ctrls) + 0x09, 0x30, // Usage (X) + 0x09, 0x31, // Usage (Y) + 0x16, 0x00, 0x00, // Logical Minimum (0) + 0x26, 0xFF, 0x7F, // Logical Maximum (32767) + 0x36, 0x00, 0x00, // Physical Minimum (0) + 0x46, 0xFF, 0x7F, // Physical Maximum (32767) + 0x75, 0x10, // Report Size (16) + 0x95, 0x02, // Report Count (2) + 0x81, 0x02, // Input (Data, Var, Abs) + 0xC0, // End Collection + + // Report ID 2: Relative Wheel Movement + 0x85, 0x02, // Report ID (2) + 0x09, 0x38, // Usage (Wheel) + 0x15, 0x81, // Logical Minimum (-127) + 0x25, 0x7F, // Logical Maximum (127) + 0x75, 0x08, // Report Size (8) + 0x95, 0x01, // Report Count (1) + 0x81, 0x06, // Input (Data, Var, Rel) + + 0xC0, // End Collection +} + +func TestUsbGadgetInit(t *testing.T) { + assert := assert.New(t) + usbGadget = NewUsbGadget(usbGadgetName, usbDevices, usbConfig, nil) + + assert.NotNil(usbGadget) +} + +func TestUsbGadgetStrictModeInitFail(t *testing.T) { + usbConfig.strictMode = true + u := NewUsbGadget("test", usbDevices, usbConfig, nil) + assert.Nil(t, u, "should be nil") +} + +func TestUsbGadgetUDCNotBoundAfterReportDescrChanged(t *testing.T) { + assert := assert.New(t) + usbGadget = NewUsbGadget(usbGadgetName, usbDevices, usbConfig, nil) + assert.NotNil(usbGadget) + + // release the usb gadget and create a new one + usbGadget = nil + + altGadgetConfig := defaultGadgetConfig + + oldAbsoluteMouseConfig := altGadgetConfig["absolute_mouse"] + oldAbsoluteMouseConfig.reportDesc = oldAbsoluteMouseCombinedReportDesc + altGadgetConfig["absolute_mouse"] = oldAbsoluteMouseConfig + + usbGadget = newUsbGadget(usbGadgetName, altGadgetConfig, usbDevices, usbConfig, nil) + assert.NotNil(usbGadget) + + udcs := getUdcs() + assert.Equal(1, len(udcs), "should be only one UDC") + // check if the UDC is bound + udc := udcs[0] + assert.NotNil(udc, "UDC should exist") + + udcStr, err := os.ReadFile("/sys/kernel/config/usb_gadget/jetkvm/UDC") + assert.Nil(err, "usb_gadget/UDC should exist") + assert.Equal(strings.TrimSpace(udc), strings.TrimSpace(string(udcStr)), "UDC should be the same") +} diff --git a/internal/usbgadget/changeset_resolver.go b/internal/usbgadget/changeset_resolver.go new file mode 100644 index 0000000..67812e0 --- /dev/null +++ b/internal/usbgadget/changeset_resolver.go @@ -0,0 +1,192 @@ +package usbgadget + +import ( + "fmt" + + "github.com/rs/zerolog" + "github.com/sourcegraph/tf-dag/dag" +) + +type ChangeSetResolver struct { + changeset *ChangeSet + + l *zerolog.Logger + g *dag.AcyclicGraph + + changesMap map[string]*FileChange + conditionalChangesMap map[string]*FileChange + + orderedChanges []dag.Vertex + resolvedChanges []*FileChange + additionalResolveRequired bool +} + +func (c *ChangeSetResolver) toOrderedChanges() error { + for key, change := range c.changesMap { + v := c.g.Add(key) + + for _, dependsOn := range change.DependsOn { + c.g.Connect(dag.BasicEdge(dependsOn, v)) + } + for _, dependsOn := range change.resolvedDeps { + c.g.Connect(dag.BasicEdge(dependsOn, v)) + } + } + + cycles := c.g.Cycles() + if len(cycles) > 0 { + return fmt.Errorf("cycles detected: %v", cycles) + } + + orderedChanges := c.g.TopologicalOrder() + c.orderedChanges = orderedChanges + return nil +} + +func (c *ChangeSetResolver) doResolveChanges(initial bool) error { + resolvedChanges := make([]*FileChange, 0) + + for _, key := range c.orderedChanges { + change := c.changesMap[key.(string)] + if change == nil { + c.l.Error().Str("key", key.(string)).Msg("fileChange not found") + continue + } + + if !initial { + change.ResetActionResolution() + } + + resolvedAction := change.Action() + + resolvedChanges = append(resolvedChanges, change) + // no need to check the triggers if there's no change + if resolvedAction == FileChangeResolvedActionDoNothing { + continue + } + + if !initial { + continue + } + + if change.BeforeChange != nil { + change.resolvedDeps = append(change.resolvedDeps, change.BeforeChange...) + c.additionalResolveRequired = true + + // add the dependencies to the changes map + for _, dep := range change.BeforeChange { + depChange, ok := c.conditionalChangesMap[dep] + if !ok { + return fmt.Errorf("dependency %s not found", dep) + } + + c.changesMap[dep] = depChange + } + } + } + + c.resolvedChanges = resolvedChanges + return nil +} + +func (c *ChangeSetResolver) resolveChanges(initial bool) error { + // get the ordered changes + err := c.toOrderedChanges() + if err != nil { + return err + } + + // resolve the changes + err = c.doResolveChanges(initial) + if err != nil { + return err + } + + for _, change := range c.resolvedChanges { + c.l.Trace().Str("change", change.String()).Msg("resolved change") + } + + if !c.additionalResolveRequired || !initial { + return nil + } + + return c.resolveChanges(false) +} + +func (c *ChangeSetResolver) applyChanges() error { + for _, change := range c.resolvedChanges { + change.ResetActionResolution() + action := change.Action() + actionStr := FileChangeResolvedActionString[action] + + l := c.l.Info() + if action == FileChangeResolvedActionDoNothing { + l = c.l.Trace() + } + + l.Str("action", actionStr).Str("change", change.String()).Msg("applying change") + + err := c.changeset.applyChange(change) + if err != nil { + if change.IgnoreErrors { + c.l.Warn().Str("change", change.String()).Err(err).Msg("ignoring error") + } else { + return err + } + } + } + + return nil +} + +func (c *ChangeSetResolver) GetChanges() ([]*FileChange, error) { + localChanges := c.changeset.Changes + changesMap := make(map[string]*FileChange) + conditionalChangesMap := make(map[string]*FileChange) + + // build the map of the changes + for _, change := range localChanges { + key := change.Key + if key == "" { + key = change.Path + } + + // remove it from the map first + if change.When != "" { + conditionalChangesMap[key] = &change + continue + } + + if _, ok := changesMap[key]; ok { + if changesMap[key].IsSame(&change.RequestedFileChange) { + continue + } + return nil, fmt.Errorf( + "duplicate change: %s, current: %s, requested: %s", + key, + changesMap[key].String(), + change.String(), + ) + } + + changesMap[key] = &change + } + + c.changesMap = changesMap + c.conditionalChangesMap = conditionalChangesMap + + err := c.resolveChanges(true) + if err != nil { + return nil, err + } + + return c.resolvedChanges, nil +} + +func (c *ChangeSetResolver) Apply() error { + if _, err := c.GetChanges(); err != nil { + return err + } + + return c.applyChanges() +} diff --git a/internal/usbgadget/changeset_symlink.go b/internal/usbgadget/changeset_symlink.go new file mode 100644 index 0000000..d94c759 --- /dev/null +++ b/internal/usbgadget/changeset_symlink.go @@ -0,0 +1,136 @@ +package usbgadget + +import ( + "fmt" + "os" + "path" + "path/filepath" + "reflect" + + "github.com/rs/zerolog" +) + +type symlink struct { + Path string + Target string +} + +func compareSymlinks(expected []symlink, actual []symlink) bool { + if len(expected) != len(actual) { + return false + } + + return reflect.DeepEqual(expected, actual) +} + +func checkIfSymlinksInOrder(fc *FileChange, logger *zerolog.Logger) (FileState, error) { + if logger == nil { + logger = defaultLogger + } + l := logger.With().Str("path", fc.Path).Logger() + + if len(fc.ParamSymlinks) == 0 { + return FileStateUnknown, fmt.Errorf("no symlinks to check") + } + + fi, err := os.Lstat(fc.Path) + + if err != nil { + if os.IsNotExist(err) { + return FileStateAbsent, nil + } else { + l.Warn().Err(err).Msg("failed to stat file") + return FileStateUnknown, fmt.Errorf("failed to stat file") + } + } + + if !fi.IsDir() { + return FileStateUnknown, fmt.Errorf("file is not a directory") + } + + files, err := os.ReadDir(fc.Path) + symlinks := make([]symlink, 0) + if err != nil { + return FileStateUnknown, fmt.Errorf("failed to read directory") + } + + for _, file := range files { + if file.Type()&os.ModeSymlink != os.ModeSymlink { + continue + } + + path := filepath.Join(fc.Path, file.Name()) + target, err := os.Readlink(path) + if err != nil { + return FileStateUnknown, fmt.Errorf("failed to read symlink") + } + + if !filepath.IsAbs(target) { + target = filepath.Join(fc.Path, target) + newTarget, err := filepath.Abs(target) + if err != nil { + return FileStateUnknown, fmt.Errorf("failed to get absolute path") + } + target = newTarget + } + + symlinks = append(symlinks, symlink{ + Path: path, + Target: target, + }) + } + + // compare the symlinks with the expected symlinks + if compareSymlinks(fc.ParamSymlinks, symlinks) { + return FileStateSymlinkInOrderConfigFS, nil + } + + l.Trace().Interface("expected", fc.ParamSymlinks).Interface("actual", symlinks).Msg("symlinks are not in order") + + return FileStateSymlinkNotInOrderConfigFS, nil +} + +func recreateSymlinks(fc *FileChange, logger *zerolog.Logger) error { + if logger == nil { + logger = defaultLogger + } + // remove all symlinks + files, err := os.ReadDir(fc.Path) + if err != nil { + return fmt.Errorf("failed to read directory") + } + + l := logger.With().Str("path", fc.Path).Logger() + l.Info().Msg("recreate symlinks") + + for _, file := range files { + if file.Type()&os.ModeSymlink != os.ModeSymlink { + continue + } + l.Info().Str("name", file.Name()).Msg("remove symlink") + err := os.Remove(path.Join(fc.Path, file.Name())) + if err != nil { + return fmt.Errorf("failed to remove symlink") + } + } + + l.Info().Interface("param-symlinks", fc.ParamSymlinks).Msg("create symlinks") + + // create the symlinks + for _, symlink := range fc.ParamSymlinks { + l.Info().Str("name", symlink.Path).Str("target", symlink.Target).Msg("create symlink") + + path := symlink.Path + if !filepath.IsAbs(path) { + path = filepath.Join(fc.Path, path) + } + + err := os.Symlink(symlink.Target, path) + if err != nil { + l.Warn().Err(err).Msg("failed to create symlink") + return fmt.Errorf("failed to create symlink") + } + } + + return nil +} diff --git a/internal/usbgadget/config.go b/internal/usbgadget/config.go index 5cc3ed2..6d1bd39 100644 --- a/internal/usbgadget/config.go +++ b/internal/usbgadget/config.go @@ -2,11 +2,7 @@ package usbgadget import ( "fmt" - "os" "os/exec" - "path" - "path/filepath" - "sort" ) type gadgetConfigItem struct { @@ -34,8 +30,8 @@ var defaultGadgetConfig = map[string]gadgetConfigItem{ attrs: gadgetAttributes{ "bcdUSB": "0x0200", // USB 2.0 "idVendor": "0x1d6b", // The Linux Foundation - "idProduct": "0104", // Multifunction Composite Gadget - "bcdDevice": "0100", + "idProduct": "0x0104", // Multifunction Composite Gadget + "bcdDevice": "0x0100", // USB2 }, configAttrs: gadgetAttributes{ "MaxPower": "250", // in unit of 2mA @@ -84,7 +80,7 @@ func (u *UsbGadget) isGadgetConfigItemEnabled(itemKey string) bool { func (u *UsbGadget) loadGadgetConfig() { if u.customConfig.isEmpty { - u.log.Trace("using default gadget config") + u.log.Trace().Msg("using default gadget config") return } @@ -137,20 +133,33 @@ func (u *UsbGadget) GetPath(itemKey string) (string, error) { return joinPath(u.kvmGadgetPath, item.path), nil } -func mountConfigFS() error { - _, err := os.Stat(gadgetPath) - // TODO: check if it's mounted properly - if err == nil { - return nil +// OverrideGadgetConfig overrides the gadget config for the given item and attribute. +// It returns an error if the item is not found or the attribute is not found. +// It returns true if the attribute is overridden, false otherwise. +func (u *UsbGadget) OverrideGadgetConfig(itemKey string, itemAttr string, value string) (error, bool) { + u.configLock.Lock() + defer u.configLock.Unlock() + + // get it as a pointer + _, ok := u.configMap[itemKey] + if !ok { + return fmt.Errorf("config item %s not found", itemKey), false } - if os.IsNotExist(err) { - err = exec.Command("mount", "-t", "configfs", "none", configFSPath).Run() - if err != nil { - return fmt.Errorf("failed to mount configfs: %w", err) - } - } else { - return fmt.Errorf("unable to access usb gadget path: %w", err) + if u.configMap[itemKey].attrs[itemAttr] == value { + return nil, false + } + + u.configMap[itemKey].attrs[itemAttr] = value + u.log.Info().Str("itemKey", itemKey).Str("itemAttr", itemAttr).Str("value", value).Msg("overriding gadget config") + + return nil, true +} + +func mountConfigFS(path string) error { + err := exec.Command("mount", "-t", "configfs", "none", path).Run() + if err != nil { + return fmt.Errorf("failed to mount configfs: %w", err) } return nil } @@ -163,26 +172,14 @@ func (u *UsbGadget) Init() error { udcs := getUdcs() if len(udcs) < 1 { - u.log.Error("no udc found, skipping USB stack init") - return nil + return u.logWarn("no udc found, skipping USB stack init", nil) } u.udc = udcs[0] - _, err := os.Stat(u.kvmGadgetPath) - if err == nil { - u.log.Info("usb gadget already exists") - } - if err := mountConfigFS(); err != nil { - u.log.Errorf("failed to mount configfs: %v, usb stack might not function properly", err) - } - - if err := os.MkdirAll(u.configC1Path, 0755); err != nil { - u.log.Errorf("failed to create config path: %v", err) - } - - if err := u.writeGadgetConfig(); err != nil { - u.log.Errorf("failed to start gadget: %v", err) + err := u.configureUsbGadget(false) + if err != nil { + return u.logError("unable to initialize USB stack", err) } return nil @@ -194,143 +191,22 @@ func (u *UsbGadget) UpdateGadgetConfig() error { u.loadGadgetConfig() - if err := u.writeGadgetConfig(); err != nil { - u.log.Errorf("failed to update gadget: %v", err) + err := u.configureUsbGadget(true) + if err != nil { + return u.logError("unable to update gadget config", err) } return nil } -func (u *UsbGadget) getOrderedConfigItems() orderedGadgetConfigItems { - items := make([]gadgetConfigItemWithKey, 0) - for key, item := range u.configMap { - items = append(items, gadgetConfigItemWithKey{key, item}) - } - - sort.Slice(items, func(i, j int) bool { - return items[i].item.order < items[j].item.order +func (u *UsbGadget) configureUsbGadget(resetUsb bool) error { + return u.WithTransaction(func() error { + u.tx.MountConfigFS() + u.tx.CreateConfigPath() + u.tx.WriteGadgetConfig() + if resetUsb { + u.tx.RebindUsb(true) + } + return nil }) - - return items -} - -func (u *UsbGadget) writeGadgetConfig() error { - // create kvm gadget path - err := os.MkdirAll(u.kvmGadgetPath, 0755) - if err != nil { - return err - } - - u.log.Tracef("writing gadget config") - for _, val := range u.getOrderedConfigItems() { - key := val.key - item := val.item - - // check if the item is enabled in the config - if !u.isGadgetConfigItemEnabled(key) { - u.log.Tracef("disabling gadget config: %s", key) - err = u.disableGadgetItemConfig(item) - if err != nil { - return err - } - continue - } - u.log.Tracef("writing gadget config: %s", key) - err = u.writeGadgetItemConfig(item) - if err != nil { - return err - } - } - - if err = u.writeUDC(); err != nil { - u.log.Errorf("failed to write UDC: %v", err) - return err - } - - if err = u.rebindUsb(true); err != nil { - u.log.Infof("failed to rebind usb: %v", err) - } - - return nil -} - -func (u *UsbGadget) disableGadgetItemConfig(item gadgetConfigItem) error { - // remove symlink if exists - if item.configPath == nil { - return nil - } - - configPath := joinPath(u.configC1Path, item.configPath) - - if _, err := os.Lstat(configPath); os.IsNotExist(err) { - u.log.Tracef("symlink %s does not exist", item.configPath) - return nil - } - - if err := os.Remove(configPath); err != nil { - return fmt.Errorf("failed to remove symlink %s: %w", item.configPath, err) - } - - return nil -} - -func (u *UsbGadget) writeGadgetItemConfig(item gadgetConfigItem) error { - // create directory for the item - gadgetItemPath := joinPath(u.kvmGadgetPath, item.path) - err := os.MkdirAll(gadgetItemPath, 0755) - if err != nil { - return fmt.Errorf("failed to create path %s: %w", gadgetItemPath, err) - } - - if len(item.attrs) > 0 { - // write attributes for the item - err = u.writeGadgetAttrs(gadgetItemPath, item.attrs) - if err != nil { - return fmt.Errorf("failed to write attributes for %s: %w", gadgetItemPath, err) - } - } - - // write report descriptor if available - if item.reportDesc != nil { - err = u.writeIfDifferent(path.Join(gadgetItemPath, "report_desc"), item.reportDesc, 0644) - if err != nil { - return err - } - } - - // create config directory if configAttrs are set - if len(item.configAttrs) > 0 { - configItemPath := joinPath(u.configC1Path, item.configPath) - err = os.MkdirAll(configItemPath, 0755) - if err != nil { - return fmt.Errorf("failed to create path %s: %w", configItemPath, err) - } - - err = u.writeGadgetAttrs(configItemPath, item.configAttrs) - if err != nil { - return fmt.Errorf("failed to write config attributes for %s: %w", configItemPath, err) - } - } - - // create symlink if configPath is set - if item.configPath != nil && item.configAttrs == nil { - configPath := joinPath(u.configC1Path, item.configPath) - u.log.Tracef("Creating symlink from %s to %s", configPath, gadgetItemPath) - if err := ensureSymlink(configPath, gadgetItemPath); err != nil { - return err - } - } - - return nil -} - -func (u *UsbGadget) writeGadgetAttrs(basePath string, attrs gadgetAttributes) error { - for key, val := range attrs { - filePath := filepath.Join(basePath, key) - err := u.writeIfDifferent(filePath, []byte(val), 0644) - if err != nil { - return fmt.Errorf("failed to write to %s: %w", filePath, err) - } - } - return nil } diff --git a/internal/usbgadget/config_tx.go b/internal/usbgadget/config_tx.go new file mode 100644 index 0000000..df8a3d1 --- /dev/null +++ b/internal/usbgadget/config_tx.go @@ -0,0 +1,349 @@ +package usbgadget + +import ( + "fmt" + "path" + "path/filepath" + "sort" + + "github.com/rs/zerolog" +) + +// no os package should occur in this file + +type UsbGadgetTransaction struct { + c *ChangeSet + + // below are the fields that are needed to be set by the caller + log *zerolog.Logger + udc string + dwc3Path string + kvmGadgetPath string + configC1Path string + orderedConfigItems orderedGadgetConfigItems + isGadgetConfigItemEnabled func(key string) bool + + reorderSymlinkChanges *RequestedFileChange +} + +func (u *UsbGadget) newUsbGadgetTransaction(lock bool) error { + if lock { + u.txLock.Lock() + defer u.txLock.Unlock() + } + + if u.tx != nil { + return fmt.Errorf("transaction already exists") + } + + tx := &UsbGadgetTransaction{ + c: &ChangeSet{}, + log: u.log, + udc: u.udc, + dwc3Path: dwc3Path, + kvmGadgetPath: u.kvmGadgetPath, + configC1Path: u.configC1Path, + orderedConfigItems: u.getOrderedConfigItems(), + isGadgetConfigItemEnabled: u.isGadgetConfigItemEnabled, + } + u.tx = tx + + return nil +} + +func (u *UsbGadget) WithTransaction(fn func() error) error { + u.txLock.Lock() + defer u.txLock.Unlock() + + err := u.newUsbGadgetTransaction(false) + if err != nil { + u.log.Error().Err(err).Msg("failed to create transaction") + return err + } + if err := fn(); err != nil { + u.log.Error().Err(err).Msg("transaction failed") + return err + } + result := u.tx.Commit() + u.tx = nil + + return result +} + +func (tx *UsbGadgetTransaction) addFileChange(component string, change RequestedFileChange) string { + change.Component = component + tx.c.AddFileChangeStruct(change) + + key := change.Key + if key == "" { + key = change.Path + } + return key +} + +func (tx *UsbGadgetTransaction) mkdirAll(component string, path string, description string, deps []string) string { + return tx.addFileChange(component, RequestedFileChange{ + Path: path, + ExpectedState: FileStateDirectory, + Description: description, + DependsOn: deps, + }) +} + +func (tx *UsbGadgetTransaction) removeFile(component string, path string, description string) string { + return tx.addFileChange(component, RequestedFileChange{ + Path: path, + ExpectedState: FileStateAbsent, + Description: description, + }) +} + +func (tx *UsbGadgetTransaction) Commit() error { + tx.addFileChange("gadget-finalize", *tx.reorderSymlinkChanges) + + err := tx.c.Apply() + if err != nil { + tx.log.Error().Err(err).Msg("failed to update usbgadget configuration") + return err + } + tx.log.Info().Msg("usbgadget configuration updated") + return nil +} + +func (u *UsbGadget) getOrderedConfigItems() orderedGadgetConfigItems { + items := make([]gadgetConfigItemWithKey, 0) + for key, item := range u.configMap { + items = append(items, gadgetConfigItemWithKey{key, item}) + } + + sort.Slice(items, func(i, j int) bool { + return items[i].item.order < items[j].item.order + }) + + return items +} + +func (tx *UsbGadgetTransaction) MountConfigFS() { + tx.addFileChange("gadget", RequestedFileChange{ + Path: configFSPath, + ExpectedState: FileStateMountedConfigFS, + Description: "mount configfs", + }) +} + +func (tx *UsbGadgetTransaction) CreateConfigPath() { + tx.mkdirAll( + "gadget", + tx.configC1Path, + "create config path", + []string{configFSPath}, + ) +} + +func (tx *UsbGadgetTransaction) WriteGadgetConfig() { + // create kvm gadget path + tx.mkdirAll( + "gadget", + tx.kvmGadgetPath, + "create kvm gadget path", + []string{tx.configC1Path}, + ) + + deps := make([]string, 0) + deps = append(deps, tx.kvmGadgetPath) + + for _, val := range tx.orderedConfigItems { + key := val.key + item := val.item + + // check if the item is enabled in the config + if !tx.isGadgetConfigItemEnabled(key) { + tx.DisableGadgetItemConfig(item) + continue + } + deps = tx.writeGadgetItemConfig(item, deps) + } + + tx.WriteUDC() +} + +func (tx *UsbGadgetTransaction) getDisableKeys() []string { + disableKeys := make([]string, 0) + for _, item := range tx.orderedConfigItems { + if !tx.isGadgetConfigItemEnabled(item.key) { + continue + } + if item.item.configPath == nil || item.item.configAttrs != nil { + continue + } + + disableKeys = append(disableKeys, fmt.Sprintf("disable-%s", item.item.device)) + } + return disableKeys +} + +func (tx *UsbGadgetTransaction) DisableGadgetItemConfig(item gadgetConfigItem) { + // remove symlink if exists + if item.configPath == nil { + return + } + + configPath := joinPath(tx.configC1Path, item.configPath) + _ = tx.removeFile("gadget", configPath, "remove symlink: disable gadget config") +} + +func (tx *UsbGadgetTransaction) writeGadgetItemConfig(item gadgetConfigItem, deps []string) []string { + component := item.device + + // create directory for the item + files := make([]string, 0) + files = append(files, deps...) + + gadgetItemPath := joinPath(tx.kvmGadgetPath, item.path) + if gadgetItemPath != tx.kvmGadgetPath { + gadgetItemDir := tx.mkdirAll(component, gadgetItemPath, "create gadget item directory", files) + files = append(files, gadgetItemDir) + } + + beforeChange := make([]string, 0) + disableGadgetItemKey := fmt.Sprintf("disable-%s", item.device) + if item.configPath != nil && item.configAttrs == nil { + beforeChange = append(beforeChange, tx.getDisableKeys()...) + } + + if len(item.attrs) > 0 { + // write attributes for the item + files = append(files, tx.writeGadgetAttrs( + gadgetItemPath, + item.attrs, + component, + beforeChange, + )...) + } + + // write report descriptor if available + reportDescPath := path.Join(gadgetItemPath, "report_desc") + if item.reportDesc != nil { + tx.addFileChange(component, RequestedFileChange{ + Path: reportDescPath, + ExpectedState: FileStateFileContentMatch, + ExpectedContent: item.reportDesc, + Description: "write report descriptor", + BeforeChange: beforeChange, + DependsOn: files, + }) + } else { + tx.addFileChange(component, RequestedFileChange{ + Path: reportDescPath, + ExpectedState: FileStateAbsent, + Description: "remove report descriptor", + BeforeChange: beforeChange, + DependsOn: files, + }) + } + files = append(files, reportDescPath) + + // create config directory if configAttrs are set + if len(item.configAttrs) > 0 { + configItemPath := joinPath(tx.configC1Path, item.configPath) + if configItemPath != tx.configC1Path { + configItemDir := tx.mkdirAll(component, configItemPath, "create config item directory", files) + files = append(files, configItemDir) + } + files = append(files, tx.writeGadgetAttrs( + configItemPath, + item.configAttrs, + component, + beforeChange, + )...) + } + + // create symlink if configPath is set + if item.configPath != nil && item.configAttrs == nil { + configPath := joinPath(tx.configC1Path, item.configPath) + + // the change will be only applied by `beforeChange` + tx.addFileChange(component, RequestedFileChange{ + Key: disableGadgetItemKey, + Path: configPath, + ExpectedState: FileStateAbsent, + When: "beforeChange", // TODO: make it more flexible + Description: "remove symlink", + }) + + tx.addReorderSymlinkChange(configPath, gadgetItemPath, files) + } + + return files +} + +func (tx *UsbGadgetTransaction) writeGadgetAttrs(basePath string, attrs gadgetAttributes, component string, beforeChange []string) (files []string) { + files = make([]string, 0) + for key, val := range attrs { + filePath := filepath.Join(basePath, key) + tx.addFileChange(component, RequestedFileChange{ + Path: filePath, + ExpectedState: FileStateFileContentMatch, + ExpectedContent: []byte(val), + Description: "write gadget attribute", + DependsOn: []string{basePath}, + BeforeChange: beforeChange, + }) + files = append(files, filePath) + } + return files +} + +func (tx *UsbGadgetTransaction) addReorderSymlinkChange(path string, target string, deps []string) { + tx.log.Trace().Str("path", path).Str("target", target).Msg("add reorder symlink change") + + if tx.reorderSymlinkChanges == nil { + tx.reorderSymlinkChanges = &RequestedFileChange{ + Component: "gadget-finalize", + Key: "reorder-symlinks", + Path: tx.configC1Path, + ExpectedState: FileStateSymlinkInOrderConfigFS, + Description: "order symlinks", + ParamSymlinks: []symlink{}, + } + } + + tx.reorderSymlinkChanges.DependsOn = append(tx.reorderSymlinkChanges.DependsOn, deps...) + tx.reorderSymlinkChanges.ParamSymlinks = append(tx.reorderSymlinkChanges.ParamSymlinks, symlink{ + Path: path, + Target: target, + }) +} + +func (tx *UsbGadgetTransaction) WriteUDC() { + // bound the gadget to a UDC (USB Device Controller) + path := path.Join(tx.kvmGadgetPath, "UDC") + tx.addFileChange("udc", RequestedFileChange{ + Key: "udc", + Path: path, + ExpectedState: FileStateFileContentMatch, + ExpectedContent: []byte(tx.udc), + DependsOn: []string{"reorder-symlinks"}, + Description: "write UDC", + }) +} + +func (tx *UsbGadgetTransaction) RebindUsb(ignoreUnbindError bool) { + // remove the gadget from the UDC + tx.addFileChange("udc", RequestedFileChange{ + Path: path.Join(tx.dwc3Path, "unbind"), + ExpectedState: FileStateFileWrite, + ExpectedContent: []byte(tx.udc), + Description: "unbind UDC", + DependsOn: []string{"udc"}, + IgnoreErrors: ignoreUnbindError, + }) + // bind the gadget to the UDC + tx.addFileChange("udc", RequestedFileChange{ + Path: path.Join(tx.dwc3Path, "bind"), + ExpectedState: FileStateFileWrite, + ExpectedContent: []byte(tx.udc), + Description: "bind UDC", + DependsOn: []string{path.Join(tx.dwc3Path, "unbind")}, + }) +} diff --git a/internal/usbgadget/hid_keyboard.go b/internal/usbgadget/hid_keyboard.go index 030f7af..6ad3b6a 100644 --- a/internal/usbgadget/hid_keyboard.go +++ b/internal/usbgadget/hid_keyboard.go @@ -1,8 +1,11 @@ package usbgadget import ( + "context" "fmt" "os" + "reflect" + "time" ) var keyboardConfig = gadgetConfigItem{ @@ -11,9 +14,10 @@ var keyboardConfig = gadgetConfigItem{ path: []string{"functions", "hid.usb0"}, configPath: []string{"hid.usb0"}, attrs: gadgetAttributes{ - "protocol": "1", - "subclass": "1", - "report_length": "8", + "protocol": "1", + "subclass": "1", + "report_length": "8", + "no_out_endpoint": "0", }, reportDesc: keyboardReportDesc, } @@ -36,6 +40,7 @@ var keyboardReportDesc = []byte{ 0x81, 0x03, /* INPUT (Cnst,Var,Abs) */ 0x95, 0x05, /* REPORT_COUNT (5) */ 0x75, 0x01, /* REPORT_SIZE (1) */ + 0x05, 0x08, /* USAGE_PAGE (LEDs) */ 0x19, 0x01, /* USAGE_MINIMUM (Num Lock) */ 0x29, 0x05, /* USAGE_MAXIMUM (Kana) */ @@ -54,23 +59,155 @@ var keyboardReportDesc = []byte{ 0xc0, /* END_COLLECTION */ } -func (u *UsbGadget) keyboardWriteHidFile(data []byte) error { - if u.keyboardHidFile == nil { - var err error - u.keyboardHidFile, err = os.OpenFile("/dev/hidg0", os.O_RDWR, 0666) - if err != nil { - return fmt.Errorf("failed to open hidg0: %w", err) +const ( + hidReadBufferSize = 8 + // https://www.usb.org/sites/default/files/documents/hid1_11.pdf + // https://www.usb.org/sites/default/files/hut1_2.pdf + KeyboardLedMaskNumLock = 1 << 0 + KeyboardLedMaskCapsLock = 1 << 1 + KeyboardLedMaskScrollLock = 1 << 2 + KeyboardLedMaskCompose = 1 << 3 + KeyboardLedMaskKana = 1 << 4 + ValidKeyboardLedMasks = KeyboardLedMaskNumLock | KeyboardLedMaskCapsLock | KeyboardLedMaskScrollLock | KeyboardLedMaskCompose | KeyboardLedMaskKana +) + +// Synchronization between LED states and CAPS LOCK, NUM LOCK, SCROLL LOCK, +// COMPOSE, and KANA events is maintained by the host and NOT the keyboard. If +// using the keyboard descriptor in Appendix B, LED states are set by sending a +// 5-bit absolute report to the keyboard via a Set_Report(Output) request. +type KeyboardState struct { + NumLock bool `json:"num_lock"` + CapsLock bool `json:"caps_lock"` + ScrollLock bool `json:"scroll_lock"` + Compose bool `json:"compose"` + Kana bool `json:"kana"` +} + +func getKeyboardState(b byte) KeyboardState { + // should we check if it's the correct usage page? + return KeyboardState{ + NumLock: b&KeyboardLedMaskNumLock != 0, + CapsLock: b&KeyboardLedMaskCapsLock != 0, + ScrollLock: b&KeyboardLedMaskScrollLock != 0, + Compose: b&KeyboardLedMaskCompose != 0, + Kana: b&KeyboardLedMaskKana != 0, + } +} + +func (u *UsbGadget) updateKeyboardState(b byte) { + u.keyboardStateLock.Lock() + defer u.keyboardStateLock.Unlock() + + if b&^ValidKeyboardLedMasks != 0 { + u.log.Trace().Uint8("b", b).Msg("contains invalid bits, ignoring") + return + } + + newState := getKeyboardState(b) + if reflect.DeepEqual(u.keyboardState, newState) { + return + } + u.log.Info().Interface("old", u.keyboardState).Interface("new", newState).Msg("keyboardState updated") + u.keyboardState = newState + + if u.onKeyboardStateChange != nil { + (*u.onKeyboardStateChange)(newState) + } +} + +func (u *UsbGadget) SetOnKeyboardStateChange(f func(state KeyboardState)) { + u.onKeyboardStateChange = &f +} + +func (u *UsbGadget) GetKeyboardState() KeyboardState { + u.keyboardStateLock.Lock() + defer u.keyboardStateLock.Unlock() + + return u.keyboardState +} + +func (u *UsbGadget) listenKeyboardEvents() { + var path string + if u.keyboardHidFile != nil { + path = u.keyboardHidFile.Name() + } + l := u.log.With().Str("listener", "keyboardEvents").Str("path", path).Logger() + l.Trace().Msg("starting") + + go func() { + buf := make([]byte, hidReadBufferSize) + for { + select { + case <-u.keyboardStateCtx.Done(): + l.Info().Msg("context done") + return + default: + l.Trace().Msg("reading from keyboard") + if u.keyboardHidFile == nil { + u.logWithSuppression("keyboardHidFileNil", 100, &l, nil, "keyboardHidFile is nil") + // show the error every 100 times to avoid spamming the logs + time.Sleep(time.Second) + continue + } + // reset the counter + u.resetLogSuppressionCounter("keyboardHidFileNil") + + n, err := u.keyboardHidFile.Read(buf) + if err != nil { + u.logWithSuppression("keyboardHidFileRead", 100, &l, err, "failed to read") + continue + } + u.resetLogSuppressionCounter("keyboardHidFileRead") + + l.Trace().Int("n", n).Bytes("buf", buf).Msg("got data from keyboard") + if n != 1 { + l.Trace().Int("n", n).Msg("expected 1 byte, got") + continue + } + u.updateKeyboardState(buf[0]) + } } + }() +} + +func (u *UsbGadget) openKeyboardHidFile() error { + if u.keyboardHidFile != nil { + return nil + } + + var err error + u.keyboardHidFile, err = os.OpenFile("/dev/hidg0", os.O_RDWR, 0666) + if err != nil { + return fmt.Errorf("failed to open hidg0: %w", err) + } + + if u.keyboardStateCancel != nil { + u.keyboardStateCancel() + } + + u.keyboardStateCtx, u.keyboardStateCancel = context.WithCancel(context.Background()) + u.listenKeyboardEvents() + + return nil +} + +func (u *UsbGadget) OpenKeyboardHidFile() error { + return u.openKeyboardHidFile() +} + +func (u *UsbGadget) keyboardWriteHidFile(data []byte) error { + if err := u.openKeyboardHidFile(); err != nil { + return err } _, err := u.keyboardHidFile.Write(data) if err != nil { - u.log.Errorf("failed to write to hidg0: %w", err) + u.logWithSuppression("keyboardWriteHidFile", 100, u.log, err, "failed to write to hidg0") u.keyboardHidFile.Close() u.keyboardHidFile = nil return err } - + u.resetLogSuppressionCounter("keyboardWriteHidFile") return nil } diff --git a/internal/usbgadget/hid_mouse_absolute.go b/internal/usbgadget/hid_mouse_absolute.go index c59b591..2718f20 100644 --- a/internal/usbgadget/hid_mouse_absolute.go +++ b/internal/usbgadget/hid_mouse_absolute.go @@ -11,9 +11,10 @@ var absoluteMouseConfig = gadgetConfigItem{ path: []string{"functions", "hid.usb1"}, configPath: []string{"hid.usb1"}, attrs: gadgetAttributes{ - "protocol": "2", - "subclass": "1", - "report_length": "6", + "protocol": "2", + "subclass": "0", + "report_length": "6", + "no_out_endpoint": "1", }, reportDesc: absoluteMouseCombinedReportDesc, } @@ -55,6 +56,8 @@ var absoluteMouseCombinedReportDesc = []byte{ 0x09, 0x38, // Usage (Wheel) 0x15, 0x81, // Logical Minimum (-127) 0x25, 0x7F, // Logical Maximum (127) + 0x35, 0x00, // Physical Minimum (0) = Reset Physical Minimum + 0x45, 0x00, // Physical Maximum (0) = Reset Physical Maximum 0x75, 0x08, // Report Size (8) 0x95, 0x01, // Report Count (1) 0x81, 0x06, // Input (Data, Var, Rel) @@ -73,11 +76,12 @@ func (u *UsbGadget) absMouseWriteHidFile(data []byte) error { _, err := u.absMouseHidFile.Write(data) if err != nil { - u.log.Errorf("failed to write to hidg1: %w", err) + u.logWithSuppression("absMouseWriteHidFile", 100, u.log, err, "failed to write to hidg1") u.absMouseHidFile.Close() u.absMouseHidFile = nil return err } + u.resetLogSuppressionCounter("absMouseWriteHidFile") return nil } @@ -105,24 +109,16 @@ func (u *UsbGadget) AbsMouseWheelReport(wheelY int8) error { u.absMouseLock.Lock() defer u.absMouseLock.Unlock() - // Accumulate the wheelY value - u.absMouseAccumulatedWheelY += float64(wheelY) / 8.0 - - // Only send a report if the accumulated value is significant - if abs(u.absMouseAccumulatedWheelY) < 1.0 { + // Only send a report if the value is non-zero + if wheelY == 0 { return nil } - scaledWheelY := int8(u.absMouseAccumulatedWheelY) - err := u.absMouseWriteHidFile([]byte{ - 2, // Report ID 2 - byte(scaledWheelY), // Scaled Wheel Y (signed) + 2, // Report ID 2 + byte(wheelY), // Wheel Y (signed) }) - // Reset the accumulator, keeping any remainder - u.absMouseAccumulatedWheelY -= float64(scaledWheelY) - u.resetUserInputTime() return err } diff --git a/internal/usbgadget/hid_mouse_relative.go b/internal/usbgadget/hid_mouse_relative.go index df844dc..786f265 100644 --- a/internal/usbgadget/hid_mouse_relative.go +++ b/internal/usbgadget/hid_mouse_relative.go @@ -11,9 +11,10 @@ var relativeMouseConfig = gadgetConfigItem{ path: []string{"functions", "hid.usb2"}, configPath: []string{"hid.usb2"}, attrs: gadgetAttributes{ - "protocol": "2", - "subclass": "1", - "report_length": "4", + "protocol": "2", + "subclass": "1", + "report_length": "4", + "no_out_endpoint": "1", }, reportDesc: relativeMouseCombinedReportDesc, } @@ -65,11 +66,12 @@ func (u *UsbGadget) relMouseWriteHidFile(data []byte) error { _, err := u.relMouseHidFile.Write(data) if err != nil { - u.log.Errorf("failed to write to hidg2: %w", err) + u.logWithSuppression("relMouseWriteHidFile", 100, u.log, err, "failed to write to hidg2") u.relMouseHidFile.Close() u.relMouseHidFile = nil return err } + u.resetLogSuppressionCounter("relMouseWriteHidFile") return nil } diff --git a/internal/usbgadget/log.go b/internal/usbgadget/log.go new file mode 100644 index 0000000..f979f6c --- /dev/null +++ b/internal/usbgadget/log.go @@ -0,0 +1,27 @@ +package usbgadget + +import ( + "errors" +) + +func (u *UsbGadget) logWarn(msg string, err error) error { + if err == nil { + err = errors.New(msg) + } + if u.strictMode { + return err + } + u.log.Warn().Err(err).Msg(msg) + return nil +} + +func (u *UsbGadget) logError(msg string, err error) error { + if err == nil { + err = errors.New(msg) + } + if u.strictMode { + return err + } + u.log.Error().Err(err).Msg(msg) + return nil +} diff --git a/internal/usbgadget/mass_storage.go b/internal/usbgadget/mass_storage.go index f962cb4..41c1521 100644 --- a/internal/usbgadget/mass_storage.go +++ b/internal/usbgadget/mass_storage.go @@ -14,10 +14,13 @@ var massStorageLun0Config = gadgetConfigItem{ order: 3001, path: []string{"functions", "mass_storage.usb0", "lun.0"}, attrs: gadgetAttributes{ - "cdrom": "1", - "ro": "1", - "removable": "1", - "file": "\n", - "inquiry_string": "JetKVM Virtual Media", + "cdrom": "1", + "ro": "1", + "removable": "1", + "file": "\n", + // the additional whitespace is intentional to avoid the "JetKVM V irtual Media" string + // https://github.com/jetkvm/rv1106-system/blob/778133a1c153041e73f7de86c9c434a2753ea65d/sysdrv/source/uboot/u-boot/drivers/usb/gadget/f_mass_storage.c#L2556 + // Vendor (8 chars), product (16 chars) + "inquiry_string": "JetKVM Virtual Media", }, } diff --git a/internal/usbgadget/udc.go b/internal/usbgadget/udc.go index 6316b83..4b7fbe3 100644 --- a/internal/usbgadget/udc.go +++ b/internal/usbgadget/udc.go @@ -38,7 +38,7 @@ func rebindUsb(udc string, ignoreUnbindError bool) error { } func (u *UsbGadget) rebindUsb(ignoreUnbindError bool) error { - u.log.Infof("rebinding USB gadget to UDC %s", u.udc) + u.log.Info().Str("udc", u.udc).Msg("rebinding USB gadget to UDC") return rebindUsb(u.udc, ignoreUnbindError) } @@ -50,18 +50,6 @@ func (u *UsbGadget) RebindUsb(ignoreUnbindError bool) error { return u.rebindUsb(ignoreUnbindError) } -func (u *UsbGadget) writeUDC() error { - path := path.Join(u.kvmGadgetPath, "UDC") - - u.log.Tracef("writing UDC %s to %s", u.udc, path) - err := u.writeIfDifferent(path, []byte(u.udc), 0644) - if err != nil { - return fmt.Errorf("failed to write UDC: %w", err) - } - - return nil -} - // GetUsbState returns the current state of the USB gadget func (u *UsbGadget) GetUsbState() (state string) { stateFile := path.Join("/sys/class/udc", u.udc, "state") @@ -70,7 +58,7 @@ func (u *UsbGadget) GetUsbState() (state string) { if os.IsNotExist(err) { return "not attached" } else { - u.log.Tracef("failed to read usb state: %v", err) + u.log.Trace().Err(err).Msg("failed to read usb state") } return "unknown" } diff --git a/internal/usbgadget/usbgadget.go b/internal/usbgadget/usbgadget.go index 9fc34d5..cb70655 100644 --- a/internal/usbgadget/usbgadget.go +++ b/internal/usbgadget/usbgadget.go @@ -3,12 +3,14 @@ package usbgadget import ( + "context" "os" "path" "sync" "time" - "github.com/pion/logging" + "github.com/jetkvm/kvm/internal/logging" + "github.com/rs/zerolog" ) // Devices is a struct that represents the USB devices that can be enabled on a USB gadget. @@ -28,7 +30,8 @@ type Config struct { Manufacturer string `json:"manufacturer"` Product string `json:"product"` - isEmpty bool + strictMode bool // when it's enabled, all warnings will be converted to errors + isEmpty bool } var defaultUsbGadgetDevices = Devices{ @@ -57,24 +60,43 @@ type UsbGadget struct { relMouseHidFile *os.File relMouseLock sync.Mutex + keyboardState KeyboardState + keyboardStateLock sync.Mutex + keyboardStateCtx context.Context + keyboardStateCancel context.CancelFunc + enabledDevices Devices + strictMode bool // only intended for testing for now + absMouseAccumulatedWheelY float64 lastUserInput time.Time - log logging.LeveledLogger + tx *UsbGadgetTransaction + txLock sync.Mutex + + onKeyboardStateChange *func(state KeyboardState) + + log *zerolog.Logger + + logSuppressionCounter map[string]int + logSuppressionLock sync.Mutex } const configFSPath = "/sys/kernel/config" const gadgetPath = "/sys/kernel/config/usb_gadget" -var defaultLogger = logging.NewDefaultLoggerFactory().NewLogger("usbgadget") +var defaultLogger = logging.GetSubsystemLogger("usbgadget") // NewUsbGadget creates a new UsbGadget. -func NewUsbGadget(name string, enabledDevices *Devices, config *Config, logger *logging.LeveledLogger) *UsbGadget { +func NewUsbGadget(name string, enabledDevices *Devices, config *Config, logger *zerolog.Logger) *UsbGadget { + return newUsbGadget(name, defaultGadgetConfig, enabledDevices, config, logger) +} + +func newUsbGadget(name string, configMap map[string]gadgetConfigItem, enabledDevices *Devices, config *Config, logger *zerolog.Logger) *UsbGadget { if logger == nil { - logger = &defaultLogger + logger = defaultLogger } if enabledDevices == nil { @@ -85,24 +107,34 @@ func NewUsbGadget(name string, enabledDevices *Devices, config *Config, logger * config = &Config{isEmpty: true} } + keyboardCtx, keyboardCancel := context.WithCancel(context.Background()) + g := &UsbGadget{ - name: name, - kvmGadgetPath: path.Join(gadgetPath, name), - configC1Path: path.Join(gadgetPath, name, "configs/c.1"), - configMap: defaultGadgetConfig, - customConfig: *config, - configLock: sync.Mutex{}, - keyboardLock: sync.Mutex{}, - absMouseLock: sync.Mutex{}, - relMouseLock: sync.Mutex{}, - enabledDevices: *enabledDevices, - lastUserInput: time.Now(), - log: *logger, + name: name, + kvmGadgetPath: path.Join(gadgetPath, name), + configC1Path: path.Join(gadgetPath, name, "configs/c.1"), + configMap: configMap, + customConfig: *config, + configLock: sync.Mutex{}, + keyboardLock: sync.Mutex{}, + absMouseLock: sync.Mutex{}, + relMouseLock: sync.Mutex{}, + txLock: sync.Mutex{}, + keyboardStateCtx: keyboardCtx, + keyboardStateCancel: keyboardCancel, + keyboardState: KeyboardState{}, + enabledDevices: *enabledDevices, + lastUserInput: time.Now(), + log: logger, + + strictMode: config.strictMode, + + logSuppressionCounter: make(map[string]int), absMouseAccumulatedWheelY: 0, } if err := g.Init(); err != nil { - g.log.Errorf("failed to init USB gadget: %v", err) + logger.Error().Err(err).Msg("failed to init USB gadget") return nil } diff --git a/internal/usbgadget/utils.go b/internal/usbgadget/utils.go index 3f0adda..8654924 100644 --- a/internal/usbgadget/utils.go +++ b/internal/usbgadget/utils.go @@ -3,61 +3,110 @@ package usbgadget import ( "bytes" "fmt" - "os" "path/filepath" -) + "strconv" + "strings" -// Helper function to get absolute value of float64 -func abs(x float64) float64 { - if x < 0 { - return -x - } - return x -} + "github.com/rs/zerolog" +) func joinPath(basePath string, paths []string) string { pathArr := append([]string{basePath}, paths...) return filepath.Join(pathArr...) } -func ensureSymlink(linkPath string, target string) error { - if _, err := os.Lstat(linkPath); err == nil { - currentTarget, err := os.Readlink(linkPath) - if err != nil || currentTarget != target { - err = os.Remove(linkPath) - if err != nil { - return fmt.Errorf("failed to remove existing symlink %s: %w", linkPath, err) - } - } - } else if !os.IsNotExist(err) { - return fmt.Errorf("failed to check if symlink exists: %w", err) +func hexToDecimal(hex string) (int64, error) { + decimal, err := strconv.ParseInt(hex, 16, 64) + if err != nil { + return 0, err } - - if err := os.Symlink(target, linkPath); err != nil { - return fmt.Errorf("failed to create symlink from %s to %s: %w", linkPath, target, err) - } - - return nil + return decimal, nil } -func (u *UsbGadget) writeIfDifferent(filePath string, content []byte, permMode os.FileMode) error { - if _, err := os.Stat(filePath); err == nil { - oldContent, err := os.ReadFile(filePath) - if err == nil { - if bytes.Equal(oldContent, content) { - u.log.Tracef("skipping writing to %s as it already has the correct content", filePath) - return nil - } +func decimalToOctal(decimal int64) string { + return fmt.Sprintf("%04o", decimal) +} - if len(oldContent) == len(content)+1 && - bytes.Equal(oldContent[:len(content)], content) && - oldContent[len(content)] == 10 { - u.log.Tracef("skipping writing to %s as it already has the correct content", filePath) - return nil - } +func hexToOctal(hex string) (string, error) { + hex = strings.ToLower(hex) + hex = strings.Replace(hex, "0x", "", 1) //remove 0x or 0X - u.log.Tracef("writing to %s as it has different content old%v new%v", filePath, oldContent, content) + decimal, err := hexToDecimal(hex) + if err != nil { + return "", err + } + + // Convert the decimal integer to an octal string. + octal := decimalToOctal(decimal) + return octal, nil +} + +func compareFileContent(oldContent []byte, newContent []byte, looserMatch bool) bool { + if bytes.Equal(oldContent, newContent) { + return true + } + + if len(oldContent) == len(newContent)+1 && + bytes.Equal(oldContent[:len(newContent)], newContent) && + oldContent[len(newContent)] == 10 { + return true + } + + if len(newContent) == 4 { + if len(oldContent) < 6 || len(oldContent) > 7 { + return false + } + + if len(oldContent) == 7 && oldContent[6] == 0x0a { + oldContent = oldContent[:6] + } + + oldOctalValue, err := hexToOctal(string(oldContent)) + if err != nil { + return false + } + + if oldOctalValue == string(newContent) { + return true } } - return os.WriteFile(filePath, content, permMode) + + if looserMatch { + oldContentStr := strings.TrimSpace(string(oldContent)) + newContentStr := strings.TrimSpace(string(newContent)) + + return oldContentStr == newContentStr + } + + return false +} + +func (u *UsbGadget) logWithSuppression(counterName string, every int, logger *zerolog.Logger, err error, msg string, args ...interface{}) { + u.logSuppressionLock.Lock() + defer u.logSuppressionLock.Unlock() + + if _, ok := u.logSuppressionCounter[counterName]; !ok { + u.logSuppressionCounter[counterName] = 0 + } else { + u.logSuppressionCounter[counterName]++ + } + + l := logger.With().Int("counter", u.logSuppressionCounter[counterName]).Logger() + + if u.logSuppressionCounter[counterName]%every == 0 { + if err != nil { + l.Error().Err(err).Msgf(msg, args...) + } else { + l.Error().Msgf(msg, args...) + } + } +} + +func (u *UsbGadget) resetLogSuppressionCounter(counterName string) { + u.logSuppressionLock.Lock() + defer u.logSuppressionLock.Unlock() + + if _, ok := u.logSuppressionCounter[counterName]; !ok { + u.logSuppressionCounter[counterName] = 0 + } } diff --git a/internal/websecure/ed25519_test.go b/internal/websecure/ed25519_test.go new file mode 100644 index 0000000..0753be0 --- /dev/null +++ b/internal/websecure/ed25519_test.go @@ -0,0 +1,55 @@ +package websecure + +import ( + "os" + "testing" +) + +var ( + fixtureEd25519Certificate = `-----BEGIN CERTIFICATE----- +MIIBQDCB86ADAgECAhQdB4qB6dV0/u1lwhJofQgkmjjV1zAFBgMrZXAwLzELMAkG +A1UEBhMCREUxIDAeBgNVBAMMF2VkMjU1MTktdGVzdC5qZXRrdm0uY29tMB4XDTI1 +MDUyMzEyNTkyN1oXDTI3MDQyMzEyNTkyN1owLzELMAkGA1UEBhMCREUxIDAeBgNV +BAMMF2VkMjU1MTktdGVzdC5qZXRrdm0uY29tMCowBQYDK2VwAyEA9tLyoulJn7Ev +bf8kuD1ZGdA092773pCRjFEDKpXHonyjITAfMB0GA1UdDgQWBBRkmrVMfsLY57iy +r/0POP0S4QxCADAFBgMrZXADQQBfTRvqavLHDYQiKQTgbGod+Yn+fIq2lE584+1U +C4wh9peIJDFocLBEAYTQpEMKxa4s0AIRxD+a7aCS5oz0e/0I +-----END CERTIFICATE-----` + + fixtureEd25519PrivateKey = `-----BEGIN PRIVATE KEY----- +MC4CAQAwBQYDK2VwBCIEIKV08xUsLRHBfMXqZwxVRzIbViOp8G7aQGjPvoRFjujB +-----END PRIVATE KEY-----` + + certStore *CertStore + certSigner *SelfSigner +) + +func TestMain(m *testing.M) { + tlsStorePath, err := os.MkdirTemp("", "jktls.*") + if err != nil { + defaultLogger.Fatal().Err(err).Msg("failed to create temp directory") + } + + certStore = NewCertStore(tlsStorePath, nil) + certStore.LoadCertificates() + + certSigner = NewSelfSigner( + certStore, + nil, + "ci.jetkvm.com", + "JetKVM", + "JetKVM", + "JetKVM", + ) + + m.Run() + + os.RemoveAll(tlsStorePath) +} + +func TestSaveEd25519Certificate(t *testing.T) { + err, _ := certStore.ValidateAndSaveCertificate("ed25519-test.jetkvm.com", fixtureEd25519Certificate, fixtureEd25519PrivateKey, true) + if err != nil { + t.Fatalf("failed to save certificate: %v", err) + } +} diff --git a/internal/websecure/log.go b/internal/websecure/log.go new file mode 100644 index 0000000..f45767e --- /dev/null +++ b/internal/websecure/log.go @@ -0,0 +1,9 @@ +package websecure + +import ( + "os" + + "github.com/rs/zerolog" +) + +var defaultLogger = zerolog.New(os.Stdout).With().Str("component", "websecure").Logger() diff --git a/internal/websecure/selfsign.go b/internal/websecure/selfsign.go new file mode 100644 index 0000000..77efa37 --- /dev/null +++ b/internal/websecure/selfsign.go @@ -0,0 +1,191 @@ +package websecure + +import ( + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/tls" + "crypto/x509" + "crypto/x509/pkix" + "net" + "strings" + "time" + + "github.com/rs/zerolog" + "golang.org/x/net/idna" +) + +const selfSignerCAMagicName = "__ca__" + +type SelfSigner struct { + store *CertStore + log *zerolog.Logger + + caInfo pkix.Name + + DefaultDomain string + DefaultOrg string + DefaultOU string +} + +func NewSelfSigner( + store *CertStore, + log *zerolog.Logger, + defaultDomain, + defaultOrg, + defaultOU, + caName string, +) *SelfSigner { + return &SelfSigner{ + store: store, + log: log, + DefaultDomain: defaultDomain, + DefaultOrg: defaultOrg, + DefaultOU: defaultOU, + caInfo: pkix.Name{ + CommonName: caName, + Organization: []string{defaultOrg}, + OrganizationalUnit: []string{defaultOU}, + }, + } +} + +func (s *SelfSigner) getCA() *tls.Certificate { + return s.createSelfSignedCert(selfSignerCAMagicName) +} + +func (s *SelfSigner) createSelfSignedCert(hostname string) *tls.Certificate { + if tlsCert := s.store.certificates[hostname]; tlsCert != nil { + return tlsCert + } + + // check if hostname is the CA magic name + var ca *tls.Certificate + if hostname != selfSignerCAMagicName { + ca = s.getCA() + if ca == nil { + s.log.Error().Msg("Failed to get CA certificate") + return nil + } + } + + s.log.Info().Str("hostname", hostname).Msg("Creating self-signed certificate") + + // lock the store while creating the certificate (do not move upwards) + s.store.certLock.Lock() + defer s.store.certLock.Unlock() + + priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + s.log.Error().Err(err).Msg("Failed to generate private key") + return nil + } + + notBefore := time.Now() + notAfter := notBefore.AddDate(1, 0, 0) + + serialNumber, err := generateSerialNumber() + if err != nil { + s.log.Error().Err(err).Msg("Failed to generate serial number") + return nil + } + + dnsName := hostname + ip := net.ParseIP(hostname) + if ip != nil { + dnsName = s.DefaultDomain + } + + // set up CSR + isCA := hostname == selfSignerCAMagicName + subject := pkix.Name{ + CommonName: hostname, + Organization: []string{s.DefaultOrg}, + OrganizationalUnit: []string{s.DefaultOU}, + } + keyUsage := x509.KeyUsageDigitalSignature + extKeyUsage := []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth} + + // check if hostname is the CA magic name, and if so, set the subject to the CA info + if isCA { + subject = s.caInfo + keyUsage |= x509.KeyUsageCertSign + extKeyUsage = append(extKeyUsage, x509.ExtKeyUsageClientAuth) + notAfter = notBefore.AddDate(10, 0, 0) + } + + cert := x509.Certificate{ + SerialNumber: serialNumber, + Subject: subject, + NotBefore: notBefore, + NotAfter: notAfter, + IsCA: isCA, + KeyUsage: keyUsage, + ExtKeyUsage: extKeyUsage, + BasicConstraintsValid: true, + } + + // set up DNS names and IP addresses + if !isCA { + cert.DNSNames = []string{dnsName} + if ip != nil { + cert.IPAddresses = []net.IP{ip} + } + } + + // set up parent certificate + parent := &cert + parentPriv := priv + if ca != nil { + parent, err = x509.ParseCertificate(ca.Certificate[0]) + if err != nil { + s.log.Error().Err(err).Msg("Failed to parse parent certificate") + return nil + } + parentPriv = ca.PrivateKey.(*ecdsa.PrivateKey) + } + + certBytes, err := x509.CreateCertificate(rand.Reader, &cert, parent, &priv.PublicKey, parentPriv) + if err != nil { + s.log.Error().Err(err).Msg("Failed to create certificate") + return nil + } + + tlsCert := &tls.Certificate{ + Certificate: [][]byte{certBytes}, + PrivateKey: priv, + } + if ca != nil { + tlsCert.Certificate = append(tlsCert.Certificate, ca.Certificate...) + } + + s.store.certificates[hostname] = tlsCert + s.store.saveCertificate(hostname) + + return tlsCert +} + +// GetCertificate returns the certificate for the given hostname +// returns nil if the certificate is not found +func (s *SelfSigner) GetCertificate(info *tls.ClientHelloInfo) (*tls.Certificate, error) { + var hostname string + if info.ServerName != "" && info.ServerName != selfSignerCAMagicName { + hostname = info.ServerName + } else { + hostname = strings.Split(info.Conn.LocalAddr().String(), ":")[0] + } + + s.log.Info().Str("hostname", hostname).Strs("supported_protos", info.SupportedProtos).Msg("TLS handshake") + + // convert hostname to punycode + h, err := idna.Lookup.ToASCII(hostname) + if err != nil { + s.log.Warn().Str("hostname", hostname).Err(err).Str("remote_addr", info.Conn.RemoteAddr().String()).Msg("Hostname is not valid") + hostname = s.DefaultDomain + } else { + hostname = h + } + + cert := s.createSelfSignedCert(hostname) + return cert, nil +} diff --git a/internal/websecure/store.go b/internal/websecure/store.go new file mode 100644 index 0000000..ea7911c --- /dev/null +++ b/internal/websecure/store.go @@ -0,0 +1,179 @@ +package websecure + +import ( + "crypto/tls" + "fmt" + "os" + "path" + "strings" + "sync" + + "github.com/rs/zerolog" +) + +type CertStore struct { + certificates map[string]*tls.Certificate + certLock *sync.Mutex + + storePath string + + log *zerolog.Logger +} + +func NewCertStore(storePath string, log *zerolog.Logger) *CertStore { + if log == nil { + log = &defaultLogger + } + + return &CertStore{ + certificates: make(map[string]*tls.Certificate), + certLock: &sync.Mutex{}, + + storePath: storePath, + log: log, + } +} + +func (s *CertStore) ensureStorePath() error { + // check if directory exists + stat, err := os.Stat(s.storePath) + if err == nil { + if stat.IsDir() { + return nil + } + + return fmt.Errorf("TLS store path exists but is not a directory: %s", s.storePath) + } + + if os.IsNotExist(err) { + s.log.Trace().Str("path", s.storePath).Msg("TLS store directory does not exist, creating directory") + err = os.MkdirAll(s.storePath, 0755) + if err != nil { + return fmt.Errorf("failed to create TLS store path: %w", err) + } + return nil + } + + return fmt.Errorf("failed to check TLS store path: %w", err) +} + +func (s *CertStore) LoadCertificates() { + err := s.ensureStorePath() + if err != nil { + s.log.Error().Err(err).Msg("Failed to ensure store path") + return + } + + files, err := os.ReadDir(s.storePath) + if err != nil { + s.log.Error().Err(err).Msg("Failed to read TLS directory") + return + } + + for _, file := range files { + if file.IsDir() { + continue + } + + if strings.HasSuffix(file.Name(), ".crt") { + s.loadCertificate(strings.TrimSuffix(file.Name(), ".crt")) + } + } +} + +func (s *CertStore) loadCertificate(hostname string) { + s.certLock.Lock() + defer s.certLock.Unlock() + + keyFile := path.Join(s.storePath, hostname+".key") + crtFile := path.Join(s.storePath, hostname+".crt") + + cert, err := tls.LoadX509KeyPair(crtFile, keyFile) + if err != nil { + s.log.Error().Err(err).Str("hostname", hostname).Msg("Failed to load certificate") + return + } + + s.certificates[hostname] = &cert + + if hostname == selfSignerCAMagicName { + s.log.Info().Msg("loaded CA certificate") + } else { + s.log.Info().Str("hostname", hostname).Msg("loaded certificate") + } +} + +// GetCertificate returns the certificate for the given hostname +// returns nil if the certificate is not found +func (s *CertStore) GetCertificate(hostname string) *tls.Certificate { + s.certLock.Lock() + defer s.certLock.Unlock() + + return s.certificates[hostname] +} + +// ValidateAndSaveCertificate validates the certificate and saves it to the store +// returns are: +// - error: if the certificate is invalid or if there's any error during saving the certificate +// - error: if there's any warning or error during saving the certificate +func (s *CertStore) ValidateAndSaveCertificate(hostname string, cert string, key string, ignoreWarning bool) (error, error) { + tlsCert, err := tls.X509KeyPair([]byte(cert), []byte(key)) + if err != nil { + return fmt.Errorf("failed to parse certificate: %w", err), nil + } + + // this can be skipped as current implementation supports one custom certificate only + if tlsCert.Leaf != nil { + // add recover to avoid panic + defer func() { + if r := recover(); r != nil { + s.log.Error().Interface("recovered", r).Msg("Failed to verify hostname") + } + }() + + if err = tlsCert.Leaf.VerifyHostname(hostname); err != nil { + if !ignoreWarning { + return nil, fmt.Errorf("certificate does not match hostname: %w", err) + } + s.log.Warn().Err(err).Msg("certificate does not match hostname") + } + } + + s.certLock.Lock() + s.certificates[hostname] = &tlsCert + s.certLock.Unlock() + + s.saveCertificate(hostname) + + return nil, nil +} + +func (s *CertStore) saveCertificate(hostname string) { + // check if certificate already exists + tlsCert := s.certificates[hostname] + if tlsCert == nil { + s.log.Error().Str("hostname", hostname).Msg("Certificate for hostname does not exist, skipping saving certificate") + return + } + + err := s.ensureStorePath() + if err != nil { + s.log.Error().Err(err).Msg("Failed to ensure store path") + return + } + + keyFile := path.Join(s.storePath, hostname+".key") + crtFile := path.Join(s.storePath, hostname+".crt") + + if err := keyToFile(tlsCert, keyFile); err != nil { + s.log.Error().Err(err).Msg("Failed to save key file") + return + } + + if err := certToFile(tlsCert, crtFile); err != nil { + s.log.Error().Err(err).Msg("Failed to save certificate") + return + } + + s.log.Info().Str("hostname", hostname).Msg("Saved certificate") +} diff --git a/internal/websecure/utils.go b/internal/websecure/utils.go new file mode 100644 index 0000000..b333bf9 --- /dev/null +++ b/internal/websecure/utils.go @@ -0,0 +1,85 @@ +package websecure + +import ( + "crypto/ecdsa" + "crypto/ed25519" + "crypto/rand" + "crypto/rsa" + "crypto/tls" + "crypto/x509" + "encoding/pem" + "fmt" + "math/big" + "os" +) + +var serialNumberLimit = new(big.Int).Lsh(big.NewInt(1), 4096) + +func withSecretFile(filename string, f func(*os.File) error) error { + file, err := os.OpenFile(filename, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0600) + if err != nil { + return err + } + defer file.Close() + + return f(file) +} + +func keyToFile(cert *tls.Certificate, filename string) error { + var keyBlock pem.Block + switch k := cert.PrivateKey.(type) { + case *rsa.PrivateKey: + keyBlock = pem.Block{ + Type: "RSA PRIVATE KEY", + Bytes: x509.MarshalPKCS1PrivateKey(k), + } + case *ecdsa.PrivateKey: + b, e := x509.MarshalECPrivateKey(k) + if e != nil { + return fmt.Errorf("failed to marshal EC private key: %v", e) + } + keyBlock = pem.Block{ + Type: "EC PRIVATE KEY", + Bytes: b, + } + case ed25519.PrivateKey: + keyBlock = pem.Block{ + Type: "ED25519 PRIVATE KEY", + Bytes: k, + } + default: + return fmt.Errorf("unknown private key type: %T", k) + } + + err := withSecretFile(filename, func(file *os.File) error { + return pem.Encode(file, &keyBlock) + }) + + if err != nil { + return fmt.Errorf("failed to save private key: %w", err) + } + + return nil +} + +func certToFile(cert *tls.Certificate, filename string) error { + return withSecretFile(filename, func(file *os.File) error { + for _, c := range cert.Certificate { + block := pem.Block{ + Type: "CERTIFICATE", + Bytes: c, + } + + err := pem.Encode(file, &block) + if err != nil { + return fmt.Errorf("failed to save certificate: %w", err) + } + } + + return nil + }) +} + +func generateSerialNumber() (*big.Int, error) { + return rand.Int(rand.Reader, serialNumberLimit) +} diff --git a/jiggler.go b/jiggler.go index 783c276..8b85876 100644 --- a/jiggler.go +++ b/jiggler.go @@ -2,9 +2,10 @@ package kvm import ( "fmt" - "github.com/go-co-op/gocron/v2" "math/rand" "time" + + "github.com/go-co-op/gocron/v2" ) type JigglerConfig struct { @@ -29,7 +30,7 @@ func rpcGetJigglerConfig() (JigglerConfig, error) { } func rpcSetJigglerConfig(jigglerConfig JigglerConfig) error { - logger.Infof("jigglerConfig: %v, %v, %v", jigglerConfig.InactivityLimitSeconds, jigglerConfig.JitterPercentage, jigglerConfig.ScheduleCronTab) + logger.Info().Msgf("jigglerConfig: %v, %v, %v", jigglerConfig.InactivityLimitSeconds, jigglerConfig.JitterPercentage, jigglerConfig.ScheduleCronTab) config.JigglerConfig = &jigglerConfig err := removeExistingCrobJobs(scheduler) if err != nil { @@ -60,7 +61,7 @@ func init() { ensureConfigLoaded() err := runJigglerCronTab() if err != nil { - logger.Errorf("Error scheduling jiggler crontab: %v", err) + logger.Error().Msgf("Error scheduling jiggler crontab: %v", err) return } } @@ -89,7 +90,7 @@ func runJigglerCronTab() error { s.Start() delta, err := calculateJobDelta(s) jobDelta = delta - logger.Infof("Time between jiggler runs: %v", jobDelta) + logger.Info().Msgf("Time between jiggler runs: %v", jobDelta) if err != nil { return err } @@ -104,17 +105,17 @@ func runJiggler() { } inactivitySeconds := config.JigglerConfig.InactivityLimitSeconds timeSinceLastInput := time.Since(gadget.GetLastUserInputTime()) - logger.Debugf("Time since last user input %v", timeSinceLastInput) + logger.Debug().Msgf("Time since last user input %v", timeSinceLastInput) if timeSinceLastInput > time.Duration(inactivitySeconds)*time.Second { - logger.Debug("Jiggling mouse...") + logger.Debug().Msg("Jiggling mouse...") //TODO: change to rel mouse err := rpcAbsMouseReport(1, 1, 0) if err != nil { - logger.Warnf("Failed to jiggle mouse: %v", err) + logger.Warn().Msgf("Failed to jiggle mouse: %v", err) } err = rpcAbsMouseReport(0, 0, 0) if err != nil { - logger.Warnf("Failed to reset mouse position: %v", err) + logger.Warn().Msgf("Failed to reset mouse position: %v", err) } } } diff --git a/jsonrpc.go b/jsonrpc.go index c9663ec..a0264b8 100644 --- a/jsonrpc.go +++ b/jsonrpc.go @@ -38,6 +38,10 @@ type JSONRPCEvent struct { Params interface{} `json:"params,omitempty"` } +type DisplayRotationSettings struct { + Rotation string `json:"rotation"` +} + type BacklightSettings struct { MaxBrightness int `json:"max_brightness"` DimAfter int `json:"dim_after"` @@ -47,12 +51,12 @@ type BacklightSettings struct { func writeJSONRPCResponse(response JSONRPCResponse, session *Session) { responseBytes, err := json.Marshal(response) if err != nil { - logger.Warnf("Error marshalling JSONRPC response: %v", err) + jsonRpcLogger.Warn().Err(err).Msg("Error marshalling JSONRPC response") return } err = session.RPCChannel.SendText(string(responseBytes)) if err != nil { - logger.Warnf("Error sending JSONRPC response: %v", err) + jsonRpcLogger.Warn().Err(err).Msg("Error sending JSONRPC response") return } } @@ -65,16 +69,24 @@ func writeJSONRPCEvent(event string, params interface{}, session *Session) { } requestBytes, err := json.Marshal(request) if err != nil { - logger.Warnf("Error marshalling JSONRPC event: %v", err) + jsonRpcLogger.Warn().Err(err).Msg("Error marshalling JSONRPC event") return } if session == nil || session.RPCChannel == nil { - logger.Info("RPC channel not available") + jsonRpcLogger.Info().Msg("RPC channel not available") return } - err = session.RPCChannel.SendText(string(requestBytes)) + + requestString := string(requestBytes) + scopedLogger := jsonRpcLogger.With(). + Str("data", requestString). + Logger() + + scopedLogger.Info().Msg("sending JSONRPC event") + + err = session.RPCChannel.SendText(requestString) if err != nil { - logger.Warnf("Error sending JSONRPC event: %v", err) + scopedLogger.Warn().Err(err).Msg("error sending JSONRPC event") return } } @@ -83,6 +95,11 @@ func onRPCMessage(message webrtc.DataChannelMessage, session *Session) { var request JSONRPCRequest err := json.Unmarshal(message.Data, &request) if err != nil { + jsonRpcLogger.Warn(). + Str("data", string(message.Data)). + Err(err). + Msg("Error unmarshalling JSONRPC request") + errorResponse := JSONRPCResponse{ JSONRPC: "2.0", Error: map[string]interface{}{ @@ -95,7 +112,13 @@ func onRPCMessage(message webrtc.DataChannelMessage, session *Session) { return } - //logger.Infof("Received RPC request: Method=%s, Params=%v, ID=%d", request.Method, request.Params, request.ID) + scopedLogger := jsonRpcLogger.With(). + Str("method", request.Method). + Interface("params", request.Params). + Interface("id", request.ID).Logger() + + scopedLogger.Trace().Msg("Received RPC request") + handler, ok := rpcHandlers[request.Method] if !ok { errorResponse := JSONRPCResponse{ @@ -110,8 +133,10 @@ func onRPCMessage(message webrtc.DataChannelMessage, session *Session) { return } + scopedLogger.Trace().Msg("Calling RPC handler") result, err := callRPCHandler(handler, request.Params) if err != nil { + scopedLogger.Error().Err(err).Msg("Error calling RPC handler") errorResponse := JSONRPCResponse{ JSONRPC: "2.0", Error: map[string]interface{}{ @@ -125,6 +150,8 @@ func onRPCMessage(message webrtc.DataChannelMessage, session *Session) { return } + scopedLogger.Trace().Interface("result", result).Msg("RPC handler returned") + response := JSONRPCResponse{ JSONRPC: "2.0", Result: result, @@ -141,6 +168,30 @@ func rpcGetDeviceID() (string, error) { return GetDeviceID(), nil } +func rpcReboot(force bool) error { + logger.Info().Msg("Got reboot request from JSONRPC, rebooting...") + + args := []string{} + if force { + args = append(args, "-f") + } + + cmd := exec.Command("reboot", args...) + err := cmd.Start() + if err != nil { + logger.Error().Err(err).Msg("failed to reboot") + return fmt.Errorf("failed to reboot: %w", err) + } + + // If the reboot command is successful, exit the program after 5 seconds + go func() { + time.Sleep(5 * time.Second) + os.Exit(0) + }() + + return nil +} + var streamFactor = 1.0 func rpcGetStreamQualityFactor() (float64, error) { @@ -148,7 +199,7 @@ func rpcGetStreamQualityFactor() (float64, error) { } func rpcSetStreamQualityFactor(factor float64) error { - logger.Infof("Setting stream quality factor to: %f", factor) + logger.Info().Float64("factor", factor).Msg("Setting stream quality factor") var _, err = CallCtrlAction("set_video_quality_factor", map[string]interface{}{"quality_factor": factor}) if err != nil { return err @@ -184,10 +235,10 @@ func rpcGetEDID() (string, error) { func rpcSetEDID(edid string) error { if edid == "" { - logger.Info("Restoring EDID to default") + logger.Info().Msg("Restoring EDID to default") edid = "00ffffffffffff0052620188008888881c150103800000780a0dc9a05747982712484c00000001010101010101010101010101010101023a801871382d40582c4500c48e2100001e011d007251d01e206e285500c48e2100001e000000fc00543734392d6648443732300a20000000fd00147801ff1d000a202020202020017b" } else { - logger.Infof("Setting EDID to: %s", edid) + logger.Info().Str("edid", edid).Msg("Setting EDID") } _, err := CallCtrlAction("set_edid", map[string]interface{}{"edid": edid}) if err != nil { @@ -215,8 +266,13 @@ func rpcSetDevChannelState(enabled bool) error { func rpcGetUpdateStatus() (*UpdateStatus, error) { includePreRelease := config.IncludePreRelease updateStatus, err := GetUpdateStatus(context.Background(), GetDeviceID(), includePreRelease) + // to ensure backwards compatibility, + // if there's an error, we won't return an error, but we will set the error field if err != nil { - return nil, fmt.Errorf("error checking for updates: %w", err) + if updateStatus == nil { + return nil, fmt.Errorf("error checking for updates: %w", err) + } + updateStatus.Error = err.Error() } return updateStatus, nil @@ -227,12 +283,30 @@ func rpcTryUpdate() error { go func() { err := TryUpdate(context.Background(), GetDeviceID(), includePreRelease) if err != nil { - logger.Warnf("failed to try update: %v", err) + logger.Warn().Err(err).Msg("failed to try update") } }() return nil } +func rpcSetDisplayRotation(params DisplayRotationSettings) error { + var err error + _, err = lvDispSetRotation(params.Rotation) + if err == nil { + config.DisplayRotation = params.Rotation + if err := SaveConfig(); err != nil { + return fmt.Errorf("failed to save config: %w", err) + } + } + return err +} + +func rpcGetDisplayRotation() (*DisplayRotationSettings, error) { + return &DisplayRotationSettings{ + Rotation: config.DisplayRotation, + }, nil +} + func rpcSetBacklightSettings(params BacklightSettings) error { blConfig := params @@ -257,7 +331,7 @@ func rpcSetBacklightSettings(params BacklightSettings) error { return fmt.Errorf("failed to save config: %w", err) } - logger.Infof("rpc: display: settings applied, max_brightness: %d, dim after: %ds, off after: %ds", config.DisplayMaxBrightness, config.DisplayDimAfterSec, config.DisplayOffAfterSec) + logger.Info().Int("max_brightness", config.DisplayMaxBrightness).Int("dim_after", config.DisplayDimAfterSec).Int("off_after", config.DisplayOffAfterSec).Msg("rpc: display: settings applied") // If the device started up with auto-dim and/or auto-off set to zero, the display init // method will not have started the tickers. So in case that has changed, attempt to start the tickers now. @@ -318,7 +392,7 @@ func rpcSetDevModeState(enabled bool) error { return fmt.Errorf("failed to create devmode file: %w", err) } } else { - logger.Debug("dev mode already enabled") + logger.Debug().Msg("dev mode already enabled") return nil } } else { @@ -327,7 +401,7 @@ func rpcSetDevModeState(enabled bool) error { return fmt.Errorf("failed to remove devmode file: %w", err) } } else if os.IsNotExist(err) { - logger.Debug("dev mode already disabled") + logger.Debug().Msg("dev mode already disabled") return nil } else { return fmt.Errorf("error checking dev mode file: %w", err) @@ -337,7 +411,7 @@ func rpcSetDevModeState(enabled bool) error { cmd := exec.Command("dropbear.sh") output, err := cmd.CombinedOutput() if err != nil { - logger.Warnf("Failed to start/stop SSH: %v, %v", err, output) + logger.Warn().Err(err).Bytes("output", output).Msg("Failed to start/stop SSH") return fmt.Errorf("failed to start/stop SSH, you may need to reboot for changes to take effect") } @@ -375,7 +449,48 @@ func rpcSetSSHKeyState(sshKey string) error { return nil } -func callRPCHandler(handler RPCHandler, params map[string]interface{}) (interface{}, error) { +func rpcGetTLSState() TLSState { + return getTLSState() +} + +func rpcSetTLSState(state TLSState) error { + err := setTLSState(state) + if err != nil { + return fmt.Errorf("failed to set TLS state: %w", err) + } + + if err := SaveConfig(); err != nil { + return fmt.Errorf("failed to save config: %w", err) + } + + return nil +} + +type RPCHandler struct { + Func interface{} + Params []string +} + +// call the handler but recover from a panic to ensure our RPC thread doesn't collapse on malformed calls +func callRPCHandler(handler RPCHandler, params map[string]interface{}) (result interface{}, err error) { + // Use defer to recover from a panic + defer func() { + if r := recover(); r != nil { + // Convert the panic to an error + if e, ok := r.(error); ok { + err = e + } else { + err = fmt.Errorf("panic occurred: %v", r) + } + } + }() + + // Call the handler + result, err = riskyCallRPCHandler(handler, params) + return result, err +} + +func riskyCallRPCHandler(handler RPCHandler, params map[string]interface{}) (interface{}, error) { handlerValue := reflect.ValueOf(handler.Func) handlerType := handlerValue.Type() @@ -472,36 +587,34 @@ func callRPCHandler(handler RPCHandler, params map[string]interface{}) (interfac return nil, errors.New("unexpected return values from handler") } -type RPCHandler struct { - Func interface{} - Params []string -} - func rpcSetMassStorageMode(mode string) (string, error) { - logger.Infof("[jsonrpc.go:rpcSetMassStorageMode] Setting mass storage mode to: %s", mode) + logger.Info().Str("mode", mode).Msg("Setting mass storage mode") var cdrom bool - if mode == "cdrom" { + switch mode { + case "cdrom": cdrom = true - } else if mode != "file" { - logger.Infof("[jsonrpc.go:rpcSetMassStorageMode] Invalid mode provided: %s", mode) + case "file": + cdrom = false + default: + logger.Info().Str("mode", mode).Msg("Invalid mode provided") return "", fmt.Errorf("invalid mode: %s", mode) } - logger.Infof("[jsonrpc.go:rpcSetMassStorageMode] Setting mass storage mode to: %s", mode) + logger.Info().Str("mode", mode).Msg("Setting mass storage mode") err := setMassStorageMode(cdrom) if err != nil { return "", fmt.Errorf("failed to set mass storage mode: %w", err) } - logger.Infof("[jsonrpc.go:rpcSetMassStorageMode] Mass storage mode set to %s", mode) + logger.Info().Str("mode", mode).Msg("Mass storage mode set") // Get the updated mode after setting return rpcGetMassStorageMode() } func rpcGetMassStorageMode() (string, error) { - cdrom, err := getMassStorageMode() + cdrom, err := getMassStorageCDROMEnabled() if err != nil { return "", fmt.Errorf("failed to get mass storage mode: %w", err) } @@ -563,15 +676,16 @@ func rpcResetConfig() error { return fmt.Errorf("failed to reset config: %w", err) } - logger.Info("Configuration reset to default") + logger.Info().Msg("Configuration reset to default") return nil } type DCPowerState struct { - IsOn bool `json:"isOn"` - Voltage float64 `json:"voltage"` - Current float64 `json:"current"` - Power float64 `json:"power"` + IsOn bool `json:"isOn"` + Voltage float64 `json:"voltage"` + Current float64 `json:"current"` + Power float64 `json:"power"` + RestoreState int `json:"restoreState"` } func rpcGetDCPowerState() (DCPowerState, error) { @@ -579,7 +693,7 @@ func rpcGetDCPowerState() (DCPowerState, error) { } func rpcSetDCPowerState(enabled bool) error { - logger.Infof("[jsonrpc.go:rpcSetDCPowerState] Setting DC power state to: %v", enabled) + logger.Info().Bool("enabled", enabled).Msg("Setting DC power state") err := setDCPowerState(enabled) if err != nil { return fmt.Errorf("failed to set DC power state: %w", err) @@ -587,6 +701,15 @@ func rpcSetDCPowerState(enabled bool) error { return nil } +func rpcSetDCRestoreState(state int) error { + logger.Info().Int("state", state).Msg("Setting DC restore state") + err := setDCRestoreState(state) + if err != nil { + return fmt.Errorf("failed to set DC restore state: %w", err) + } + return nil +} + func rpcGetActiveExtension() (string, error) { return config.ActiveExtension, nil } @@ -595,34 +718,36 @@ func rpcSetActiveExtension(extensionId string) error { if config.ActiveExtension == extensionId { return nil } - if config.ActiveExtension == "atx-power" { + switch config.ActiveExtension { + case "atx-power": _ = unmountATXControl() - } else if config.ActiveExtension == "dc-power" { + case "dc-power": _ = unmountDCControl() } config.ActiveExtension = extensionId if err := SaveConfig(); err != nil { return fmt.Errorf("failed to save config: %w", err) } - if extensionId == "atx-power" { + switch extensionId { + case "atx-power": _ = mountATXControl() - } else if extensionId == "dc-power" { + case "dc-power": _ = mountDCControl() } return nil } func rpcSetATXPowerAction(action string) error { - logger.Debugf("[jsonrpc.go:rpcSetATXPowerAction] Executing ATX power action: %s", action) + logger.Debug().Str("action", action).Msg("Executing ATX power action") switch action { case "power-short": - logger.Debug("[jsonrpc.go:rpcSetATXPowerAction] Simulating short power button press") + logger.Debug().Msg("Simulating short power button press") return pressATXPowerButton(200 * time.Millisecond) case "power-long": - logger.Debug("[jsonrpc.go:rpcSetATXPowerAction] Simulating long power button press") + logger.Debug().Msg("Simulating long power button press") return pressATXPowerButton(5 * time.Second) case "reset": - logger.Debug("[jsonrpc.go:rpcSetATXPowerAction] Simulating reset button press") + logger.Debug().Msg("Simulating reset button press") return pressATXResetButton(200 * time.Millisecond) default: return fmt.Errorf("invalid action: %s", action) @@ -771,9 +896,14 @@ func rpcSetUsbDeviceState(device string, enabled bool) error { } func rpcSetCloudUrl(apiUrl string, appUrl string) error { + currentCloudURL := config.CloudURL config.CloudURL = apiUrl config.CloudAppURL = appUrl + if currentCloudURL != apiUrl { + disconnectCloud(fmt.Errorf("cloud url changed from %s to %s", currentCloudURL, apiUrl)) + } + if err := SaveConfig(); err != nil { return fmt.Errorf("failed to save config: %w", err) } @@ -781,23 +911,142 @@ func rpcSetCloudUrl(apiUrl string, appUrl string) error { return nil } -var currentScrollSensitivity string = "default" - -func rpcGetScrollSensitivity() (string, error) { - return currentScrollSensitivity, nil +func rpcGetKeyboardLayout() (string, error) { + return config.KeyboardLayout, nil } -func rpcSetScrollSensitivity(sensitivity string) error { - currentScrollSensitivity = sensitivity +func rpcSetKeyboardLayout(layout string) error { + config.KeyboardLayout = layout + if err := SaveConfig(); err != nil { + return fmt.Errorf("failed to save config: %w", err) + } + return nil +} + +func getKeyboardMacros() (interface{}, error) { + macros := make([]KeyboardMacro, len(config.KeyboardMacros)) + copy(macros, config.KeyboardMacros) + + return macros, nil +} + +type KeyboardMacrosParams struct { + Macros []interface{} `json:"macros"` +} + +func setKeyboardMacros(params KeyboardMacrosParams) (interface{}, error) { + if params.Macros == nil { + return nil, fmt.Errorf("missing or invalid macros parameter") + } + + newMacros := make([]KeyboardMacro, 0, len(params.Macros)) + + for i, item := range params.Macros { + macroMap, ok := item.(map[string]interface{}) + if !ok { + return nil, fmt.Errorf("invalid macro at index %d", i) + } + + id, _ := macroMap["id"].(string) + if id == "" { + id = fmt.Sprintf("macro-%d", time.Now().UnixNano()) + } + + name, _ := macroMap["name"].(string) + + sortOrder := i + 1 + if sortOrderFloat, ok := macroMap["sortOrder"].(float64); ok { + sortOrder = int(sortOrderFloat) + } + + steps := []KeyboardMacroStep{} + if stepsArray, ok := macroMap["steps"].([]interface{}); ok { + for _, stepItem := range stepsArray { + stepMap, ok := stepItem.(map[string]interface{}) + if !ok { + continue + } + + step := KeyboardMacroStep{} + + if keysArray, ok := stepMap["keys"].([]interface{}); ok { + for _, k := range keysArray { + if keyStr, ok := k.(string); ok { + step.Keys = append(step.Keys, keyStr) + } + } + } + + if modsArray, ok := stepMap["modifiers"].([]interface{}); ok { + for _, m := range modsArray { + if modStr, ok := m.(string); ok { + step.Modifiers = append(step.Modifiers, modStr) + } + } + } + + if delay, ok := stepMap["delay"].(float64); ok { + step.Delay = int(delay) + } + + steps = append(steps, step) + } + } + + macro := KeyboardMacro{ + ID: id, + Name: name, + Steps: steps, + SortOrder: sortOrder, + } + + if err := macro.Validate(); err != nil { + return nil, fmt.Errorf("invalid macro at index %d: %w", i, err) + } + + newMacros = append(newMacros, macro) + } + + config.KeyboardMacros = newMacros + + if err := SaveConfig(); err != nil { + return nil, err + } + + return nil, nil +} + +func rpcGetLocalLoopbackOnly() (bool, error) { + return config.LocalLoopbackOnly, nil +} + +func rpcSetLocalLoopbackOnly(enabled bool) error { + // Check if the setting is actually changing + if config.LocalLoopbackOnly == enabled { + return nil + } + + // Update the setting + config.LocalLoopbackOnly = enabled + if err := SaveConfig(); err != nil { + return fmt.Errorf("failed to save config: %w", err) + } + return nil } var rpcHandlers = map[string]RPCHandler{ "ping": {Func: rpcPing}, + "reboot": {Func: rpcReboot, Params: []string{"force"}}, "getDeviceID": {Func: rpcGetDeviceID}, "deregisterDevice": {Func: rpcDeregisterDevice}, "getCloudState": {Func: rpcGetCloudState}, + "getNetworkState": {Func: rpcGetNetworkState}, + "getNetworkSettings": {Func: rpcGetNetworkSettings}, + "setNetworkSettings": {Func: rpcSetNetworkSettings, Params: []string{"settings"}}, + "renewDHCPLease": {Func: rpcRenewDHCPLease}, "keyboardReport": {Func: rpcKeyboardReport, Params: []string{"modifier", "keys"}}, + "getKeyboardLedState": {Func: rpcGetKeyboardLedState}, "absMouseReport": {Func: rpcAbsMouseReport, Params: []string{"x", "y", "buttons"}}, "relMouseReport": {Func: rpcRelMouseReport, Params: []string{"dx", "dy", "buttons"}}, "wheelReport": {Func: rpcWheelReport, Params: []string{"wheelY"}}, @@ -824,6 +1073,8 @@ var rpcHandlers = map[string]RPCHandler{ "setDevModeState": {Func: rpcSetDevModeState, Params: []string{"enabled"}}, "getSSHKeyState": {Func: rpcGetSSHKeyState}, "setSSHKeyState": {Func: rpcSetSSHKeyState, Params: []string{"sshKey"}}, + "getTLSState": {Func: rpcGetTLSState}, + "setTLSState": {Func: rpcSetTLSState, Params: []string{"state"}}, "setMassStorageMode": {Func: rpcSetMassStorageMode, Params: []string{"mode"}}, "getMassStorageMode": {Func: rpcGetMassStorageMode}, "isUpdatePending": {Func: rpcIsUpdatePending}, @@ -843,10 +1094,13 @@ var rpcHandlers = map[string]RPCHandler{ "getWakeOnLanDevices": {Func: rpcGetWakeOnLanDevices}, "setWakeOnLanDevices": {Func: rpcSetWakeOnLanDevices, Params: []string{"params"}}, "resetConfig": {Func: rpcResetConfig}, + "setDisplayRotation": {Func: rpcSetDisplayRotation, Params: []string{"params"}}, + "getDisplayRotation": {Func: rpcGetDisplayRotation}, "setBacklightSettings": {Func: rpcSetBacklightSettings, Params: []string{"params"}}, "getBacklightSettings": {Func: rpcGetBacklightSettings}, "getDCPowerState": {Func: rpcGetDCPowerState}, "setDCPowerState": {Func: rpcSetDCPowerState, Params: []string{"enabled"}}, + "setDCRestoreState": {Func: rpcSetDCRestoreState, Params: []string{"state"}}, "getActiveExtension": {Func: rpcGetActiveExtension}, "setActiveExtension": {Func: rpcSetActiveExtension, Params: []string{"extensionId"}}, "getATXState": {Func: rpcGetATXState}, @@ -857,6 +1111,10 @@ var rpcHandlers = map[string]RPCHandler{ "setUsbDevices": {Func: rpcSetUsbDevices, Params: []string{"devices"}}, "setUsbDeviceState": {Func: rpcSetUsbDeviceState, Params: []string{"device", "enabled"}}, "setCloudUrl": {Func: rpcSetCloudUrl, Params: []string{"apiUrl", "appUrl"}}, - "getScrollSensitivity": {Func: rpcGetScrollSensitivity}, - "setScrollSensitivity": {Func: rpcSetScrollSensitivity, Params: []string{"sensitivity"}}, + "getKeyboardLayout": {Func: rpcGetKeyboardLayout}, + "setKeyboardLayout": {Func: rpcSetKeyboardLayout, Params: []string{"layout"}}, + "getKeyboardMacros": {Func: getKeyboardMacros}, + "setKeyboardMacros": {Func: setKeyboardMacros, Params: []string{"params"}}, + "getLocalLoopbackOnly": {Func: rpcGetLocalLoopbackOnly}, + "setLocalLoopbackOnly": {Func: rpcSetLocalLoopbackOnly, Params: []string{"enabled"}}, } diff --git a/log.go b/log.go index 7718a28..b353a2c 100644 --- a/log.go +++ b/log.go @@ -1,8 +1,32 @@ package kvm -import "github.com/pion/logging" +import ( + "github.com/jetkvm/kvm/internal/logging" + "github.com/rs/zerolog" +) -// we use logging framework from pion -// ref: https://github.com/pion/webrtc/wiki/Debugging-WebRTC -var logger = logging.NewDefaultLoggerFactory().NewLogger("jetkvm") -var cloudLogger = logging.NewDefaultLoggerFactory().NewLogger("cloud") +func ErrorfL(l *zerolog.Logger, format string, err error, args ...interface{}) error { + return logging.ErrorfL(l, format, err, args...) +} + +var ( + logger = logging.GetSubsystemLogger("jetkvm") + networkLogger = logging.GetSubsystemLogger("network") + cloudLogger = logging.GetSubsystemLogger("cloud") + websocketLogger = logging.GetSubsystemLogger("websocket") + webrtcLogger = logging.GetSubsystemLogger("webrtc") + nativeLogger = logging.GetSubsystemLogger("native") + nbdLogger = logging.GetSubsystemLogger("nbd") + timesyncLogger = logging.GetSubsystemLogger("timesync") + jsonRpcLogger = logging.GetSubsystemLogger("jsonrpc") + watchdogLogger = logging.GetSubsystemLogger("watchdog") + websecureLogger = logging.GetSubsystemLogger("websecure") + otaLogger = logging.GetSubsystemLogger("ota") + serialLogger = logging.GetSubsystemLogger("serial") + terminalLogger = logging.GetSubsystemLogger("terminal") + displayLogger = logging.GetSubsystemLogger("display") + wolLogger = logging.GetSubsystemLogger("wol") + usbLogger = logging.GetSubsystemLogger("usb") + // external components + ginLogger = logging.GetSubsystemLogger("gin") +) diff --git a/main.go b/main.go index 6a55595..c25d8b8 100644 --- a/main.go +++ b/main.go @@ -14,25 +14,55 @@ import ( var appCtx context.Context func Main() { + LoadConfig() + var cancel context.CancelFunc appCtx, cancel = context.WithCancel(context.Background()) defer cancel() - logger.Info("Starting JetKvm") + + systemVersionLocal, appVersionLocal, err := GetLocalVersion() + if err != nil { + logger.Warn().Err(err).Msg("failed to get local version") + } + + logger.Info(). + Interface("system_version", systemVersionLocal). + Interface("app_version", appVersionLocal). + Msg("starting JetKVM") + go runWatchdog() go confirmCurrentSystem() http.DefaultClient.Timeout = 1 * time.Minute - LoadConfig() - logger.Debug("config loaded") - err := rootcerts.UpdateDefaultTransport() + err = rootcerts.UpdateDefaultTransport() if err != nil { - logger.Errorf("failed to load CA certs: %v", err) + logger.Warn().Err(err).Msg("failed to load Root CA certificates") + } + logger.Info(). + Int("ca_certs_loaded", len(rootcerts.Certs())). + Msg("loaded Root CA certificates") + + // Initialize network + if err := initNetwork(); err != nil { + logger.Error().Err(err).Msg("failed to initialize network") + os.Exit(1) } - go TimeSyncLoop() + // Initialize time sync + initTimeSync() + timeSync.Start() + // Initialize mDNS + if err := initMdns(); err != nil { + logger.Error().Err(err).Msg("failed to initialize mDNS") + os.Exit(1) + } + + // Initialize native ctrl socket server StartNativeCtrlSocketServer() + + // Initialize native video socket server StartNativeVideoSocketServer() initPrometheus() @@ -40,48 +70,62 @@ func Main() { go func() { err = ExtractAndRunNativeBin() if err != nil { - logger.Errorf("failed to extract and run native bin: %v", err) + logger.Warn().Err(err).Msg("failed to extract and run native bin") //TODO: prepare an error message screen buffer to show on kvm screen } }() + // initialize usb gadget initUsbGadget() + if err := setInitialVirtualMediaState(); err != nil { + logger.Warn().Err(err).Msg("failed to set initial virtual media state") + } + + if err := initImagesFolder(); err != nil { + logger.Warn().Err(err).Msg("failed to init images folder") + } + initJiggler() + + // initialize display + initDisplay() go func() { time.Sleep(15 * time.Minute) for { - logger.Debugf("UPDATING - Auto update enabled: %v", config.AutoUpdateEnabled) + logger.Debug().Bool("auto_update_enabled", config.AutoUpdateEnabled).Msg("UPDATING") if !config.AutoUpdateEnabled { return } if currentSession != nil { - logger.Debugf("skipping update since a session is active") + logger.Debug().Msg("skipping update since a session is active") time.Sleep(1 * time.Minute) continue } includePreRelease := config.IncludePreRelease err = TryUpdate(context.Background(), GetDeviceID(), includePreRelease) if err != nil { - logger.Errorf("failed to auto update: %v", err) + logger.Warn().Err(err).Msg("failed to auto update") } time.Sleep(1 * time.Hour) } }() //go RunFuseServer() go RunWebServer() + + go RunWebSecureServer() + // Web secure server is started only if TLS mode is enabled if config.TLSMode != "" { - go RunWebSecureServer() - } - // If the cloud token isn't set, the client won't be started by default. - // However, if the user adopts the device via the web interface, handleCloudRegister will start the client. - if config.CloudToken != "" { - go RunWebsocketClient() + startWebSecureServer() } + + // As websocket client already checks if the cloud token is set, we can start it here. + go RunWebsocketClient() + initSerialPort() sigs := make(chan os.Signal, 1) signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM) <-sigs - logger.Info("JetKVM Shutting Down") + logger.Info().Msg("JetKVM Shutting Down") //if fuseServer != nil { // err := setMassStorageImage(" ") // if err != nil { diff --git a/mdns.go b/mdns.go new file mode 100644 index 0000000..d7a3b55 --- /dev/null +++ b/mdns.go @@ -0,0 +1,29 @@ +package kvm + +import ( + "github.com/jetkvm/kvm/internal/mdns" +) + +var mDNS *mdns.MDNS + +func initMdns() error { + m, err := mdns.NewMDNS(&mdns.MDNSOptions{ + Logger: logger, + LocalNames: []string{ + networkState.GetHostname(), + networkState.GetFQDN(), + }, + ListenOptions: &mdns.MDNSListenOptions{ + IPv4: true, + IPv6: true, + }, + }) + if err != nil { + return err + } + + // do not start the server yet, as we need to wait for the network state to be set + mDNS = m + + return nil +} diff --git a/native.go b/native.go index 8960304..9807206 100644 --- a/native.go +++ b/native.go @@ -3,13 +3,14 @@ package kvm import ( "bytes" "encoding/json" + "errors" "fmt" "io" "net" "os" "os/exec" + "strings" "sync" - "syscall" "time" "github.com/jetkvm/kvm/resource" @@ -42,6 +43,11 @@ var ongoingRequests = make(map[int32]chan *CtrlResponse) var lock = &sync.Mutex{} +var ( + nativeCmd *exec.Cmd + nativeCmdLock = &sync.Mutex{} +) + func CallCtrlAction(action string, params map[string]interface{}) (*CtrlResponse, error) { lock.Lock() defer lock.Unlock() @@ -61,25 +67,33 @@ func CallCtrlAction(action string, params map[string]interface{}) (*CtrlResponse return nil, fmt.Errorf("error marshaling ctrl action: %w", err) } - logger.Infof("sending ctrl action: %s", string(jsonData)) + scopedLogger := nativeLogger.With(). + Str("action", ctrlAction.Action). + Interface("params", ctrlAction.Params).Logger() + + scopedLogger.Debug().Msg("sending ctrl action") err = WriteCtrlMessage(jsonData) if err != nil { delete(ongoingRequests, ctrlAction.Seq) - return nil, fmt.Errorf("error writing ctrl message: %w", err) + return nil, ErrorfL(&scopedLogger, "error writing ctrl message", err) } select { case response := <-responseChan: delete(ongoingRequests, seq) if response.Error != "" { - return nil, fmt.Errorf("error native response: %s", response.Error) + return nil, ErrorfL( + &scopedLogger, + "error native response: %s", + errors.New(response.Error), + ) } return response, nil case <-time.After(5 * time.Second): close(responseChan) delete(ongoingRequests, seq) - return nil, fmt.Errorf("timeout waiting for response") + return nil, ErrorfL(&scopedLogger, "timeout waiting for response", nil) } } @@ -101,33 +115,47 @@ func waitCtrlClientConnected() { } func StartNativeSocketServer(socketPath string, handleClient func(net.Conn), isCtrl bool) net.Listener { + scopedLogger := nativeLogger.With(). + Str("socket_path", socketPath). + Logger() + // Remove the socket file if it already exists if _, err := os.Stat(socketPath); err == nil { if err := os.Remove(socketPath); err != nil { - logger.Errorf("Failed to remove existing socket file %s: %v", socketPath, err) + scopedLogger.Warn().Err(err).Msg("failed to remove existing socket file") os.Exit(1) } } listener, err := net.Listen("unixpacket", socketPath) if err != nil { - logger.Errorf("Failed to start server on %s: %v", socketPath, err) + scopedLogger.Warn().Err(err).Msg("failed to start server") os.Exit(1) } - logger.Infof("Server listening on %s", socketPath) + scopedLogger.Info().Msg("server listening") go func() { - conn, err := listener.Accept() - listener.Close() - if err != nil { - logger.Errorf("failed to accept sock: %v", err) + for { + conn, err := listener.Accept() + + if err != nil { + scopedLogger.Warn().Err(err).Msg("failed to accept socket") + continue + } + if isCtrl { + // check if the channel is closed + select { + case <-ctrlClientConnected: + scopedLogger.Debug().Msg("ctrl client reconnected") + default: + close(ctrlClientConnected) + scopedLogger.Debug().Msg("first native ctrl socket client connected") + } + } + + go handleClient(conn) } - if isCtrl { - close(ctrlClientConnected) - logger.Debug("first native ctrl socket client connected") - } - handleClient(conn) }() return listener @@ -135,20 +163,25 @@ func StartNativeSocketServer(socketPath string, handleClient func(net.Conn), isC func StartNativeCtrlSocketServer() { nativeCtrlSocketListener = StartNativeSocketServer("/var/run/jetkvm_ctrl.sock", handleCtrlClient, true) - logger.Debug("native app ctrl sock started") + nativeLogger.Debug().Msg("native app ctrl sock started") } func StartNativeVideoSocketServer() { nativeVideoSocketListener = StartNativeSocketServer("/var/run/jetkvm_video.sock", handleVideoClient, false) - logger.Debug("native app video sock started") + nativeLogger.Debug().Msg("native app video sock started") } func handleCtrlClient(conn net.Conn) { defer conn.Close() - logger.Debug("native socket client connected") + scopedLogger := nativeLogger.With(). + Str("addr", conn.RemoteAddr().String()). + Str("type", "ctrl"). + Logger() + + scopedLogger.Info().Msg("native ctrl socket client connected") if ctrlSocketConn != nil { - logger.Debugf("closing existing native socket connection") + scopedLogger.Debug().Msg("closing existing native socket connection") ctrlSocketConn.Close() } @@ -161,17 +194,19 @@ func handleCtrlClient(conn net.Conn) { for { n, err := conn.Read(readBuf) if err != nil { - logger.Errorf("error reading from ctrl sock: %v", err) + scopedLogger.Warn().Err(err).Msg("error reading from ctrl sock") break } readMsg := string(readBuf[:n]) - logger.Tracef("ctrl sock msg: %v", readMsg) + ctrlResp := CtrlResponse{} err = json.Unmarshal([]byte(readMsg), &ctrlResp) if err != nil { - logger.Warnf("error parsing ctrl sock msg: %v", err) + scopedLogger.Warn().Err(err).Str("data", readMsg).Msg("error parsing ctrl sock msg") continue } + scopedLogger.Trace().Interface("data", ctrlResp).Msg("ctrl sock msg") + if ctrlResp.Seq != 0 { responseChan, ok := ongoingRequests[ctrlResp.Seq] if ok { @@ -184,20 +219,25 @@ func handleCtrlClient(conn net.Conn) { } } - logger.Debug("ctrl sock disconnected") + scopedLogger.Debug().Msg("ctrl sock disconnected") } func handleVideoClient(conn net.Conn) { defer conn.Close() - logger.Infof("Native video socket client connected: %v", conn.RemoteAddr()) + scopedLogger := nativeLogger.With(). + Str("addr", conn.RemoteAddr().String()). + Str("type", "video"). + Logger() + + scopedLogger.Info().Msg("native video socket client connected") inboundPacket := make([]byte, maxFrameSize) lastFrame := time.Now() for { n, err := conn.Read(inboundPacket) if err != nil { - logger.Warnf("error during read: %v", err) + scopedLogger.Warn().Err(err).Msg("error during read") return } now := time.Now() @@ -206,12 +246,64 @@ func handleVideoClient(conn net.Conn) { if currentSession != nil { err := currentSession.VideoTrack.WriteSample(media.Sample{Data: inboundPacket[:n], Duration: sinceLastFrame}) if err != nil { - logger.Warnf("error writing sample: %v", err) + scopedLogger.Warn().Err(err).Msg("error writing sample") } } } } +func startNativeBinaryWithLock(binaryPath string) (*exec.Cmd, error) { + nativeCmdLock.Lock() + defer nativeCmdLock.Unlock() + + cmd, err := startNativeBinary(binaryPath) + if err != nil { + return nil, err + } + nativeCmd = cmd + return cmd, nil +} + +func restartNativeBinary(binaryPath string) error { + time.Sleep(10 * time.Second) + // restart the binary + nativeLogger.Info().Msg("restarting jetkvm_native binary") + cmd, err := startNativeBinary(binaryPath) + if err != nil { + nativeLogger.Warn().Err(err).Msg("failed to restart binary") + } + nativeCmd = cmd + + // reset the display state + time.Sleep(1 * time.Second) + clearDisplayState() + updateStaticContents() + requestDisplayUpdate(true) + + return err +} + +func superviseNativeBinary(binaryPath string) error { + nativeCmdLock.Lock() + defer nativeCmdLock.Unlock() + + if nativeCmd == nil || nativeCmd.Process == nil { + return restartNativeBinary(binaryPath) + } + + err := nativeCmd.Wait() + + if err == nil { + nativeLogger.Info().Err(err).Msg("jetkvm_native binary exited with no error") + } else if exiterr, ok := err.(*exec.ExitError); ok { + nativeLogger.Warn().Int("exit_code", exiterr.ExitCode()).Msg("jetkvm_native binary exited with error") + } else { + nativeLogger.Warn().Err(err).Msg("jetkvm_native binary exited with unknown error") + } + + return restartNativeBinary(binaryPath) +} + func ExtractAndRunNativeBin() error { binaryPath := "/userdata/jetkvm/bin/jetkvm_native" if err := ensureBinaryUpdated(binaryPath); err != nil { @@ -223,54 +315,74 @@ func ExtractAndRunNativeBin() error { return fmt.Errorf("failed to make binary executable: %w", err) } // Run the binary in the background - cmd := exec.Command(binaryPath) - - // Redirect stdout and stderr to the current process - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - - // Set the process group ID so we can kill the process and its children when this process exits - cmd.SysProcAttr = &syscall.SysProcAttr{ - Setpgid: true, - Pdeathsig: syscall.SIGKILL, - } - - // Start the command - if err := cmd.Start(); err != nil { + cmd, err := startNativeBinaryWithLock(binaryPath) + if err != nil { return fmt.Errorf("failed to start binary: %w", err) } - //TODO: add auto restart + // check if the binary is still running every 10 seconds + go func() { + for { + select { + case <-appCtx.Done(): + nativeLogger.Info().Msg("stopping native binary supervisor") + return + default: + err := superviseNativeBinary(binaryPath) + if err != nil { + nativeLogger.Warn().Err(err).Msg("failed to supervise native binary") + time.Sleep(1 * time.Second) // Add a short delay to prevent rapid successive calls + } + } + } + }() + go func() { <-appCtx.Done() - logger.Infof("killing process PID: %d", cmd.Process.Pid) + nativeLogger.Info().Int("pid", cmd.Process.Pid).Msg("killing process") err := cmd.Process.Kill() if err != nil { - logger.Errorf("failed to kill process: %v", err) + nativeLogger.Warn().Err(err).Msg("failed to kill process") return } }() - logger.Infof("Binary started with PID: %d", cmd.Process.Pid) + nativeLogger.Info().Int("pid", cmd.Process.Pid).Msg("jetkvm_native binary started") return nil } func shouldOverwrite(destPath string, srcHash []byte) bool { if srcHash == nil { - logger.Debug("error reading embedded jetkvm_native.sha256, doing overwriting") + nativeLogger.Debug().Msg("error reading embedded jetkvm_native.sha256, doing overwriting") return true } dstHash, err := os.ReadFile(destPath + ".sha256") if err != nil { - logger.Debug("error reading existing jetkvm_native.sha256, doing overwriting") + nativeLogger.Debug().Msg("error reading existing jetkvm_native.sha256, doing overwriting") return true } return !bytes.Equal(srcHash, dstHash) } +func getNativeSha256() ([]byte, error) { + version, err := resource.ResourceFS.ReadFile("jetkvm_native.sha256") + if err != nil { + return nil, err + } + return version, nil +} + +func GetNativeVersion() (string, error) { + version, err := getNativeSha256() + if err != nil { + return "", err + } + return strings.TrimSpace(string(version)), nil +} + func ensureBinaryUpdated(destPath string) error { srcFile, err := resource.ResourceFS.Open("jetkvm_native") if err != nil { @@ -278,15 +390,18 @@ func ensureBinaryUpdated(destPath string) error { } defer srcFile.Close() - srcHash, err := resource.ResourceFS.ReadFile("jetkvm_native.sha256") + srcHash, err := getNativeSha256() if err != nil { - logger.Debug("error reading embedded jetkvm_native.sha256, proceeding with update") + nativeLogger.Debug().Msg("error reading embedded jetkvm_native.sha256, proceeding with update") srcHash = nil } _, err = os.Stat(destPath) if shouldOverwrite(destPath, srcHash) || err != nil { - logger.Info("writing jetkvm_native") + nativeLogger.Info(). + Interface("hash", srcHash). + Msg("writing jetkvm_native") + _ = os.Remove(destPath) destFile, err := os.OpenFile(destPath, os.O_CREATE|os.O_RDWR, 0755) if err != nil { @@ -303,7 +418,7 @@ func ensureBinaryUpdated(destPath string) error { return err } } - logger.Info("jetkvm_native updated") + nativeLogger.Info().Msg("jetkvm_native updated") } return nil @@ -313,10 +428,10 @@ func ensureBinaryUpdated(destPath string) error { // Called after successful connection to jetkvm_native. func restoreHdmiEdid() { if config.EdidString != "" { - logger.Infof("Restoring HDMI EDID to %v", config.EdidString) + nativeLogger.Info().Str("edid", config.EdidString).Msg("Restoring HDMI EDID") _, err := CallCtrlAction("set_edid", map[string]interface{}{"edid": config.EdidString}) if err != nil { - logger.Errorf("Failed to restore HDMI EDID: %v", err) + nativeLogger.Warn().Err(err).Msg("Failed to restore HDMI EDID") } } } diff --git a/native_linux.go b/native_linux.go new file mode 100644 index 0000000..54d2150 --- /dev/null +++ b/native_linux.go @@ -0,0 +1,57 @@ +//go:build linux + +package kvm + +import ( + "fmt" + "os/exec" + "sync" + "syscall" + + "github.com/rs/zerolog" +) + +type nativeOutput struct { + mu *sync.Mutex + logger *zerolog.Event +} + +func (w *nativeOutput) Write(p []byte) (n int, err error) { + w.mu.Lock() + defer w.mu.Unlock() + + w.logger.Msg(string(p)) + return len(p), nil +} + +func startNativeBinary(binaryPath string) (*exec.Cmd, error) { + // Run the binary in the background + cmd := exec.Command(binaryPath) + + nativeOutputLock := sync.Mutex{} + nativeStdout := &nativeOutput{ + mu: &nativeOutputLock, + logger: nativeLogger.Info().Str("pipe", "stdout"), + } + nativeStderr := &nativeOutput{ + mu: &nativeOutputLock, + logger: nativeLogger.Info().Str("pipe", "stderr"), + } + + // Redirect stdout and stderr to the current process + cmd.Stdout = nativeStdout + cmd.Stderr = nativeStderr + + // Set the process group ID so we can kill the process and its children when this process exits + cmd.SysProcAttr = &syscall.SysProcAttr{ + Setpgid: true, + Pdeathsig: syscall.SIGKILL, + } + + // Start the command + if err := cmd.Start(); err != nil { + return nil, fmt.Errorf("failed to start binary: %w", err) + } + + return cmd, nil +} diff --git a/native_notlinux.go b/native_notlinux.go new file mode 100644 index 0000000..df6df74 --- /dev/null +++ b/native_notlinux.go @@ -0,0 +1,12 @@ +//go:build !linux + +package kvm + +import ( + "fmt" + "os/exec" +) + +func startNativeBinary(binaryPath string) (*exec.Cmd, error) { + return nil, fmt.Errorf("not supported") +} diff --git a/network.go b/network.go index 66b8616..d4f46e7 100644 --- a/network.go +++ b/network.go @@ -1,227 +1,110 @@ package kvm import ( - "bytes" "fmt" - "net" - "os" - "strings" - "time" - "os/exec" - - "github.com/hashicorp/go-envparse" - "github.com/pion/mdns/v2" - "golang.org/x/net/ipv4" - "golang.org/x/net/ipv6" - - "github.com/vishvananda/netlink" - "github.com/vishvananda/netlink/nl" + "github.com/jetkvm/kvm/internal/network" + "github.com/jetkvm/kvm/internal/udhcpc" ) -var mDNSConn *mdns.Conn - -var networkState NetworkState - -type NetworkState struct { - Up bool - IPv4 string - IPv6 string - MAC string - - checked bool -} - -type LocalIpInfo struct { - IPv4 string - IPv6 string - MAC string -} - const ( - NetIfName = "eth0" - DHCPLeaseFile = "/run/udhcpc.%s.info" + NetIfName = "eth0" ) -// setDhcpClientState sends signals to udhcpc to change it's current mode -// of operation. Setting active to true will force udhcpc to renew the DHCP lease. -// Setting active to false will put udhcpc into idle mode. -func setDhcpClientState(active bool) { - var signal string - if active { - signal = "-SIGUSR1" - } else { - signal = "-SIGUSR2" +var ( + networkState *network.NetworkInterfaceState +) + +func networkStateChanged() { + // do not block the main thread + go waitCtrlAndRequestDisplayUpdate(true) + + if timeSync != nil { + if networkState != nil { + timeSync.SetDhcpNtpAddresses(networkState.NtpAddressesString()) + } + + if err := timeSync.Sync(); err != nil { + networkLogger.Error().Err(err).Msg("failed to sync time after network state change") + } } - cmd := exec.Command("/usr/bin/killall", signal, "udhcpc") - if err := cmd.Run(); err != nil { - logger.Warnf("network: setDhcpClientState: failed to change udhcpc state: %s", err) + // always restart mDNS when the network state changes + if mDNS != nil { + _ = mDNS.SetListenOptions(config.NetworkConfig.GetMDNSMode()) + _ = mDNS.SetLocalNames([]string{ + networkState.GetHostname(), + networkState.GetFQDN(), + }, true) } } -func checkNetworkState() { - iface, err := netlink.LinkByName(NetIfName) - if err != nil { - logger.Warnf("failed to get [%s] interface: %v", NetIfName, err) - return - } +func initNetwork() error { + ensureConfigLoaded() - newState := NetworkState{ - Up: iface.Attrs().OperState == netlink.OperUp, - MAC: iface.Attrs().HardwareAddr.String(), + state, err := network.NewNetworkInterfaceState(&network.NetworkInterfaceOptions{ + DefaultHostname: GetDefaultHostname(), + InterfaceName: NetIfName, + NetworkConfig: config.NetworkConfig, + Logger: networkLogger, + OnStateChange: func(state *network.NetworkInterfaceState) { + networkStateChanged() + }, + OnInitialCheck: func(state *network.NetworkInterfaceState) { + networkStateChanged() + }, + OnDhcpLeaseChange: func(lease *udhcpc.Lease) { + networkStateChanged() - checked: true, - } - - addrs, err := netlink.AddrList(iface, nl.FAMILY_ALL) - if err != nil { - logger.Warnf("failed to get addresses for [%s]: %v", NetIfName, err) - } - - // If the link is going down, put udhcpc into idle mode. - // If the link is coming back up, activate udhcpc and force it to renew the lease. - if newState.Up != networkState.Up { - setDhcpClientState(newState.Up) - } - - for _, addr := range addrs { - if addr.IP.To4() != nil { - if !newState.Up && networkState.Up { - // If the network is going down, remove all IPv4 addresses from the interface. - logger.Infof("network: state transitioned to down, removing IPv4 address %s", addr.IP.String()) - err := netlink.AddrDel(iface, &addr) - if err != nil { - logger.Warnf("network: failed to delete %s", addr.IP.String()) - } - - newState.IPv4 = "..." - } else { - newState.IPv4 = addr.IP.String() + if currentSession == nil { + return } - } else if addr.IP.To16() != nil && newState.IPv6 == "" { - newState.IPv6 = addr.IP.String() - } - } - if newState != networkState { - logger.Info("network state changed") - // restart MDNS - _ = startMDNS() - networkState = newState - requestDisplayUpdate() - } -} - -func startMDNS() error { - // If server was previously running, stop it - if mDNSConn != nil { - logger.Info("Stopping mDNS server") - err := mDNSConn.Close() - if err != nil { - logger.Warnf("failed to stop mDNS server: %v", err) - } - } - - // Start a new server - logger.Info("Starting mDNS server on jetkvm.local") - addr4, err := net.ResolveUDPAddr("udp4", mdns.DefaultAddressIPv4) - if err != nil { - return err - } - - addr6, err := net.ResolveUDPAddr("udp6", mdns.DefaultAddressIPv6) - if err != nil { - return err - } - - l4, err := net.ListenUDP("udp4", addr4) - if err != nil { - return err - } - - l6, err := net.ListenUDP("udp6", addr6) - if err != nil { - return err - } - - mDNSConn, err = mdns.Server(ipv4.NewPacketConn(l4), ipv6.NewPacketConn(l6), &mdns.Config{ - LocalNames: []string{"jetkvm.local"}, //TODO: make it configurable + writeJSONRPCEvent("networkState", networkState.RpcGetNetworkState(), currentSession) + }, + OnConfigChange: func(networkConfig *network.NetworkConfig) { + config.NetworkConfig = networkConfig + networkStateChanged() + }, }) - if err != nil { - mDNSConn = nil + + if state == nil { + if err == nil { + return fmt.Errorf("failed to create NetworkInterfaceState") + } return err } - //defer server.Close() + + if err := state.Run(); err != nil { + return err + } + + networkState = state + return nil } -func getNTPServersFromDHCPInfo() ([]string, error) { - buf, err := os.ReadFile(fmt.Sprintf(DHCPLeaseFile, NetIfName)) - if err != nil { - // do not return error if file does not exist - if os.IsNotExist(err) { - return nil, nil - } - return nil, fmt.Errorf("failed to load udhcpc info: %w", err) - } - - // parse udhcpc info - env, err := envparse.Parse(bytes.NewReader(buf)) - if err != nil { - return nil, fmt.Errorf("failed to parse udhcpc info: %w", err) - } - - val, ok := env["ntpsrv"] - if !ok { - return nil, nil - } - - var servers []string - - for _, server := range strings.Fields(val) { - if net.ParseIP(server) == nil { - logger.Infof("invalid NTP server IP: %s, ignoring", server) - } - servers = append(servers, server) - } - - return servers, nil +func rpcGetNetworkState() network.RpcNetworkState { + return networkState.RpcGetNetworkState() } -func init() { - ensureConfigLoaded() - - updates := make(chan netlink.LinkUpdate) - done := make(chan struct{}) - - if err := netlink.LinkSubscribe(updates, done); err != nil { - logger.Warnf("failed to subscribe to link updates: %v", err) - return - } - - go func() { - waitCtrlClientConnected() - checkNetworkState() - ticker := time.NewTicker(1 * time.Second) - defer ticker.Stop() - - for { - select { - case update := <-updates: - if update.Link.Attrs().Name == NetIfName { - logger.Infof("link update: %+v", update) - checkNetworkState() - } - case <-ticker.C: - checkNetworkState() - case <-done: - return - } - } - }() - err := startMDNS() - if err != nil { - logger.Warnf("failed to run mDNS: %v", err) - } +func rpcGetNetworkSettings() network.RpcNetworkSettings { + return networkState.RpcGetNetworkSettings() +} + +func rpcSetNetworkSettings(settings network.RpcNetworkSettings) (*network.RpcNetworkSettings, error) { + s := networkState.RpcSetNetworkSettings(settings) + if s != nil { + return nil, s + } + + if err := SaveConfig(); err != nil { + return nil, err + } + + return &network.RpcNetworkSettings{NetworkConfig: *config.NetworkConfig}, nil +} + +func rpcRenewDHCPLease() error { + return networkState.RpcRenewDHCPLease() } diff --git a/ntp.go b/ntp.go deleted file mode 100644 index 39ea7af..0000000 --- a/ntp.go +++ /dev/null @@ -1,140 +0,0 @@ -package kvm - -import ( - "errors" - "fmt" - "net/http" - "os/exec" - "time" - - "github.com/beevik/ntp" -) - -const ( - timeSyncRetryStep = 5 * time.Second - timeSyncRetryMaxInt = 1 * time.Minute - timeSyncWaitNetChkInt = 100 * time.Millisecond - timeSyncWaitNetUpInt = 3 * time.Second - timeSyncInterval = 1 * time.Hour - timeSyncTimeout = 2 * time.Second -) - -var ( - timeSyncRetryInterval = 0 * time.Second - defaultNTPServers = []string{ - "time.cloudflare.com", - "time.apple.com", - } -) - -func TimeSyncLoop() { - for { - if !networkState.checked { - time.Sleep(timeSyncWaitNetChkInt) - continue - } - - if !networkState.Up { - logger.Infof("Waiting for network to come up") - time.Sleep(timeSyncWaitNetUpInt) - continue - } - - logger.Infof("Syncing system time") - start := time.Now() - err := SyncSystemTime() - if err != nil { - logger.Warnf("Failed to sync system time: %v", err) - - // retry after a delay - timeSyncRetryInterval += timeSyncRetryStep - time.Sleep(timeSyncRetryInterval) - // reset the retry interval if it exceeds the max interval - if timeSyncRetryInterval > timeSyncRetryMaxInt { - timeSyncRetryInterval = 0 - } - - continue - } - logger.Infof("Time sync successful, now is: %v, time taken: %v", time.Now(), time.Since(start)) - time.Sleep(timeSyncInterval) // after the first sync is done - } -} - -func SyncSystemTime() (err error) { - now, err := queryNetworkTime() - if err != nil { - return fmt.Errorf("failed to query network time: %w", err) - } - err = setSystemTime(*now) - if err != nil { - return fmt.Errorf("failed to set system time: %w", err) - } - return nil -} - -func queryNetworkTime() (*time.Time, error) { - ntpServers, err := getNTPServersFromDHCPInfo() - if err != nil { - logger.Warnf("failed to get NTP servers from DHCP info: %v\n", err) - } - - if ntpServers == nil { - ntpServers = defaultNTPServers - logger.Infof("Using default NTP servers: %v\n", ntpServers) - } else { - logger.Infof("Using NTP servers from DHCP: %v\n", ntpServers) - } - - for _, server := range ntpServers { - now, err := queryNtpServer(server, timeSyncTimeout) - if err == nil { - logger.Infof("NTP server [%s] returned time: %v\n", server, now) - return now, nil - } - } - httpUrls := []string{ - "http://apple.com", - "http://cloudflare.com", - } - for _, url := range httpUrls { - now, err := queryHttpTime(url, timeSyncTimeout) - if err == nil { - return now, nil - } - } - return nil, errors.New("failed to query network time") -} - -func queryNtpServer(server string, timeout time.Duration) (now *time.Time, err error) { - resp, err := ntp.QueryWithOptions(server, ntp.QueryOptions{Timeout: timeout}) - if err != nil { - return nil, err - } - return &resp.Time, nil -} - -func queryHttpTime(url string, timeout time.Duration) (*time.Time, error) { - client := http.Client{ - Timeout: timeout, - } - resp, err := client.Head(url) - if err != nil { - return nil, err - } - dateStr := resp.Header.Get("Date") - now, err := time.Parse(time.RFC1123, dateStr) - if err != nil { - return nil, err - } - return &now, nil -} - -func setSystemTime(now time.Time) error { - nowStr := now.Format("2006-01-02 15:04:05") - output, err := exec.Command("date", "-s", nowStr).CombinedOutput() - if err != nil { - return fmt.Errorf("failed to run date -s: %w, %s", err, string(output)) - } - return nil -} diff --git a/ota.go b/ota.go index f813c09..bf0828d 100644 --- a/ota.go +++ b/ota.go @@ -4,6 +4,7 @@ import ( "bytes" "context" "crypto/sha256" + "crypto/tls" "encoding/hex" "encoding/json" "fmt" @@ -16,6 +17,8 @@ import ( "time" "github.com/Masterminds/semver/v3" + "github.com/gwatts/rootcerts" + "github.com/rs/zerolog" ) type UpdateMetadata struct { @@ -38,12 +41,19 @@ type UpdateStatus struct { Remote *UpdateMetadata `json:"remote"` SystemUpdateAvailable bool `json:"systemUpdateAvailable"` AppUpdateAvailable bool `json:"appUpdateAvailable"` + + // for backwards compatibility + Error string `json:"error,omitempty"` } const UpdateMetadataUrl = "https://api.jetkvm.com/releases" var builtAppVersion = "0.1.0+dev" +func GetBuiltAppVersion() string { + return builtAppVersion +} + func GetLocalVersion() (systemVersion *semver.Version, appVersion *semver.Version, err error) { appVersion, err = semver.NewVersion(builtAppVersion) if err != nil { @@ -76,14 +86,21 @@ func fetchUpdateMetadata(ctx context.Context, deviceId string, includePreRelease query.Set("prerelease", fmt.Sprintf("%v", includePreRelease)) updateUrl.RawQuery = query.Encode() - logger.Infof("Checking for updates at: %s", updateUrl) + logger.Info().Str("url", updateUrl.String()).Msg("Checking for updates") req, err := http.NewRequestWithContext(ctx, "GET", updateUrl.String(), nil) if err != nil { return nil, fmt.Errorf("error creating request: %w", err) } - resp, err := http.DefaultClient.Do(req) + transport := http.DefaultTransport.(*http.Transport).Clone() + transport.Proxy = config.NetworkConfig.GetTransportProxyFunc() + + client := &http.Client{ + Transport: transport, + } + + resp, err := client.Do(req) if err != nil { return nil, fmt.Errorf("error sending request: %w", err) } @@ -126,7 +143,18 @@ func downloadFile(ctx context.Context, path string, url string, downloadProgress return fmt.Errorf("error creating request: %w", err) } - resp, err := http.DefaultClient.Do(req) + client := http.Client{ + Timeout: 10 * time.Minute, + Transport: &http.Transport{ + Proxy: config.NetworkConfig.GetTransportProxyFunc(), + TLSHandshakeTimeout: 30 * time.Second, + TLSClientConfig: &tls.Config{ + RootCAs: rootcerts.ServerCertPool(), + }, + }, + } + + resp, err := client.Do(req) if err != nil { return fmt.Errorf("error downloading file: %w", err) } @@ -185,7 +213,11 @@ func downloadFile(ctx context.Context, path string, url string, downloadProgress return nil } -func verifyFile(path string, expectedHash string, verifyProgress *float32) error { +func verifyFile(path string, expectedHash string, verifyProgress *float32, scopedLogger *zerolog.Logger) error { + if scopedLogger == nil { + scopedLogger = otaLogger + } + unverifiedPath := path + ".unverified" fileToHash, err := os.Open(unverifiedPath) if err != nil { @@ -229,7 +261,7 @@ func verifyFile(path string, expectedHash string, verifyProgress *float32) error } hashSum := hash.Sum(nil) - logger.Infof("SHA256 hash of %s: %x", path, hashSum) + scopedLogger.Info().Str("path", path).Str("hash", hex.EncodeToString(hashSum)).Msg("SHA256 hash of") if hex.EncodeToString(hashSum) != expectedHash { return fmt.Errorf("hash mismatch: %x != %s", hashSum, expectedHash) @@ -271,7 +303,7 @@ var otaState = OTAState{} func triggerOTAStateUpdate() { go func() { if currentSession == nil { - logger.Info("No active RPC session, skipping update state update") + logger.Info().Msg("No active RPC session, skipping update state update") return } writeJSONRPCEvent("otaState", otaState, currentSession) @@ -279,7 +311,12 @@ func triggerOTAStateUpdate() { } func TryUpdate(ctx context.Context, deviceId string, includePreRelease bool) error { - logger.Info("Trying to update...") + scopedLogger := otaLogger.With(). + Str("deviceId", deviceId). + Str("includePreRelease", fmt.Sprintf("%v", includePreRelease)). + Logger() + + scopedLogger.Info().Msg("Trying to update...") if otaState.Updating { return fmt.Errorf("update already in progress") } @@ -297,6 +334,7 @@ func TryUpdate(ctx context.Context, deviceId string, includePreRelease bool) err updateStatus, err := GetUpdateStatus(ctx, deviceId, includePreRelease) if err != nil { otaState.Error = fmt.Sprintf("Error checking for updates: %v", err) + scopedLogger.Error().Err(err).Msg("Error checking for updates") return fmt.Errorf("error checking for updates: %w", err) } @@ -314,11 +352,15 @@ func TryUpdate(ctx context.Context, deviceId string, includePreRelease bool) err rebootNeeded := false if appUpdateAvailable { - logger.Infof("App update available: %s -> %s", local.AppVersion, remote.AppVersion) + scopedLogger.Info(). + Str("local", local.AppVersion). + Str("remote", remote.AppVersion). + Msg("App update available") err := downloadFile(ctx, "/userdata/jetkvm/jetkvm_app.update", remote.AppUrl, &otaState.AppDownloadProgress) if err != nil { otaState.Error = fmt.Sprintf("Error downloading app update: %v", err) + scopedLogger.Error().Err(err).Msg("Error downloading app update") triggerOTAStateUpdate() return err } @@ -327,9 +369,15 @@ func TryUpdate(ctx context.Context, deviceId string, includePreRelease bool) err otaState.AppDownloadProgress = 1 triggerOTAStateUpdate() - err = verifyFile("/userdata/jetkvm/jetkvm_app.update", remote.AppHash, &otaState.AppVerificationProgress) + err = verifyFile( + "/userdata/jetkvm/jetkvm_app.update", + remote.AppHash, + &otaState.AppVerificationProgress, + &scopedLogger, + ) if err != nil { otaState.Error = fmt.Sprintf("Error verifying app update hash: %v", err) + scopedLogger.Error().Err(err).Msg("Error verifying app update hash") triggerOTAStateUpdate() return err } @@ -340,17 +388,22 @@ func TryUpdate(ctx context.Context, deviceId string, includePreRelease bool) err otaState.AppUpdateProgress = 1 triggerOTAStateUpdate() - logger.Info("App update downloaded") + scopedLogger.Info().Msg("App update downloaded") rebootNeeded = true } else { - logger.Info("App is up to date") + scopedLogger.Info().Msg("App is up to date") } if systemUpdateAvailable { - logger.Infof("System update available: %s -> %s", local.SystemVersion, remote.SystemVersion) + scopedLogger.Info(). + Str("local", local.SystemVersion). + Str("remote", remote.SystemVersion). + Msg("System update available") + err := downloadFile(ctx, "/userdata/jetkvm/update_system.tar", remote.SystemUrl, &otaState.SystemDownloadProgress) if err != nil { otaState.Error = fmt.Sprintf("Error downloading system update: %v", err) + scopedLogger.Error().Err(err).Msg("Error downloading system update") triggerOTAStateUpdate() return err } @@ -359,18 +412,25 @@ func TryUpdate(ctx context.Context, deviceId string, includePreRelease bool) err otaState.SystemDownloadProgress = 1 triggerOTAStateUpdate() - err = verifyFile("/userdata/jetkvm/update_system.tar", remote.SystemHash, &otaState.SystemVerificationProgress) + err = verifyFile( + "/userdata/jetkvm/update_system.tar", + remote.SystemHash, + &otaState.SystemVerificationProgress, + &scopedLogger, + ) if err != nil { otaState.Error = fmt.Sprintf("Error verifying system update hash: %v", err) + scopedLogger.Error().Err(err).Msg("Error verifying system update hash") triggerOTAStateUpdate() return err } - logger.Info("System update downloaded") + scopedLogger.Info().Msg("System update downloaded") verifyFinished := time.Now() otaState.SystemVerifiedAt = &verifyFinished otaState.SystemVerificationProgress = 1 triggerOTAStateUpdate() + scopedLogger.Info().Msg("Starting rk_ota command") cmd := exec.Command("rk_ota", "--misc=update", "--tar_path=/userdata/jetkvm/update_system.tar", "--save_dir=/userdata/jetkvm/ota_save", "--partition=all") var b bytes.Buffer cmd.Stdout = &b @@ -378,6 +438,7 @@ func TryUpdate(ctx context.Context, deviceId string, includePreRelease bool) err err = cmd.Start() if err != nil { otaState.Error = fmt.Sprintf("Error starting rk_ota command: %v", err) + scopedLogger.Error().Err(err).Msg("Error starting rk_ota command") return fmt.Errorf("error starting rk_ota command: %w", err) } ctx, cancel := context.WithCancel(context.Background()) @@ -409,25 +470,30 @@ func TryUpdate(ctx context.Context, deviceId string, includePreRelease bool) err output := b.String() if err != nil { otaState.Error = fmt.Sprintf("Error executing rk_ota command: %v\nOutput: %s", err, output) + scopedLogger.Error(). + Err(err). + Str("output", output). + Int("exitCode", cmd.ProcessState.ExitCode()). + Msg("Error executing rk_ota command") return fmt.Errorf("error executing rk_ota command: %w\nOutput: %s", err, output) } - - logger.Infof("rk_ota success, output: %s", output) + scopedLogger.Info().Str("output", output).Msg("rk_ota success") otaState.SystemUpdateProgress = 1 otaState.SystemUpdatedAt = &verifyFinished triggerOTAStateUpdate() rebootNeeded = true } else { - logger.Info("System is up to date") + scopedLogger.Info().Msg("System is up to date") } if rebootNeeded { - logger.Info("System Rebooting in 10s") + scopedLogger.Info().Msg("System Rebooting in 10s") time.Sleep(10 * time.Second) cmd := exec.Command("reboot") err := cmd.Start() if err != nil { otaState.Error = fmt.Sprintf("Failed to start reboot: %v", err) + scopedLogger.Error().Err(err).Msg("Failed to start reboot") return fmt.Errorf("failed to start reboot: %w", err) } else { os.Exit(0) @@ -438,52 +504,47 @@ func TryUpdate(ctx context.Context, deviceId string, includePreRelease bool) err } func GetUpdateStatus(ctx context.Context, deviceId string, includePreRelease bool) (*UpdateStatus, error) { + updateStatus := &UpdateStatus{} + // Get local versions systemVersionLocal, appVersionLocal, err := GetLocalVersion() if err != nil { - return nil, fmt.Errorf("error getting local version: %w", err) + return updateStatus, fmt.Errorf("error getting local version: %w", err) + } + updateStatus.Local = &LocalMetadata{ + AppVersion: appVersionLocal.String(), + SystemVersion: systemVersionLocal.String(), } // Get remote metadata remoteMetadata, err := fetchUpdateMetadata(ctx, deviceId, includePreRelease) if err != nil { - return nil, fmt.Errorf("error checking for updates: %w", err) - } - - // Build local UpdateMetadata - localMetadata := &LocalMetadata{ - AppVersion: appVersionLocal.String(), - SystemVersion: systemVersionLocal.String(), + return updateStatus, fmt.Errorf("error checking for updates: %w", err) } + updateStatus.Remote = remoteMetadata + // Get remote versions systemVersionRemote, err := semver.NewVersion(remoteMetadata.SystemVersion) if err != nil { - return nil, fmt.Errorf("error parsing remote system version: %w", err) + return updateStatus, fmt.Errorf("error parsing remote system version: %w", err) } appVersionRemote, err := semver.NewVersion(remoteMetadata.AppVersion) if err != nil { - return nil, fmt.Errorf("error parsing remote app version: %w, %s", err, remoteMetadata.AppVersion) + return updateStatus, fmt.Errorf("error parsing remote app version: %w, %s", err, remoteMetadata.AppVersion) } - systemUpdateAvailable := systemVersionRemote.GreaterThan(systemVersionLocal) - appUpdateAvailable := appVersionRemote.GreaterThan(appVersionLocal) + updateStatus.SystemUpdateAvailable = systemVersionRemote.GreaterThan(systemVersionLocal) + updateStatus.AppUpdateAvailable = appVersionRemote.GreaterThan(appVersionLocal) // Handle pre-release updates isRemoteSystemPreRelease := systemVersionRemote.Prerelease() != "" isRemoteAppPreRelease := appVersionRemote.Prerelease() != "" if isRemoteSystemPreRelease && !includePreRelease { - systemUpdateAvailable = false + updateStatus.SystemUpdateAvailable = false } if isRemoteAppPreRelease && !includePreRelease { - appUpdateAvailable = false - } - - updateStatus := &UpdateStatus{ - Local: localMetadata, - Remote: remoteMetadata, - SystemUpdateAvailable: systemUpdateAvailable, - AppUpdateAvailable: appUpdateAvailable, + updateStatus.AppUpdateAvailable = false } return updateStatus, nil @@ -497,6 +558,6 @@ func IsUpdatePending() bool { func confirmCurrentSystem() { output, err := exec.Command("rk_ota", "--misc=now").CombinedOutput() if err != nil { - logger.Warnf("failed to set current partition in A/B setup: %s", string(output)) + logger.Warn().Str("output", string(output)).Msg("failed to set current partition in A/B setup") } } diff --git a/prometheus.go b/prometheus.go index 8ebf259..5d4c5e7 100644 --- a/prometheus.go +++ b/prometheus.go @@ -1,15 +1,11 @@ package kvm import ( - "net/http" - "github.com/prometheus/client_golang/prometheus" versioncollector "github.com/prometheus/client_golang/prometheus/collectors/version" "github.com/prometheus/common/version" ) -var promHandler http.Handler - func initPrometheus() { // A Prometheus metrics endpoint. version.Version = builtAppVersion diff --git a/publish_source.sh b/publish_source.sh index 432d5cd..e5c133d 100755 --- a/publish_source.sh +++ b/publish_source.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash # Check if a commit message was provided if [ -z "$1" ]; then @@ -26,7 +26,7 @@ git checkout -b release-temp if git ls-remote --heads public main | grep -q 'refs/heads/main'; then git reset --soft public/main else - git reset --soft $(git rev-list --max-parents=0 HEAD) + git reset --soft "$(git rev-list --max-parents=0 HEAD)" fi # Merge changes from main diff --git a/remote_mount.go b/remote_mount.go index 5b10695..befffcb 100644 --- a/remote_mount.go +++ b/remote_mount.go @@ -44,7 +44,7 @@ func (w *WebRTCDiskReader) Read(ctx context.Context, offset int64, size int64) ( return nil, errors.New("not active session") } - logger.Debugf("reading from webrtc %v", string(jsonBytes)) + logger.Debug().Str("request", string(jsonBytes)).Msg("reading from webrtc") err = currentSession.DiskChannel.SendText(string(jsonBytes)) if err != nil { return nil, err diff --git a/resource/dev_test.sh b/resource/dev_test.sh new file mode 100644 index 0000000..0497801 --- /dev/null +++ b/resource/dev_test.sh @@ -0,0 +1,37 @@ +#!/bin/sh +JSON_OUTPUT=false +GET_COMMANDS=false +if [ "$1" = "-json" ]; then + JSON_OUTPUT=true + shift +fi +ADDITIONAL_ARGS=$@ +EXIT_CODE=0 + +runTest() { + PKG_ARGS="" + if [ "$2" != "" ]; then + PKG_ARGS="-p $2" + fi + if [ "$JSON_OUTPUT" = true ]; then + ./test2json $PKG_ARGS -t $1 -test.v $ADDITIONAL_ARGS | tee $1.result.json + if [ $? -ne 0 ]; then + EXIT_CODE=1 + fi + else + $@ + if [ $? -ne 0 ]; then + EXIT_CODE=1 + fi + fi +} + +function exit_with_code() { + if [ $EXIT_CODE -ne 0 ]; then + printf "\e[0;31m❌ Test failed\e[0m\n" + fi + + exit $EXIT_CODE +} + +trap exit_with_code EXIT diff --git a/resource/jetkvm_native b/resource/jetkvm_native index 0d0719c..a47288b 100644 Binary files a/resource/jetkvm_native and b/resource/jetkvm_native differ diff --git a/resource/jetkvm_native.sha256 b/resource/jetkvm_native.sha256 index 65da816..ceba8b2 100644 --- a/resource/jetkvm_native.sha256 +++ b/resource/jetkvm_native.sha256 @@ -1 +1 @@ -c0803a9185298398eff9a925de69bd0ca882cd5983b989a45b748648146475c6 +6dabd0e657dd099280d9173069687786a4a8c9c25cf7f9e7ce2f940cab67c521 diff --git a/serial.go b/serial.go index a4ab7d5..5439d13 100644 --- a/serial.go +++ b/serial.go @@ -35,17 +35,19 @@ var ( ) func runATXControl() { + scopedLogger := serialLogger.With().Str("service", "atx_control").Logger() + reader := bufio.NewReader(port) for { line, err := reader.ReadString('\n') if err != nil { - logger.Errorf("Error reading from serial port: %v", err) + scopedLogger.Warn().Err(err).Msg("Error reading from serial port") return } // Each line should be 4 binary digits + newline if len(line) != 5 { - logger.Warnf("Invalid line length: %d", len(line)) + scopedLogger.Warn().Int("length", len(line)).Msg("Invalid line length") continue } @@ -66,9 +68,12 @@ func runATXControl() { newLedPWRState != ledPWRState || newBtnRSTState != btnRSTState || newBtnPWRState != btnPWRState { - - logger.Debugf("Status changed: HDD LED: %v, PWR LED: %v, RST BTN: %v, PWR BTN: %v", - newLedHDDState, newLedPWRState, newBtnRSTState, newBtnPWRState) + scopedLogger.Debug(). + Bool("hdd", newLedHDDState). + Bool("pwr", newLedPWRState). + Bool("rst", newBtnRSTState). + Bool("pwr", newBtnPWRState). + Msg("Status changed") // Update states ledHDDState = newLedHDDState @@ -123,6 +128,7 @@ func pressATXResetButton(duration time.Duration) error { func mountDCControl() error { _ = port.SetMode(defaultMode) + registerDCMetrics() go runDCControl() return nil } @@ -135,45 +141,64 @@ func unmountDCControl() error { var dcState DCPowerState func runDCControl() { + scopedLogger := serialLogger.With().Str("service", "dc_control").Logger() reader := bufio.NewReader(port) + hasRestoreFeature := false for { line, err := reader.ReadString('\n') if err != nil { - logger.Errorf("Error reading from serial port: %v", err) + scopedLogger.Warn().Err(err).Msg("Error reading from serial port") return } // Split the line by semicolon parts := strings.Split(strings.TrimSpace(line), ";") - if len(parts) != 4 { - logger.Warnf("Invalid line: %s", line) + if len(parts) == 5 { + scopedLogger.Debug().Str("line", line).Msg("Detected DC extension with restore feature") + hasRestoreFeature = true + } else if len(parts) == 4 { + scopedLogger.Debug().Str("line", line).Msg("Detected DC extension without restore feature") + hasRestoreFeature = false + } else { + scopedLogger.Warn().Str("line", line).Msg("Invalid line") continue } // Parse new states powerState, err := strconv.Atoi(parts[0]) if err != nil { - logger.Warnf("Invalid power state: %v", err) + scopedLogger.Warn().Err(err).Msg("Invalid power state") continue } dcState.IsOn = powerState == 1 + if hasRestoreFeature { + restoreState, err := strconv.Atoi(parts[4]) + if err != nil { + scopedLogger.Warn().Err(err).Msg("Invalid restore state") + continue + } + dcState.RestoreState = restoreState + } else { + // -1 means not supported + dcState.RestoreState = -1 + } milliVolts, err := strconv.ParseFloat(parts[1], 64) if err != nil { - logger.Warnf("Invalid voltage: %v", err) + scopedLogger.Warn().Err(err).Msg("Invalid voltage") continue } volts := milliVolts / 1000 // Convert mV to V milliAmps, err := strconv.ParseFloat(parts[2], 64) if err != nil { - logger.Warnf("Invalid current: %v", err) + scopedLogger.Warn().Err(err).Msg("Invalid current") continue } amps := milliAmps / 1000 // Convert mA to A milliWatts, err := strconv.ParseFloat(parts[3], 64) if err != nil { - logger.Warnf("Invalid power: %v", err) + scopedLogger.Warn().Err(err).Msg("Invalid power") continue } watts := milliWatts / 1000 // Convert mW to W @@ -182,6 +207,9 @@ func runDCControl() { dcState.Current = amps dcState.Power = watts + // Update Prometheus metrics + updateDCMetrics(dcState) + if currentSession != nil { writeJSONRPCEvent("dcState", dcState, currentSession) } @@ -204,6 +232,25 @@ func setDCPowerState(on bool) error { return nil } +func setDCRestoreState(state int) error { + _, err := port.Write([]byte("\n")) + if err != nil { + return err + } + command := "RESTORE_MODE_OFF\n" + switch state { + case 1: + command = "RESTORE_MODE_ON\n" + case 2: + command = "RESTORE_MODE_LAST_STATE\n" + } + _, err = port.Write([]byte(command)) + if err != nil { + return err + } + return nil +} + var defaultMode = &serial.Mode{ BaudRate: 115200, DataBits: 8, @@ -213,9 +260,10 @@ var defaultMode = &serial.Mode{ func initSerialPort() { _ = reopenSerialPort() - if config.ActiveExtension == "atx-power" { + switch config.ActiveExtension { + case "atx-power": _ = mountATXControl() - } else if config.ActiveExtension == "dc-power" { + case "dc-power": _ = mountDCControl() } } @@ -227,12 +275,19 @@ func reopenSerialPort() error { var err error port, err = serial.Open(serialPortPath, defaultMode) if err != nil { - logger.Errorf("Error opening serial port: %v", err) + serialLogger.Error(). + Err(err). + Str("path", serialPortPath). + Interface("mode", defaultMode). + Msg("Error opening serial port") } return nil } func handleSerialChannel(d *webrtc.DataChannel) { + scopedLogger := serialLogger.With(). + Uint16("data_channel_id", *d.ID()).Logger() + d.OnOpen(func() { go func() { buf := make([]byte, 1024) @@ -240,13 +295,13 @@ func handleSerialChannel(d *webrtc.DataChannel) { n, err := port.Read(buf) if err != nil { if err != io.EOF { - logger.Errorf("Failed to read from serial port: %v", err) + scopedLogger.Warn().Err(err).Msg("Failed to read from serial port") } break } err = d.Send(buf[:n]) if err != nil { - logger.Errorf("Failed to send serial output: %v", err) + scopedLogger.Warn().Err(err).Msg("Failed to send serial output") break } } @@ -259,11 +314,15 @@ func handleSerialChannel(d *webrtc.DataChannel) { } _, err := port.Write(msg.Data) if err != nil { - logger.Errorf("Failed to write to serial: %v", err) + scopedLogger.Warn().Err(err).Msg("Failed to write to serial") } }) - d.OnClose(func() { + d.OnError(func(err error) { + scopedLogger.Warn().Err(err).Msg("Serial channel error") + }) + d.OnClose(func() { + scopedLogger.Info().Msg("Serial channel closed") }) } diff --git a/terminal.go b/terminal.go index 3e64020..e06e5cd 100644 --- a/terminal.go +++ b/terminal.go @@ -1,6 +1,7 @@ package kvm import ( + "bytes" "encoding/json" "io" "os" @@ -16,6 +17,9 @@ type TerminalSize struct { } func handleTerminalChannel(d *webrtc.DataChannel) { + scopedLogger := terminalLogger.With(). + Uint16("data_channel_id", *d.ID()).Logger() + var ptmx *os.File var cmd *exec.Cmd d.OnOpen(func() { @@ -23,7 +27,7 @@ func handleTerminalChannel(d *webrtc.DataChannel) { var err error ptmx, err = pty.Start(cmd) if err != nil { - logger.Errorf("Failed to start pty: %v", err) + scopedLogger.Warn().Err(err).Msg("Failed to start pty") d.Close() return } @@ -34,13 +38,13 @@ func handleTerminalChannel(d *webrtc.DataChannel) { n, err := ptmx.Read(buf) if err != nil { if err != io.EOF { - logger.Errorf("Failed to read from pty: %v", err) + scopedLogger.Warn().Err(err).Msg("Failed to read from pty") } break } err = d.Send(buf[:n]) if err != nil { - logger.Errorf("Failed to send pty output: %v", err) + scopedLogger.Warn().Err(err).Msg("Failed to send pty output") break } } @@ -52,22 +56,27 @@ func handleTerminalChannel(d *webrtc.DataChannel) { return } if msg.IsString { - var size TerminalSize - err := json.Unmarshal([]byte(msg.Data), &size) - if err == nil { - err = pty.Setsize(ptmx, &pty.Winsize{ - Rows: uint16(size.Rows), - Cols: uint16(size.Cols), - }) + maybeJson := bytes.TrimSpace(msg.Data) + // Cheap check to see if this resembles JSON + if len(maybeJson) > 1 && maybeJson[0] == '{' && maybeJson[len(maybeJson)-1] == '}' { + var size TerminalSize + err := json.Unmarshal(maybeJson, &size) if err == nil { - return + err = pty.Setsize(ptmx, &pty.Winsize{ + Rows: uint16(size.Rows), + Cols: uint16(size.Cols), + }) + if err == nil { + scopedLogger.Info().Int("rows", size.Rows).Int("cols", size.Cols).Msg("Set terminal size") + return + } } + scopedLogger.Warn().Err(err).Msg("Failed to parse terminal size") } - logger.Errorf("Failed to parse terminal size: %v", err) } _, err := ptmx.Write(msg.Data) if err != nil { - logger.Errorf("Failed to write to pty: %v", err) + scopedLogger.Warn().Err(err).Msg("Failed to write to pty") } }) @@ -78,5 +87,10 @@ func handleTerminalChannel(d *webrtc.DataChannel) { if cmd != nil && cmd.Process != nil { _ = cmd.Process.Kill() } + scopedLogger.Info().Msg("Terminal channel closed") + }) + + d.OnError(func(err error) { + scopedLogger.Warn().Err(err).Msg("Terminal channel error") }) } diff --git a/timesync.go b/timesync.go new file mode 100644 index 0000000..7b25fe2 --- /dev/null +++ b/timesync.go @@ -0,0 +1,53 @@ +package kvm + +import ( + "strconv" + "time" + + "github.com/jetkvm/kvm/internal/timesync" +) + +var ( + timeSync *timesync.TimeSync + builtTimestamp string +) + +func isTimeSyncNeeded() bool { + if builtTimestamp == "" { + timesyncLogger.Warn().Msg("built timestamp is not set, time sync is needed") + return true + } + + ts, err := strconv.Atoi(builtTimestamp) + if err != nil { + timesyncLogger.Warn().Str("error", err.Error()).Msg("failed to parse built timestamp") + return true + } + + // builtTimestamp is UNIX timestamp in seconds + builtTime := time.Unix(int64(ts), 0) + now := time.Now() + + if now.Sub(builtTime) < 0 { + timesyncLogger.Warn(). + Str("built_time", builtTime.Format(time.RFC3339)). + Str("now", now.Format(time.RFC3339)). + Msg("system time is behind the built time, time sync is needed") + return true + } + + return false +} + +func initTimeSync() { + timeSync = timesync.NewTimeSync(×ync.TimeSyncOptions{ + Logger: timesyncLogger, + NetworkConfig: config.NetworkConfig, + PreCheckFunc: func() (bool, error) { + if !networkState.IsOnline() { + return false, nil + } + return true, nil + }, + }) +} diff --git a/ui/.eslintrc.cjs b/ui/.eslintrc.cjs deleted file mode 100644 index 568fbd9..0000000 --- a/ui/.eslintrc.cjs +++ /dev/null @@ -1,66 +0,0 @@ -module.exports = { - root: true, - env: { browser: true, es2020: true }, - extends: [ - "eslint:recommended", - "plugin:@typescript-eslint/recommended", - "plugin:@typescript-eslint/stylistic", - "plugin:react-hooks/recommended", - "plugin:react/recommended", - "plugin:react/jsx-runtime", - "plugin:import/recommended", - "prettier", - ], - ignorePatterns: ["dist", ".eslintrc.cjs", "tailwind.config.js", "postcss.config.js"], - parser: "@typescript-eslint/parser", - plugins: ["react-refresh"], - parserOptions: { - ecmaVersion: "latest", - sourceType: "module", - project: ["./tsconfig.json", "./tsconfig.node.json"], - tsconfigRootDir: __dirname, - }, - rules: { - "react-refresh/only-export-components": ["warn", { allowConstantExport: true }], - "import/order": [ - "error", - { - /** - * @description - * - * This keeps imports separate from one another, ensuring that imports are separated - * by their relative groups. As you move through the groups, imports become closer - * to the current file. - * - * @example - * ``` - * import fs from 'fs'; - * - * import package from 'npm-package'; - * - * import xyz from '~/project-file'; - * - * import index from '../'; - * - * import sibling from './foo'; - * ``` - */ - groups: ["builtin", "external", "internal", "parent", "sibling"], - "newlines-between": "always", - }, - ], - }, - settings: { - "import/resolver": { - alias: { - map: [ - ["@components", "./src/components"], - ["@routes", "./src/routes"], - ["@assets", "./src/assets"], - ["@", "./src"], - ], - extensions: [".ts", ".tsx", ".js", ".jsx", ".json"], - }, - }, - }, -}; diff --git a/ui/.prettierrc b/ui/.prettierrc index 65b362d..43fe6c4 100644 --- a/ui/.prettierrc +++ b/ui/.prettierrc @@ -6,6 +6,7 @@ "arrowParens": "avoid", "singleQuote": false, "plugins": ["prettier-plugin-tailwindcss"], - "tailwindFunctions": ["clsx"], - "printWidth": 90 + "tailwindFunctions": ["clsx", "cx"], + "printWidth": 90, + "tailwindStylesheet": "./src/index.css" } diff --git a/ui/dev_device.sh b/ui/dev_device.sh index 092b8c8..2c7b497 100755 --- a/ui/dev_device.sh +++ b/ui/dev_device.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash # Check if an IP address was provided as an argument if [ -z "$1" ]; then @@ -15,5 +15,15 @@ echo "└─────────────────────── # Set the environment variable and run Vite echo "Starting development server with JetKVM device at: $ip_address" + +# Check if pwd is the current directory of the script +if [ "$(pwd)" != "$(dirname "$0")" ]; then + pushd "$(dirname "$0")" > /dev/null + echo "Changed directory to: $(pwd)" +fi + sleep 1 -JETKVM_PROXY_URL="http://$ip_address" npx vite dev --mode=device + +JETKVM_PROXY_URL="ws://$ip_address" npx vite dev --mode=device + +popd > /dev/null diff --git a/ui/eslint.config.cjs b/ui/eslint.config.cjs new file mode 100644 index 0000000..a6c0c1f --- /dev/null +++ b/ui/eslint.config.cjs @@ -0,0 +1,93 @@ +const { + defineConfig, + globalIgnores, +} = require("eslint/config"); + +const globals = require("globals"); + +const { + fixupConfigRules, +} = require("@eslint/compat"); + +const tsParser = require("@typescript-eslint/parser"); +const reactRefresh = require("eslint-plugin-react-refresh"); +const js = require("@eslint/js"); + +const { + FlatCompat, +} = require("@eslint/eslintrc"); + +const compat = new FlatCompat({ + baseDirectory: __dirname, + recommendedConfig: js.configs.recommended, + allConfig: js.configs.all +}); + +module.exports = defineConfig([{ + languageOptions: { + globals: { + ...globals.browser, + }, + + parser: tsParser, + ecmaVersion: "latest", + sourceType: "module", + + parserOptions: { + project: ["./tsconfig.json", "./tsconfig.node.json"], + tsconfigRootDir: __dirname, + ecmaFeatures: { + jsx: true + } + }, + }, + + extends: fixupConfigRules(compat.extends( + "eslint:recommended", + "plugin:@typescript-eslint/recommended", + "plugin:@typescript-eslint/stylistic", + "plugin:react-hooks/recommended", + "plugin:react/recommended", + "plugin:react/jsx-runtime", + "plugin:import/recommended", + "prettier", + )), + + plugins: { + "react-refresh": reactRefresh, + }, + + rules: { + "react-refresh/only-export-components": ["warn", { + allowConstantExport: true, + }], + + "import/order": ["error", { + groups: ["builtin", "external", "internal", "parent", "sibling"], + "newlines-between": "always", + }], + }, + + settings: { + "react": { + "version": "detect" + }, + "import/resolver": { + alias: { + map: [ + ["@components", "./src/components"], + ["@routes", "./src/routes"], + ["@assets", "./src/assets"], + ["@", "./src"], + ], + + extensions: [".ts", ".tsx", ".js", ".jsx", ".json"], + }, + }, + }, +}, globalIgnores([ + "**/dist", + "**/.eslintrc.cjs", + "**/tailwind.config.js", + "**/postcss.config.js", +])]); diff --git a/ui/package-lock.json b/ui/package-lock.json index e9caa20..72a4849 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -1,84 +1,86 @@ { "name": "kvm-ui", - "version": "0.0.0", + "version": "2025.08.07.001", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "kvm-ui", - "version": "0.0.0", + "version": "2025.08.07.001", "dependencies": { - "@headlessui/react": "^2.2.0", - "@headlessui/tailwindcss": "^0.2.1", + "@headlessui/react": "^2.2.7", + "@headlessui/tailwindcss": "^0.2.2", "@heroicons/react": "^2.2.0", + "@vitejs/plugin-basic-ssl": "^2.1.0", "@xterm/addon-clipboard": "^0.1.0", "@xterm/addon-fit": "^0.10.0", "@xterm/addon-unicode11": "^0.8.0", "@xterm/addon-web-links": "^0.11.0", "@xterm/addon-webgl": "^0.18.0", "@xterm/xterm": "^5.5.0", - "cva": "^1.0.0-beta.1", + "cva": "^1.0.0-beta.4", + "dayjs": "^1.11.13", "eslint-import-resolver-alias": "^1.1.2", - "focus-trap-react": "^10.2.3", - "framer-motion": "^11.15.0", + "focus-trap-react": "^11.0.4", + "framer-motion": "^12.23.12", "lodash.throttle": "^4.1.1", "mini-svg-data-uri": "^1.4.4", - "react": "^18.2.0", + "react": "^19.1.1", "react-animate-height": "^3.2.3", - "react-dom": "^18.2.0", - "react-hot-toast": "^2.4.1", - "react-icons": "^5.4.0", + "react-dom": "^19.1.1", + "react-hot-toast": "^2.5.2", + "react-icons": "^5.5.0", "react-router-dom": "^6.22.3", - "react-simple-keyboard": "^3.7.112", - "react-xtermjs": "^1.0.9", - "recharts": "^2.15.0", - "tailwind-merge": "^2.5.5", - "usehooks-ts": "^3.1.0", - "validator": "^13.12.0", - "xterm": "^5.3.0", + "react-simple-keyboard": "^3.8.106", + "react-use-websocket": "^4.13.0", + "react-xtermjs": "^1.0.10", + "recharts": "^2.15.3", + "tailwind-merge": "^3.3.1", + "usehooks-ts": "^3.1.1", + "validator": "^13.15.15", "zustand": "^4.5.2" }, "devDependencies": { - "@tailwindcss/forms": "^0.5.9", - "@tailwindcss/typography": "^0.5.15", - "@types/react": "^18.2.66", - "@types/react-dom": "^18.3.0", - "@types/semver": "^7.5.8", - "@types/validator": "^13.12.2", - "@typescript-eslint/eslint-plugin": "^8.25.0", - "@typescript-eslint/parser": "^8.25.0", - "@vitejs/plugin-react-swc": "^3.7.2", - "autoprefixer": "^10.4.20", - "eslint": "^8.20.0", - "eslint-config-prettier": "^10.0.1", - "eslint-plugin-import": "^2.31.0", - "eslint-plugin-react": "^7.37.4", - "eslint-plugin-react-hooks": "^5.1.0", - "eslint-plugin-react-refresh": "^0.4.19", - "postcss": "^8.4.49", - "prettier": "^3.4.2", - "prettier-plugin-tailwindcss": "^0.6.11", - "tailwindcss": "^3.4.17", - "typescript": "^5.7.2", - "vite": "^5.2.0", + "@eslint/compat": "^1.3.1", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "^9.32.0", + "@tailwindcss/forms": "^0.5.10", + "@tailwindcss/postcss": "^4.1.11", + "@tailwindcss/typography": "^0.5.16", + "@tailwindcss/vite": "^4.1.11", + "@types/react": "^19.1.9", + "@types/react-dom": "^19.1.7", + "@types/semver": "^7.7.0", + "@types/validator": "^13.15.2", + "@typescript-eslint/eslint-plugin": "^8.39.0", + "@typescript-eslint/parser": "^8.39.0", + "@vitejs/plugin-react-swc": "^3.10.2", + "autoprefixer": "^10.4.21", + "eslint": "^9.32.0", + "eslint-config-prettier": "^10.1.8", + "eslint-plugin-import": "^2.32.0", + "eslint-plugin-react": "^7.37.5", + "eslint-plugin-react-hooks": "^5.2.0", + "eslint-plugin-react-refresh": "^0.4.20", + "globals": "^16.3.0", + "postcss": "^8.5.6", + "prettier": "^3.6.2", + "prettier-plugin-tailwindcss": "^0.6.14", + "tailwindcss": "^4.1.11", + "typescript": "^5.9.2", + "vite": "^6.3.5", "vite-tsconfig-paths": "^5.1.4" }, "engines": { - "node": "21.1.0" - } - }, - "node_modules/@aashutoshrathi/word-wrap": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz", - "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==", - "engines": { - "node": ">=0.10.0" + "node": "22.15.0" } }, "node_modules/@alloc/quick-lru": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "dev": true, + "license": "MIT", "engines": { "node": ">=10" }, @@ -86,416 +88,535 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@babel/runtime": { - "version": "7.26.9", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.9.tgz", - "integrity": "sha512-aA63XwOkcl4xxQa3HjPMqOP6LiK0ZDv3mUPYEFXkpHbaFjtGggE1A61FjFzJnB+p7/oy2gA8E+rcBNl/zC1tMg==", + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "license": "Apache-2.0", "dependencies": { - "regenerator-runtime": "^0.14.0" + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.28.2", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.2.tgz", + "integrity": "sha512-KHp2IflsnGywDjBWDkR9iEqiWSpc8GIi0lgTT3mOElT0PP1tG26P4tmFI2YvAdzgq9RGyoHZQEIEdZy6Ec5xCA==", + "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz", - "integrity": "sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.8.tgz", + "integrity": "sha512-urAvrUedIqEiFR3FYSLTWQgLu5tb+m0qZw0NBEasUeo6wuqatkMDaRT+1uABiGXEu5vqgPd7FGE1BhsAIy9QVA==", "cpu": [ "ppc64" ], - "dev": true, + "license": "MIT", "optional": true, "os": [ "aix" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/android-arm": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.20.2.tgz", - "integrity": "sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.8.tgz", + "integrity": "sha512-RONsAvGCz5oWyePVnLdZY/HHwA++nxYWIX1atInlaW6SEkwq6XkP3+cb825EUcRs5Vss/lGh/2YxAb5xqc07Uw==", "cpu": [ "arm" ], - "dev": true, + "license": "MIT", "optional": true, "os": [ "android" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/android-arm64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.20.2.tgz", - "integrity": "sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.8.tgz", + "integrity": "sha512-OD3p7LYzWpLhZEyATcTSJ67qB5D+20vbtr6vHlHWSQYhKtzUYrETuWThmzFpZtFsBIxRvhO07+UgVA9m0i/O1w==", "cpu": [ "arm64" ], - "dev": true, + "license": "MIT", "optional": true, "os": [ "android" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/android-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.20.2.tgz", - "integrity": "sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.8.tgz", + "integrity": "sha512-yJAVPklM5+4+9dTeKwHOaA+LQkmrKFX96BM0A/2zQrbS6ENCmxc4OVoBs5dPkCCak2roAD+jKCdnmOqKszPkjA==", "cpu": [ "x64" ], - "dev": true, + "license": "MIT", "optional": true, "os": [ "android" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.20.2.tgz", - "integrity": "sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.8.tgz", + "integrity": "sha512-Jw0mxgIaYX6R8ODrdkLLPwBqHTtYHJSmzzd+QeytSugzQ0Vg4c5rDky5VgkoowbZQahCbsv1rT1KW72MPIkevw==", "cpu": [ "arm64" ], - "dev": true, + "license": "MIT", "optional": true, "os": [ "darwin" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.20.2.tgz", - "integrity": "sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.8.tgz", + "integrity": "sha512-Vh2gLxxHnuoQ+GjPNvDSDRpoBCUzY4Pu0kBqMBDlK4fuWbKgGtmDIeEC081xi26PPjn+1tct+Bh8FjyLlw1Zlg==", "cpu": [ "x64" ], - "dev": true, + "license": "MIT", "optional": true, "os": [ "darwin" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.20.2.tgz", - "integrity": "sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.8.tgz", + "integrity": "sha512-YPJ7hDQ9DnNe5vxOm6jaie9QsTwcKedPvizTVlqWG9GBSq+BuyWEDazlGaDTC5NGU4QJd666V0yqCBL2oWKPfA==", "cpu": [ "arm64" ], - "dev": true, + "license": "MIT", "optional": true, "os": [ "freebsd" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.20.2.tgz", - "integrity": "sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.8.tgz", + "integrity": "sha512-MmaEXxQRdXNFsRN/KcIimLnSJrk2r5H8v+WVafRWz5xdSVmWLoITZQXcgehI2ZE6gioE6HirAEToM/RvFBeuhw==", "cpu": [ "x64" ], - "dev": true, + "license": "MIT", "optional": true, "os": [ "freebsd" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-arm": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.20.2.tgz", - "integrity": "sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.8.tgz", + "integrity": "sha512-FuzEP9BixzZohl1kLf76KEVOsxtIBFwCaLupVuk4eFVnOZfU+Wsn+x5Ryam7nILV2pkq2TqQM9EZPsOBuMC+kg==", "cpu": [ "arm" ], - "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.20.2.tgz", - "integrity": "sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.8.tgz", + "integrity": "sha512-WIgg00ARWv/uYLU7lsuDK00d/hHSfES5BzdWAdAig1ioV5kaFNrtK8EqGcUBJhYqotlUByUKz5Qo6u8tt7iD/w==", "cpu": [ "arm64" ], - "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.20.2.tgz", - "integrity": "sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.8.tgz", + "integrity": "sha512-A1D9YzRX1i+1AJZuFFUMP1E9fMaYY+GnSQil9Tlw05utlE86EKTUA7RjwHDkEitmLYiFsRd9HwKBPEftNdBfjg==", "cpu": [ "ia32" ], - "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.20.2.tgz", - "integrity": "sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.8.tgz", + "integrity": "sha512-O7k1J/dwHkY1RMVvglFHl1HzutGEFFZ3kNiDMSOyUrB7WcoHGf96Sh+64nTRT26l3GMbCW01Ekh/ThKM5iI7hQ==", "cpu": [ "loong64" ], - "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.20.2.tgz", - "integrity": "sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.8.tgz", + "integrity": "sha512-uv+dqfRazte3BzfMp8PAQXmdGHQt2oC/y2ovwpTteqrMx2lwaksiFZ/bdkXJC19ttTvNXBuWH53zy/aTj1FgGw==", "cpu": [ "mips64el" ], - "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.20.2.tgz", - "integrity": "sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.8.tgz", + "integrity": "sha512-GyG0KcMi1GBavP5JgAkkstMGyMholMDybAf8wF5A70CALlDM2p/f7YFE7H92eDeH/VBtFJA5MT4nRPDGg4JuzQ==", "cpu": [ "ppc64" ], - "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.20.2.tgz", - "integrity": "sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.8.tgz", + "integrity": "sha512-rAqDYFv3yzMrq7GIcen3XP7TUEG/4LK86LUPMIz6RT8A6pRIDn0sDcvjudVZBiiTcZCY9y2SgYX2lgK3AF+1eg==", "cpu": [ "riscv64" ], - "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.20.2.tgz", - "integrity": "sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.8.tgz", + "integrity": "sha512-Xutvh6VjlbcHpsIIbwY8GVRbwoviWT19tFhgdA7DlenLGC/mbc3lBoVb7jxj9Z+eyGqvcnSyIltYUrkKzWqSvg==", "cpu": [ "s390x" ], - "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.20.2.tgz", - "integrity": "sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.8.tgz", + "integrity": "sha512-ASFQhgY4ElXh3nDcOMTkQero4b1lgubskNlhIfJrsH5OKZXDpUAKBlNS0Kx81jwOBp+HCeZqmoJuihTv57/jvQ==", "cpu": [ "x64" ], - "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.20.2.tgz", - "integrity": "sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ==", + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.8.tgz", + "integrity": "sha512-d1KfruIeohqAi6SA+gENMuObDbEjn22olAR7egqnkCD9DGBG0wsEARotkLgXDu6c4ncgWTZJtN5vcgxzWRMzcw==", "cpu": [ - "x64" + "arm64" ], - "dev": true, + "license": "MIT", "optional": true, "os": [ "netbsd" ], "engines": { - "node": ">=12" + "node": ">=18" } }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.20.2.tgz", - "integrity": "sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ==", + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.8.tgz", + "integrity": "sha512-nVDCkrvx2ua+XQNyfrujIG38+YGyuy2Ru9kKVNyh5jAys6n+l44tTtToqHjino2My8VAY6Lw9H7RI73XFi66Cg==", "cpu": [ "x64" ], - "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.8.tgz", + "integrity": "sha512-j8HgrDuSJFAujkivSMSfPQSAa5Fxbvk4rgNAS5i3K+r8s1X0p1uOO2Hl2xNsGFppOeHOLAVgYwDVlmxhq5h+SQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", "optional": true, "os": [ "openbsd" ], "engines": { - "node": ">=12" + "node": ">=18" } }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.20.2.tgz", - "integrity": "sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w==", + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.8.tgz", + "integrity": "sha512-1h8MUAwa0VhNCDp6Af0HToI2TJFAn1uqT9Al6DJVzdIBAd21m/G0Yfc77KDM3uF3T/YaOgQq3qTJHPbTOInaIQ==", "cpu": [ "x64" ], - "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.8.tgz", + "integrity": "sha512-r2nVa5SIK9tSWd0kJd9HCffnDHKchTGikb//9c7HX+r+wHYCpQrSgxhlY6KWV1nFo1l4KFbsMlHk+L6fekLsUg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.8.tgz", + "integrity": "sha512-zUlaP2S12YhQ2UzUfcCuMDHQFJyKABkAjvO5YSndMiIkMimPmxA+BYSBikWgsRpvyxuRnow4nS5NPnf9fpv41w==", + "cpu": [ + "x64" + ], + "license": "MIT", "optional": true, "os": [ "sunos" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.20.2.tgz", - "integrity": "sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.8.tgz", + "integrity": "sha512-YEGFFWESlPva8hGL+zvj2z/SaK+pH0SwOM0Nc/d+rVnW7GSTFlLBGzZkuSU9kFIGIo8q9X3ucpZhu8PDN5A2sQ==", "cpu": [ "arm64" ], - "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.20.2.tgz", - "integrity": "sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.8.tgz", + "integrity": "sha512-hiGgGC6KZ5LZz58OL/+qVVoZiuZlUYlYHNAmczOm7bs2oE1XriPFi5ZHHrS8ACpV5EjySrnoCKmcbQMN+ojnHg==", "cpu": [ "ia32" ], - "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/win32-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.20.2.tgz", - "integrity": "sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.8.tgz", + "integrity": "sha512-cn3Yr7+OaaZq1c+2pe+8yxC8E144SReCQjN6/2ynubzYjvyqZjTXfQJpAcQpsdJq3My7XADANiYGHoFC69pLQw==", "cpu": [ "x64" ], - "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@eslint-community/eslint-utils": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", - "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz", + "integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==", + "license": "MIT", "dependencies": { - "eslint-visitor-keys": "^3.3.0" + "eslint-visitor-keys": "^3.4.3" }, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, + "funding": { + "url": "https://opencollective.com/eslint" + }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "node_modules/@eslint-community/regexpp": { - "version": "4.10.0", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.10.0.tgz", - "integrity": "sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==", + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", + "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", + "license": "MIT", "engines": { "node": "^12.0.0 || ^14.0.0 || >=16.0.0" } }, + "node_modules/@eslint/compat": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@eslint/compat/-/compat-1.3.1.tgz", + "integrity": "sha512-k8MHony59I5EPic6EQTCNOuPoVBnoYXkP+20xvwFjN7t0qI3ImyvyBgg+hIVPwC8JaxVjjUZld+cLfBLFDLucg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "peerDependencies": { + "eslint": "^8.40 || 9" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.0", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.0.tgz", + "integrity": "sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==", + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.6", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.3.0.tgz", + "integrity": "sha512-ViuymvFmcJi04qdZeDc2whTHryouGcDlaxPqarTD0ZE10ISpxGUVZGZDx4w01upyIynL3iu6IXH2bS1NhclQMw==", + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.15.1", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.1.tgz", + "integrity": "sha512-bkOp+iumZCCbt1K1CmWf0R9pM5yKpDv+ZXtvSyQpudrI9kuFLp+bM2WOPXImuD/ceQuaa8f5pj93Y7zyECIGNA==", + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, "node_modules/@eslint/eslintrc": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", - "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", + "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", + "license": "MIT", "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", - "espree": "^9.6.0", - "globals": "^13.19.0", + "espree": "^10.0.1", + "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.0", @@ -503,64 +624,85 @@ "strip-json-comments": "^3.1.1" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "url": "https://opencollective.com/eslint" } }, - "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/@eslint/eslintrc/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dependencies": { - "brace-expansion": "^1.1.7" - }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "license": "MIT", "engines": { - "node": "*" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/@eslint/js": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz", - "integrity": "sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==", + "version": "9.32.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.32.0.tgz", + "integrity": "sha512-BBpRFZK3eX6uMLKz8WxFOBIFFcGFJ/g8XuwjTHCqHROSIsopI+ddn/d5Cfh36+7+e5edVS8dbSHnBNhrLEX0zg==", + "license": "MIT", "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz", + "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.4.tgz", + "integrity": "sha512-Ul5l+lHEcw3L5+k8POx6r74mxEYKG5kOb6Xpy2gCRW6zweT6TEhAf8vhxGgjhqrd/VO/Dirhsb+1hNpD1ue9hw==", + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.15.1", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, "node_modules/@floating-ui/core": { - "version": "1.6.7", - "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.7.tgz", - "integrity": "sha512-yDzVT/Lm101nQ5TCVeK65LtdN7Tj4Qpr9RTXJ2vPFLqtLxwOrpoxAHAJI8J3yYWUc40J0BDBheaitK5SJmno2g==", + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.3.tgz", + "integrity": "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==", + "license": "MIT", "dependencies": { - "@floating-ui/utils": "^0.2.7" + "@floating-ui/utils": "^0.2.10" } }, "node_modules/@floating-ui/dom": { - "version": "1.6.10", - "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.10.tgz", - "integrity": "sha512-fskgCFv8J8OamCmyun8MfjB1Olfn+uZKjOKZ0vhYF3gRmEUXcGOjxWL8bBr7i4kIuPZ2KD2S3EUIOxnjC8kl2A==", + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.3.tgz", + "integrity": "sha512-uZA413QEpNuhtb3/iIKoYMSK07keHPYeXF02Zhd6e213j+d1NamLix/mCLxBUDW/Gx52sPH2m+chlUsyaBs/Ag==", + "license": "MIT", "dependencies": { - "@floating-ui/core": "^1.6.0", - "@floating-ui/utils": "^0.2.7" + "@floating-ui/core": "^1.7.3", + "@floating-ui/utils": "^0.2.10" } }, "node_modules/@floating-ui/react": { - "version": "0.26.23", - "resolved": "https://registry.npmjs.org/@floating-ui/react/-/react-0.26.23.tgz", - "integrity": "sha512-9u3i62fV0CFF3nIegiWiRDwOs7OW/KhSUJDNx2MkQM3LbE5zQOY01sL3nelcVBXvX7Ovvo3A49I8ql+20Wg/Hw==", + "version": "0.26.28", + "resolved": "https://registry.npmjs.org/@floating-ui/react/-/react-0.26.28.tgz", + "integrity": "sha512-yORQuuAtVpiRjpMhdc0wJj06b9JFjrYF4qp96j++v2NBpbi6SEGF7donUJ3TMieerQ6qVkAv1tgr7L4r5roTqw==", + "license": "MIT", "dependencies": { - "@floating-ui/react-dom": "^2.1.1", - "@floating-ui/utils": "^0.2.7", + "@floating-ui/react-dom": "^2.1.2", + "@floating-ui/utils": "^0.2.8", "tabbable": "^6.0.0" }, "peerDependencies": { @@ -569,11 +711,12 @@ } }, "node_modules/@floating-ui/react-dom": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.1.tgz", - "integrity": "sha512-4h84MJt3CHrtG18mGsXuLCHMrug49d7DFkU0RMIyshRveBeyV2hmV/pDaF2Uxtu8kgq5r46llp5E5FQiR0K2Yg==", + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.5.tgz", + "integrity": "sha512-HDO/1/1oH9fjj4eLgegrlH3dklZpHtUYYFiVwMUwfGvk9jWDRWqkklA2/NFScknrcNSspbV868WjXORvreDX+Q==", + "license": "MIT", "dependencies": { - "@floating-ui/dom": "^1.0.0" + "@floating-ui/dom": "^1.7.3" }, "peerDependencies": { "react": ">=16.8.0", @@ -581,19 +724,22 @@ } }, "node_modules/@floating-ui/utils": { - "version": "0.2.7", - "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.7.tgz", - "integrity": "sha512-X8R8Oj771YRl/w+c1HqAC1szL8zWQRwFvgDwT129k9ACdBoud/+/rX9V0qiMl6LWUdP9voC2nDVZYPMQQsb6eA==" + "version": "0.2.10", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz", + "integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==", + "license": "MIT" }, "node_modules/@headlessui/react": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@headlessui/react/-/react-2.2.0.tgz", - "integrity": "sha512-RzCEg+LXsuI7mHiSomsu/gBJSjpupm6A1qIZ5sWjd7JhARNlMiSA4kKfJpCKwU9tE+zMRterhhrP74PvfJrpXQ==", + "version": "2.2.7", + "resolved": "https://registry.npmjs.org/@headlessui/react/-/react-2.2.7.tgz", + "integrity": "sha512-WKdTymY8Y49H8/gUc/lIyYK1M+/6dq0Iywh4zTZVAaiTDprRfioxSgD0wnXTQTBpjpGJuTL1NO/mqEvc//5SSg==", + "license": "MIT", "dependencies": { "@floating-ui/react": "^0.26.16", - "@react-aria/focus": "^3.17.1", - "@react-aria/interactions": "^3.21.3", - "@tanstack/react-virtual": "^3.8.1" + "@react-aria/focus": "^3.20.2", + "@react-aria/interactions": "^3.25.0", + "@tanstack/react-virtual": "^3.13.9", + "use-sync-external-store": "^1.5.0" }, "engines": { "node": ">=10" @@ -607,6 +753,7 @@ "version": "0.2.2", "resolved": "https://registry.npmjs.org/@headlessui/tailwindcss/-/tailwindcss-0.2.2.tgz", "integrity": "sha512-xNe42KjdyA4kfUKLLPGzME9zkH7Q3rOZ5huFihWNWOQFxnItxPB3/67yBI8/qBfY8nwBRx5GHn4VprsoluVMGw==", + "license": "MIT", "engines": { "node": ">=10" }, @@ -618,47 +765,51 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/@heroicons/react/-/react-2.2.0.tgz", "integrity": "sha512-LMcepvRaS9LYHJGsF0zzmgKCUim/X3N/DQKc4jepAXJ7l8QxJ1PmxJzqplF2Z3FE4PqBAIGyJAQ/w4B5dsqbtQ==", + "license": "MIT", "peerDependencies": { "react": ">= 16 || ^19.0.0-rc" } }, - "node_modules/@humanwhocodes/config-array": { - "version": "0.11.14", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", - "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==", - "dependencies": { - "@humanwhocodes/object-schema": "^2.0.2", - "debug": "^4.3.1", - "minimatch": "^3.0.5" - }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "license": "Apache-2.0", "engines": { - "node": ">=10.10.0" + "node": ">=18.18.0" } }, - "node_modules/@humanwhocodes/config-array/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "node_modules/@humanfs/node": { + "version": "0.16.6", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz", + "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==", + "license": "Apache-2.0", "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/@humanwhocodes/config-array/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dependencies": { - "brace-expansion": "^1.1.7" + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.3.0" }, "engines": { - "node": "*" + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node/node_modules/@humanwhocodes/retry": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz", + "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" } }, "node_modules/@humanwhocodes/module-importer": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "license": "Apache-2.0", "engines": { "node": ">=12.22" }, @@ -667,90 +818,66 @@ "url": "https://github.com/sponsors/nzakas" } }, - "node_modules/@humanwhocodes/object-schema": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", - "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==" - }, - "node_modules/@isaacs/cliui": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", - "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", - "dependencies": { - "string-width": "^5.1.2", - "string-width-cjs": "npm:string-width@^4.2.0", - "strip-ansi": "^7.0.1", - "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", - "wrap-ansi": "^8.1.0", - "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" - }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "license": "Apache-2.0", "engines": { - "node": ">=12" - } - }, - "node_modules/@isaacs/cliui/node_modules/ansi-regex": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", - "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", - "engines": { - "node": ">=12" + "node": ">=18.18" }, "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" + "type": "github", + "url": "https://github.com/sponsors/nzakas" } }, - "node_modules/@isaacs/cliui/node_modules/strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "node_modules/@isaacs/fs-minipass": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", + "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "dev": true, + "license": "ISC", "dependencies": { - "ansi-regex": "^6.0.1" + "minipass": "^7.0.4" }, "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" + "node": ">=18.0.0" } }, "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", - "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", + "version": "0.3.12", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.12.tgz", + "integrity": "sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==", + "dev": true, + "license": "MIT", "dependencies": { - "@jridgewell/set-array": "^1.2.1", - "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" - }, - "engines": { - "node": ">=6.0.0" } }, "node_modules/@jridgewell/resolve-uri": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/set-array": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", - "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "dev": true, + "license": "MIT", "engines": { "node": ">=6.0.0" } }, "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.4.15", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", - "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==" + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.4.tgz", + "integrity": "sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw==", + "dev": true, + "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.25", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", - "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "version": "0.3.29", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.29.tgz", + "integrity": "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==", + "dev": true, + "license": "MIT", "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" @@ -760,6 +887,8 @@ "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" @@ -772,6 +901,8 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", "engines": { "node": ">= 8" } @@ -780,6 +911,8 @@ "version": "1.2.8", "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" @@ -788,48 +921,45 @@ "node": ">= 8" } }, - "node_modules/@pkgjs/parseargs": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", - "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", - "optional": true, - "engines": { - "node": ">=14" - } - }, "node_modules/@react-aria/focus": { - "version": "3.18.2", - "resolved": "https://registry.npmjs.org/@react-aria/focus/-/focus-3.18.2.tgz", - "integrity": "sha512-Jc/IY+StjA3uqN73o6txKQ527RFU7gnG5crEl5Xy3V+gbYp2O5L3ezAo/E0Ipi2cyMbG6T5Iit1IDs7hcGu8aw==", + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/@react-aria/focus/-/focus-3.21.0.tgz", + "integrity": "sha512-7NEGtTPsBy52EZ/ToVKCu0HSelE3kq9qeis+2eEq90XSuJOMaDHUQrA7RC2Y89tlEwQB31bud/kKRi9Qme1dkA==", + "license": "Apache-2.0", "dependencies": { - "@react-aria/interactions": "^3.22.2", - "@react-aria/utils": "^3.25.2", - "@react-types/shared": "^3.24.1", + "@react-aria/interactions": "^3.25.4", + "@react-aria/utils": "^3.30.0", + "@react-types/shared": "^3.31.0", "@swc/helpers": "^0.5.0", "clsx": "^2.0.0" }, "peerDependencies": { - "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0" + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "node_modules/@react-aria/interactions": { - "version": "3.22.2", - "resolved": "https://registry.npmjs.org/@react-aria/interactions/-/interactions-3.22.2.tgz", - "integrity": "sha512-xE/77fRVSlqHp2sfkrMeNLrqf2amF/RyuAS6T5oDJemRSgYM3UoxTbWjucPhfnoW7r32pFPHHgz4lbdX8xqD/g==", + "version": "3.25.4", + "resolved": "https://registry.npmjs.org/@react-aria/interactions/-/interactions-3.25.4.tgz", + "integrity": "sha512-HBQMxgUPHrW8V63u9uGgBymkMfj6vdWbB0GgUJY49K9mBKMsypcHeWkWM6+bF7kxRO728/IK8bWDV6whDbqjHg==", + "license": "Apache-2.0", "dependencies": { - "@react-aria/ssr": "^3.9.5", - "@react-aria/utils": "^3.25.2", - "@react-types/shared": "^3.24.1", + "@react-aria/ssr": "^3.9.10", + "@react-aria/utils": "^3.30.0", + "@react-stately/flags": "^3.1.2", + "@react-types/shared": "^3.31.0", "@swc/helpers": "^0.5.0" }, "peerDependencies": { - "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0" + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "node_modules/@react-aria/ssr": { - "version": "3.9.5", - "resolved": "https://registry.npmjs.org/@react-aria/ssr/-/ssr-3.9.5.tgz", - "integrity": "sha512-xEwGKoysu+oXulibNUSkXf8itW0npHHTa6c4AyYeZIJyRoegeteYuFpZUBPtIDE8RfHdNsSmE1ssOkxRnwbkuQ==", + "version": "3.9.10", + "resolved": "https://registry.npmjs.org/@react-aria/ssr/-/ssr-3.9.10.tgz", + "integrity": "sha512-hvTm77Pf+pMBhuBm760Li0BVIO38jv1IBws1xFm1NoL26PU+fe+FMW5+VZWyANR6nYL65joaJKZqOdTQMkO9IQ==", + "license": "Apache-2.0", "dependencies": { "@swc/helpers": "^0.5.0" }, @@ -837,241 +967,328 @@ "node": ">= 12" }, "peerDependencies": { - "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0" + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "node_modules/@react-aria/utils": { - "version": "3.25.2", - "resolved": "https://registry.npmjs.org/@react-aria/utils/-/utils-3.25.2.tgz", - "integrity": "sha512-GdIvG8GBJJZygB4L2QJP1Gabyn2mjFsha73I2wSe+o4DYeGWoJiMZRM06PyTIxLH4S7Sn7eVDtsSBfkc2VY/NA==", + "version": "3.30.0", + "resolved": "https://registry.npmjs.org/@react-aria/utils/-/utils-3.30.0.tgz", + "integrity": "sha512-ydA6y5G1+gbem3Va2nczj/0G0W7/jUVo/cbN10WA5IizzWIwMP5qhFr7macgbKfHMkZ+YZC3oXnt2NNre5odKw==", + "license": "Apache-2.0", "dependencies": { - "@react-aria/ssr": "^3.9.5", - "@react-stately/utils": "^3.10.3", - "@react-types/shared": "^3.24.1", + "@react-aria/ssr": "^3.9.10", + "@react-stately/flags": "^3.1.2", + "@react-stately/utils": "^3.10.8", + "@react-types/shared": "^3.31.0", "@swc/helpers": "^0.5.0", "clsx": "^2.0.0" }, "peerDependencies": { - "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0" + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-stately/flags": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@react-stately/flags/-/flags-3.1.2.tgz", + "integrity": "sha512-2HjFcZx1MyQXoPqcBGALwWWmgFVUk2TuKVIQxCbRq7fPyWXIl6VHcakCLurdtYC2Iks7zizvz0Idv48MQ38DWg==", + "license": "Apache-2.0", + "dependencies": { + "@swc/helpers": "^0.5.0" } }, "node_modules/@react-stately/utils": { - "version": "3.10.3", - "resolved": "https://registry.npmjs.org/@react-stately/utils/-/utils-3.10.3.tgz", - "integrity": "sha512-moClv7MlVSHpbYtQIkm0Cx+on8Pgt1XqtPx6fy9rQFb2DNc9u1G3AUVnqA17buOkH1vLxAtX4MedlxMWyRCYYA==", + "version": "3.10.8", + "resolved": "https://registry.npmjs.org/@react-stately/utils/-/utils-3.10.8.tgz", + "integrity": "sha512-SN3/h7SzRsusVQjQ4v10LaVsDc81jyyR0DD5HnsQitm/I5WDpaSr2nRHtyloPFU48jlql1XX/S04T2DLQM7Y3g==", + "license": "Apache-2.0", "dependencies": { "@swc/helpers": "^0.5.0" }, "peerDependencies": { - "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0" + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "node_modules/@react-types/shared": { - "version": "3.24.1", - "resolved": "https://registry.npmjs.org/@react-types/shared/-/shared-3.24.1.tgz", - "integrity": "sha512-AUQeGYEm/zDTN6zLzdXolDxz3Jk5dDL7f506F07U8tBwxNNI3WRdhU84G0/AaFikOZzDXhOZDr3MhQMzyE7Ydw==", + "version": "3.31.0", + "resolved": "https://registry.npmjs.org/@react-types/shared/-/shared-3.31.0.tgz", + "integrity": "sha512-ua5U6V66gDcbLZe4P2QeyNgPp4YWD1ymGA6j3n+s8CGExtrCPe64v+g4mvpT8Bnb985R96e4zFT61+m0YCwqMg==", + "license": "Apache-2.0", "peerDependencies": { - "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0" + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "node_modules/@remix-run/router": { - "version": "1.15.3", - "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.15.3.tgz", - "integrity": "sha512-Oy8rmScVrVxWZVOpEF57ovlnhpZ8CCPlnIIumVcV9nFdiSIrus99+Lw78ekXyGvVDlIsFJbSfmSovJUhCWYV3w==", + "version": "1.23.0", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.0.tgz", + "integrity": "sha512-O3rHJzAQKamUz1fvE0Qaw0xSFqsA/yafi2iqeE0pvdFtCO1viYx8QL6f3Ln/aCCTLxs68SLf0KPM9eSeM8yBnA==", + "license": "MIT", "engines": { "node": ">=14.0.0" } }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "dev": true, + "license": "MIT" + }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.14.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.14.1.tgz", - "integrity": "sha512-fH8/o8nSUek8ceQnT7K4EQbSiV7jgkHq81m9lWZFIXjJ7lJzpWXbQFpT/Zh6OZYnpFykvzC3fbEvEAFZu03dPA==", + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.46.2.tgz", + "integrity": "sha512-Zj3Hl6sN34xJtMv7Anwb5Gu01yujyE/cLBDB2gnHTAHaWS1Z38L7kuSG+oAh0giZMqG060f/YBStXtMH6FvPMA==", "cpu": [ "arm" ], - "dev": true, + "license": "MIT", "optional": true, "os": [ "android" ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.14.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.14.1.tgz", - "integrity": "sha512-Y/9OHLjzkunF+KGEoJr3heiD5X9OLa8sbT1lm0NYeKyaM3oMhhQFvPB0bNZYJwlq93j8Z6wSxh9+cyKQaxS7PQ==", + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.46.2.tgz", + "integrity": "sha512-nTeCWY83kN64oQ5MGz3CgtPx8NSOhC5lWtsjTs+8JAJNLcP3QbLCtDDgUKQc/Ro/frpMq4SHUaHN6AMltcEoLQ==", "cpu": [ "arm64" ], - "dev": true, + "license": "MIT", "optional": true, "os": [ "android" ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.14.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.14.1.tgz", - "integrity": "sha512-+kecg3FY84WadgcuSVm6llrABOdQAEbNdnpi5X3UwWiFVhZIZvKgGrF7kmLguvxHNQy+UuRV66cLVl3S+Rkt+Q==", + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.46.2.tgz", + "integrity": "sha512-HV7bW2Fb/F5KPdM/9bApunQh68YVDU8sO8BvcW9OngQVN3HHHkw99wFupuUJfGR9pYLLAjcAOA6iO+evsbBaPQ==", "cpu": [ "arm64" ], - "dev": true, + "license": "MIT", "optional": true, "os": [ "darwin" ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.14.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.14.1.tgz", - "integrity": "sha512-2pYRzEjVqq2TB/UNv47BV/8vQiXkFGVmPFwJb+1E0IFFZbIX8/jo1olxqqMbo6xCXf8kabANhp5bzCij2tFLUA==", + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.46.2.tgz", + "integrity": "sha512-SSj8TlYV5nJixSsm/y3QXfhspSiLYP11zpfwp6G/YDXctf3Xkdnk4woJIF5VQe0of2OjzTt8EsxnJDCdHd2xMA==", "cpu": [ "x64" ], - "dev": true, + "license": "MIT", "optional": true, "os": [ "darwin" ] }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.46.2.tgz", + "integrity": "sha512-ZyrsG4TIT9xnOlLsSSi9w/X29tCbK1yegE49RYm3tu3wF1L/B6LVMqnEWyDB26d9Ecx9zrmXCiPmIabVuLmNSg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.46.2.tgz", + "integrity": "sha512-pCgHFoOECwVCJ5GFq8+gR8SBKnMO+xe5UEqbemxBpCKYQddRQMgomv1104RnLSg7nNvgKy05sLsY51+OVRyiVw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.14.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.14.1.tgz", - "integrity": "sha512-mS6wQ6Do6/wmrF9aTFVpIJ3/IDXhg1EZcQFYHZLHqw6AzMBjTHWnCG35HxSqUNphh0EHqSM6wRTT8HsL1C0x5g==", + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.46.2.tgz", + "integrity": "sha512-EtP8aquZ0xQg0ETFcxUbU71MZlHaw9MChwrQzatiE8U/bvi5uv/oChExXC4mWhjiqK7azGJBqU0tt5H123SzVA==", "cpu": [ "arm" ], - "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.46.2.tgz", + "integrity": "sha512-qO7F7U3u1nfxYRPM8HqFtLd+raev2K137dsV08q/LRKRLEc7RsiDWihUnrINdsWQxPR9jqZ8DIIZ1zJJAm5PjQ==", + "cpu": [ + "arm" + ], + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.14.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.14.1.tgz", - "integrity": "sha512-p9rGKYkHdFMzhckOTFubfxgyIO1vw//7IIjBBRVzyZebWlzRLeNhqxuSaZ7kCEKVkm/kuC9fVRW9HkC/zNRG2w==", + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.46.2.tgz", + "integrity": "sha512-3dRaqLfcOXYsfvw5xMrxAk9Lb1f395gkoBYzSFcc/scgRFptRXL9DOaDpMiehf9CO8ZDRJW2z45b6fpU5nwjng==", "cpu": [ "arm64" ], - "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.14.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.14.1.tgz", - "integrity": "sha512-nDY6Yz5xS/Y4M2i9JLQd3Rofh5OR8Bn8qe3Mv/qCVpHFlwtZSBYSPaU4mrGazWkXrdQ98GB//H0BirGR/SKFSw==", + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.46.2.tgz", + "integrity": "sha512-fhHFTutA7SM+IrR6lIfiHskxmpmPTJUXpWIsBXpeEwNgZzZZSg/q4i6FU4J8qOGyJ0TR+wXBwx/L7Ho9z0+uDg==", "cpu": [ "arm64" ], - "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, - "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.14.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.14.1.tgz", - "integrity": "sha512-im7HE4VBL+aDswvcmfx88Mp1soqL9OBsdDBU8NqDEYtkri0qV0THhQsvZtZeNNlLeCUQ16PZyv7cqutjDF35qw==", + "node_modules/@rollup/rollup-linux-loongarch64-gnu": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.46.2.tgz", + "integrity": "sha512-i7wfGFXu8x4+FRqPymzjD+Hyav8l95UIZ773j7J7zRYc3Xsxy2wIn4x+llpunexXe6laaO72iEjeeGyUFmjKeA==", "cpu": [ - "ppc64le" + "loong64" ], - "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.46.2.tgz", + "integrity": "sha512-B/l0dFcHVUnqcGZWKcWBSV2PF01YUt0Rvlurci5P+neqY/yMKchGU8ullZvIv5e8Y1C6wOn+U03mrDylP5q9Yw==", + "cpu": [ + "ppc64" + ], + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.14.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.14.1.tgz", - "integrity": "sha512-RWdiHuAxWmzPJgaHJdpvUUlDz8sdQz4P2uv367T2JocdDa98iRw2UjIJ4QxSyt077mXZT2X6pKfT2iYtVEvOFw==", + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.46.2.tgz", + "integrity": "sha512-32k4ENb5ygtkMwPMucAb8MtV8olkPT03oiTxJbgkJa7lJ7dZMr0GCFJlyvy+K8iq7F/iuOr41ZdUHaOiqyR3iQ==", "cpu": [ "riscv64" ], - "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.46.2.tgz", + "integrity": "sha512-t5B2loThlFEauloaQkZg9gxV05BYeITLvLkWOkRXogP4qHXLkWSbSHKM9S6H1schf/0YGP/qNKtiISlxvfmmZw==", + "cpu": [ + "riscv64" + ], + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.14.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.14.1.tgz", - "integrity": "sha512-VMgaGQ5zRX6ZqV/fas65/sUGc9cPmsntq2FiGmayW9KMNfWVG/j0BAqImvU4KTeOOgYSf1F+k6at1UfNONuNjA==", + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.46.2.tgz", + "integrity": "sha512-YKjekwTEKgbB7n17gmODSmJVUIvj8CX7q5442/CK80L8nqOUbMtf8b01QkG3jOqyr1rotrAnW6B/qiHwfcuWQA==", "cpu": [ "s390x" ], - "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.14.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.14.1.tgz", - "integrity": "sha512-9Q7DGjZN+hTdJomaQ3Iub4m6VPu1r94bmK2z3UeWP3dGUecRC54tmVu9vKHTm1bOt3ASoYtEz6JSRLFzrysKlA==", + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.46.2.tgz", + "integrity": "sha512-Jj5a9RUoe5ra+MEyERkDKLwTXVu6s3aACP51nkfnK9wJTraCC8IMe3snOfALkrjTYd2G1ViE1hICj0fZ7ALBPA==", "cpu": [ "x64" ], - "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.14.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.14.1.tgz", - "integrity": "sha512-JNEG/Ti55413SsreTguSx0LOVKX902OfXIKVg+TCXO6Gjans/k9O6ww9q3oLGjNDaTLxM+IHFMeXy/0RXL5R/g==", + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.46.2.tgz", + "integrity": "sha512-7kX69DIrBeD7yNp4A5b81izs8BqoZkCIaxQaOpumcJ1S/kmqNFjPhDu1LHeVXv0SexfHQv5cqHsxLOjETuqDuA==", "cpu": [ "x64" ], - "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.14.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.14.1.tgz", - "integrity": "sha512-ryS22I9y0mumlLNwDFYZRDFLwWh3aKaC72CWjFcFvxK0U6v/mOkM5Up1bTbCRAhv3kEIwW2ajROegCIQViUCeA==", + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.46.2.tgz", + "integrity": "sha512-wiJWMIpeaak/jsbaq2HMh/rzZxHVW1rU6coyeNNpMwk5isiPjSTx0a4YLSlYDwBH/WBvLz+EtsNqQScZTLJy3g==", "cpu": [ "arm64" ], - "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.14.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.14.1.tgz", - "integrity": "sha512-TdloItiGk+T0mTxKx7Hp279xy30LspMso+GzQvV2maYePMAWdmrzqSNZhUpPj3CGw12aGj57I026PgLCTu8CGg==", + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.46.2.tgz", + "integrity": "sha512-gBgaUDESVzMgWZhcyjfs9QFK16D8K6QZpwAaVNJxYDLHWayOta4ZMjGm/vsAEy3hvlS2GosVFlBlP9/Wb85DqQ==", "cpu": [ "ia32" ], - "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.14.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.14.1.tgz", - "integrity": "sha512-wQGI+LY/Py20zdUPq+XCem7JcPOyzIJBm3dli+56DJsQOHbnXZFEwgmnC6el1TPAfC8lBT3m+z69RmLykNUbew==", + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.46.2.tgz", + "integrity": "sha512-CvUo2ixeIQGtF6WvuB87XWqPQkoFAFqW+HUo/WzHwuHDvIwZCtjdWXoYCcr06iKGydiqTclC4jU/TNObC/xKZg==", "cpu": [ "x64" ], - "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" @@ -1080,17 +1297,19 @@ "node_modules/@rtsao/scc": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", - "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==" + "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==", + "license": "MIT" }, "node_modules/@swc/core": { - "version": "1.11.4", - "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.11.4.tgz", - "integrity": "sha512-EHl6eNod/914xDRK4nu7gr78riK2cfi4DkAMvJt6COdaNGOnbR5eKrLe3SnRizyzzrPcxUMhflDL5hrcXS8rAQ==", + "version": "1.13.3", + "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.13.3.tgz", + "integrity": "sha512-ZaDETVWnm6FE0fc+c2UE8MHYVS3Fe91o5vkmGfgwGXFbxYvAjKSqxM/j4cRc9T7VZNSJjriXq58XkfCp3Y6f+w==", "dev": true, "hasInstallScript": true, + "license": "Apache-2.0", "dependencies": { "@swc/counter": "^0.1.3", - "@swc/types": "^0.1.19" + "@swc/types": "^0.1.23" }, "engines": { "node": ">=10" @@ -1100,19 +1319,19 @@ "url": "https://opencollective.com/swc" }, "optionalDependencies": { - "@swc/core-darwin-arm64": "1.11.4", - "@swc/core-darwin-x64": "1.11.4", - "@swc/core-linux-arm-gnueabihf": "1.11.4", - "@swc/core-linux-arm64-gnu": "1.11.4", - "@swc/core-linux-arm64-musl": "1.11.4", - "@swc/core-linux-x64-gnu": "1.11.4", - "@swc/core-linux-x64-musl": "1.11.4", - "@swc/core-win32-arm64-msvc": "1.11.4", - "@swc/core-win32-ia32-msvc": "1.11.4", - "@swc/core-win32-x64-msvc": "1.11.4" + "@swc/core-darwin-arm64": "1.13.3", + "@swc/core-darwin-x64": "1.13.3", + "@swc/core-linux-arm-gnueabihf": "1.13.3", + "@swc/core-linux-arm64-gnu": "1.13.3", + "@swc/core-linux-arm64-musl": "1.13.3", + "@swc/core-linux-x64-gnu": "1.13.3", + "@swc/core-linux-x64-musl": "1.13.3", + "@swc/core-win32-arm64-msvc": "1.13.3", + "@swc/core-win32-ia32-msvc": "1.13.3", + "@swc/core-win32-x64-msvc": "1.13.3" }, "peerDependencies": { - "@swc/helpers": "*" + "@swc/helpers": ">=0.5.17" }, "peerDependenciesMeta": { "@swc/helpers": { @@ -1121,13 +1340,14 @@ } }, "node_modules/@swc/core-darwin-arm64": { - "version": "1.11.4", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.11.4.tgz", - "integrity": "sha512-Oi4lt4wqjpp80pcCh+vzvpsESJ8XXozYCE5EM/dDpr+9m2oRpkseds7Gq4ulzgdbUDPo1jJ1PonjjrKpfKY+sQ==", + "version": "1.13.3", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.13.3.tgz", + "integrity": "sha512-ux0Ws4pSpBTqbDS9GlVP354MekB1DwYlbxXU3VhnDr4GBcCOimpocx62x7cFJkSpEBF8bmX8+/TTCGKh4PbyXw==", "cpu": [ "arm64" ], "dev": true, + "license": "Apache-2.0 AND MIT", "optional": true, "os": [ "darwin" @@ -1137,13 +1357,14 @@ } }, "node_modules/@swc/core-darwin-x64": { - "version": "1.11.4", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.11.4.tgz", - "integrity": "sha512-Tb7ez94DXxhX5iJ5slnAlT2gwJinQk3pMnQ46Npi6adKr3ZXM5Bdk0jpRUp8XjEcgNXkQRV1DtrySgCz6YlEnQ==", + "version": "1.13.3", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.13.3.tgz", + "integrity": "sha512-p0X6yhxmNUOMZrbeZ3ZNsPige8lSlSe1llllXvpCLkKKxN/k5vZt1sULoq6Nj4eQ7KeHQVm81/+AwKZyf/e0TA==", "cpu": [ "x64" ], "dev": true, + "license": "Apache-2.0 AND MIT", "optional": true, "os": [ "darwin" @@ -1153,13 +1374,14 @@ } }, "node_modules/@swc/core-linux-arm-gnueabihf": { - "version": "1.11.4", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.11.4.tgz", - "integrity": "sha512-p1uV+6Mi+0M+1kL7qL206ZaohomYMW7yroXSLDTJXbIylx7wG2xrUQL6AFtz2DwqDoX/E8jMNBjp+GcEy8r8Ig==", + "version": "1.13.3", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.13.3.tgz", + "integrity": "sha512-OmDoiexL2fVWvQTCtoh0xHMyEkZweQAlh4dRyvl8ugqIPEVARSYtaj55TBMUJIP44mSUOJ5tytjzhn2KFxFcBA==", "cpu": [ "arm" ], "dev": true, + "license": "Apache-2.0", "optional": true, "os": [ "linux" @@ -1169,13 +1391,14 @@ } }, "node_modules/@swc/core-linux-arm64-gnu": { - "version": "1.11.4", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.11.4.tgz", - "integrity": "sha512-4ijX4bWf9oc7kWkT6xUhugVGzEJ7U9c7CHNmt/xhI/yWsQdfM11+HECqWh7ay3m+aaEoVdvTeU5gykeF5jSxDA==", + "version": "1.13.3", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.13.3.tgz", + "integrity": "sha512-STfKku3QfnuUj6k3g9ld4vwhtgCGYIFQmsGPPgT9MK/dI3Lwnpe5Gs5t1inoUIoGNP8sIOLlBB4HV4MmBjQuhw==", "cpu": [ "arm64" ], "dev": true, + "license": "Apache-2.0 AND MIT", "optional": true, "os": [ "linux" @@ -1185,13 +1408,14 @@ } }, "node_modules/@swc/core-linux-arm64-musl": { - "version": "1.11.4", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.11.4.tgz", - "integrity": "sha512-XI+gOgcuSanejbAC5QXKTjNA3GUJi7bzHmeJbNhKpX9d349RdVwan0k9okHmhMBY7BywAg3LK0ovF9PmOLgMHg==", + "version": "1.13.3", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.13.3.tgz", + "integrity": "sha512-bc+CXYlFc1t8pv9yZJGus372ldzOVscBl7encUBlU1m/Sig0+NDJLz6cXXRcFyl6ABNOApWeR4Yl7iUWx6C8og==", "cpu": [ "arm64" ], "dev": true, + "license": "Apache-2.0 AND MIT", "optional": true, "os": [ "linux" @@ -1201,13 +1425,14 @@ } }, "node_modules/@swc/core-linux-x64-gnu": { - "version": "1.11.4", - "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.11.4.tgz", - "integrity": "sha512-wyD6noaCPFayKOvl9mTxuiQoEULAagGuO0od2VkW7h4HvlgpOAZNekZYX73WEP/b+WuePNHurZ9KGpom43IzmA==", + "version": "1.13.3", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.13.3.tgz", + "integrity": "sha512-dFXoa0TEhohrKcxn/54YKs1iwNeW6tUkHJgXW33H381SvjKFUV53WR231jh1sWVJETjA3vsAwxKwR23s7UCmUA==", "cpu": [ "x64" ], "dev": true, + "license": "Apache-2.0 AND MIT", "optional": true, "os": [ "linux" @@ -1217,13 +1442,14 @@ } }, "node_modules/@swc/core-linux-x64-musl": { - "version": "1.11.4", - "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.11.4.tgz", - "integrity": "sha512-e2vG9gUF1BRX0BWqSEHop6u14l5BtV3VS2Pmr+oquc0Ycs/zj81xhYc3ML4ByK5OxDkAaKBWryAOKTLaJA/DVg==", + "version": "1.13.3", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.13.3.tgz", + "integrity": "sha512-ieyjisLB+ldexiE/yD8uomaZuZIbTc8tjquYln9Quh5ykOBY7LpJJYBWvWtm1g3pHv6AXlBI8Jay7Fffb6aLfA==", "cpu": [ "x64" ], "dev": true, + "license": "Apache-2.0 AND MIT", "optional": true, "os": [ "linux" @@ -1233,13 +1459,14 @@ } }, "node_modules/@swc/core-win32-arm64-msvc": { - "version": "1.11.4", - "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.11.4.tgz", - "integrity": "sha512-rm51iljNqjCA/41gxYameuyjX1ENaTlvdxmaoPPYeUDt6hfypG93IxMJJCewaeHN9XfNxqZU7d4cupNqk+8nng==", + "version": "1.13.3", + "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.13.3.tgz", + "integrity": "sha512-elTQpnaX5vESSbhCEgcwXjpMsnUbqqHfEpB7ewpkAsLzKEXZaK67ihSRYAuAx6ewRQTo7DS5iTT6X5aQD3MzMw==", "cpu": [ "arm64" ], "dev": true, + "license": "Apache-2.0 AND MIT", "optional": true, "os": [ "win32" @@ -1249,13 +1476,14 @@ } }, "node_modules/@swc/core-win32-ia32-msvc": { - "version": "1.11.4", - "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.11.4.tgz", - "integrity": "sha512-PHy3N6zlyU8te7Umi0ggXNbcx2VUkwpE59PW9FQQy9MBZM1Qn+OEGnO/4KLWjGFABw+9CwIeaRYgq6uCi1ry6A==", + "version": "1.13.3", + "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.13.3.tgz", + "integrity": "sha512-nvehQVEOdI1BleJpuUgPLrclJ0TzbEMc+MarXDmmiRFwEUGqj+pnfkTSb7RZyS1puU74IXdK/YhTirHurtbI9w==", "cpu": [ "ia32" ], "dev": true, + "license": "Apache-2.0 AND MIT", "optional": true, "os": [ "win32" @@ -1265,13 +1493,14 @@ } }, "node_modules/@swc/core-win32-x64-msvc": { - "version": "1.11.4", - "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.11.4.tgz", - "integrity": "sha512-0TiriDGl7Dr4ObfMBk07PS4Ql5hgQH0QnU3E8I+fbs45hqfwC5OrN47HOsXx4ZbEw8XYxp2NM8SGnVoTIm4J8w==", + "version": "1.13.3", + "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.13.3.tgz", + "integrity": "sha512-A+JSKGkRbPLVV2Kwx8TaDAV0yXIXm/gc8m98hSkVDGlPBBmydgzNdWy3X7HTUBM7IDk7YlWE7w2+RUGjdgpTmg==", "cpu": [ "x64" ], "dev": true, + "license": "Apache-2.0 AND MIT", "optional": true, "os": [ "win32" @@ -1284,21 +1513,24 @@ "version": "0.1.3", "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==", - "dev": true + "dev": true, + "license": "Apache-2.0" }, "node_modules/@swc/helpers": { - "version": "0.5.13", - "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.13.tgz", - "integrity": "sha512-UoKGxQ3r5kYI9dALKJapMmuK+1zWM/H17Z1+iwnNmzcJRnfFuevZs375TA5rW31pu4BS4NoSy1fRsexDXfWn5w==", + "version": "0.5.17", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.17.tgz", + "integrity": "sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A==", + "license": "Apache-2.0", "dependencies": { - "tslib": "^2.4.0" + "tslib": "^2.8.0" } }, "node_modules/@swc/types": { - "version": "0.1.19", - "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.19.tgz", - "integrity": "sha512-WkAZaAfj44kh/UFdAQcrMP1I0nwRqpt27u+08LMBYMqmQfwwMofYoMh/48NGkMMRfC4ynpfwRbJuu8ErfNloeA==", + "version": "0.1.24", + "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.24.tgz", + "integrity": "sha512-tjTMh3V4vAORHtdTprLlfoMptu1WfTZG9Rsca6yOKyNYsRr+MUXutKmliB17orgSZk5DpnDxs8GUdd/qwYxOng==", "dev": true, + "license": "Apache-2.0", "dependencies": { "@swc/counter": "^0.1.3" } @@ -1308,6 +1540,7 @@ "resolved": "https://registry.npmjs.org/@tailwindcss/forms/-/forms-0.5.10.tgz", "integrity": "sha512-utI1ONF6uf/pPNO68kmN1b8rEwNXv3czukalo8VtJH8ksIkZXr3Q3VYudZLkCsDd4Wku120uF02hYK25XGPorw==", "dev": true, + "license": "MIT", "dependencies": { "mini-svg-data-uri": "^1.2.3" }, @@ -1315,11 +1548,288 @@ "tailwindcss": ">=3.0.0 || >= 3.0.0-alpha.1 || >= 4.0.0-alpha.20 || >= 4.0.0-beta.1" } }, + "node_modules/@tailwindcss/node": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.11.tgz", + "integrity": "sha512-yzhzuGRmv5QyU9qLNg4GTlYI6STedBWRE7NjxP45CsFYYq9taI0zJXZBMqIC/c8fViNLhmrbpSFS57EoxUmD6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.3.0", + "enhanced-resolve": "^5.18.1", + "jiti": "^2.4.2", + "lightningcss": "1.30.1", + "magic-string": "^0.30.17", + "source-map-js": "^1.2.1", + "tailwindcss": "4.1.11" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.11.tgz", + "integrity": "sha512-Q69XzrtAhuyfHo+5/HMgr1lAiPP/G40OMFAnws7xcFEYqcypZmdW8eGXaOUIeOl1dzPJBPENXgbjsOyhg2nkrg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.4", + "tar": "^7.4.3" + }, + "engines": { + "node": ">= 10" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.1.11", + "@tailwindcss/oxide-darwin-arm64": "4.1.11", + "@tailwindcss/oxide-darwin-x64": "4.1.11", + "@tailwindcss/oxide-freebsd-x64": "4.1.11", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.11", + "@tailwindcss/oxide-linux-arm64-gnu": "4.1.11", + "@tailwindcss/oxide-linux-arm64-musl": "4.1.11", + "@tailwindcss/oxide-linux-x64-gnu": "4.1.11", + "@tailwindcss/oxide-linux-x64-musl": "4.1.11", + "@tailwindcss/oxide-wasm32-wasi": "4.1.11", + "@tailwindcss/oxide-win32-arm64-msvc": "4.1.11", + "@tailwindcss/oxide-win32-x64-msvc": "4.1.11" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.11.tgz", + "integrity": "sha512-3IfFuATVRUMZZprEIx9OGDjG3Ou3jG4xQzNTvjDoKmU9JdmoCohQJ83MYd0GPnQIu89YoJqvMM0G3uqLRFtetg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.11.tgz", + "integrity": "sha512-ESgStEOEsyg8J5YcMb1xl8WFOXfeBmrhAwGsFxxB2CxY9evy63+AtpbDLAyRkJnxLy2WsD1qF13E97uQyP1lfQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.11.tgz", + "integrity": "sha512-EgnK8kRchgmgzG6jE10UQNaH9Mwi2n+yw1jWmof9Vyg2lpKNX2ioe7CJdf9M5f8V9uaQxInenZkOxnTVL3fhAw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.11.tgz", + "integrity": "sha512-xdqKtbpHs7pQhIKmqVpxStnY1skuNh4CtbcyOHeX1YBE0hArj2romsFGb6yUmzkq/6M24nkxDqU8GYrKrz+UcA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.11.tgz", + "integrity": "sha512-ryHQK2eyDYYMwB5wZL46uoxz2zzDZsFBwfjssgB7pzytAeCCa6glsiJGjhTEddq/4OsIjsLNMAiMlHNYnkEEeg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.11.tgz", + "integrity": "sha512-mYwqheq4BXF83j/w75ewkPJmPZIqqP1nhoghS9D57CLjsh3Nfq0m4ftTotRYtGnZd3eCztgbSPJ9QhfC91gDZQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.11.tgz", + "integrity": "sha512-m/NVRFNGlEHJrNVk3O6I9ggVuNjXHIPoD6bqay/pubtYC9QIdAMpS+cswZQPBLvVvEF6GtSNONbDkZrjWZXYNQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.11.tgz", + "integrity": "sha512-YW6sblI7xukSD2TdbbaeQVDysIm/UPJtObHJHKxDEcW2exAtY47j52f8jZXkqE1krdnkhCMGqP3dbniu1Te2Fg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.11.tgz", + "integrity": "sha512-e3C/RRhGunWYNC3aSF7exsQkdXzQ/M+aYuZHKnw4U7KQwTJotnWsGOIVih0s2qQzmEzOFIJ3+xt7iq67K/p56Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.11.tgz", + "integrity": "sha512-Xo1+/GU0JEN/C/dvcammKHzeM6NqKovG+6921MR6oadee5XPBaKOumrJCXvopJ/Qb5TH7LX/UAywbqrP4lax0g==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.4.3", + "@emnapi/runtime": "^1.4.3", + "@emnapi/wasi-threads": "^1.0.2", + "@napi-rs/wasm-runtime": "^0.2.11", + "@tybys/wasm-util": "^0.9.0", + "tslib": "^2.8.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.11.tgz", + "integrity": "sha512-UgKYx5PwEKrac3GPNPf6HVMNhUIGuUh4wlDFR2jYYdkX6pL/rn73zTq/4pzUm8fOjAn5L8zDeHp9iXmUGOXZ+w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.11.tgz", + "integrity": "sha512-YfHoggn1j0LK7wR82TOucWc5LDCguHnoS879idHekmmiR7g9HUtMw9MI0NHatS28u/Xlkfi9w5RJWgz2Dl+5Qg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/postcss": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.1.11.tgz", + "integrity": "sha512-q/EAIIpF6WpLhKEuQSEVMZNMIY8KhWoAemZ9eylNAih9jxMGAYPPWBn3I9QL/2jZ+e7OEz/tZkX5HwbBR4HohA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "@tailwindcss/node": "4.1.11", + "@tailwindcss/oxide": "4.1.11", + "postcss": "^8.4.41", + "tailwindcss": "4.1.11" + } + }, "node_modules/@tailwindcss/typography": { "version": "0.5.16", "resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.16.tgz", "integrity": "sha512-0wDLwCVF5V3x3b1SGXPCDcdsbDHMBe+lkFzBRaHeLvNi+nrrnZ1lA18u+OTWO8iSWU2GxUOCvlXtDuqftc1oiA==", "dev": true, + "license": "MIT", "dependencies": { "lodash.castarray": "^4.4.0", "lodash.isplainobject": "^4.0.6", @@ -1330,39 +1840,43 @@ "tailwindcss": ">=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1" } }, - "node_modules/@tailwindcss/typography/node_modules/postcss-selector-parser": { - "version": "6.0.10", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz", - "integrity": "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==", + "node_modules/@tailwindcss/vite": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.1.11.tgz", + "integrity": "sha512-RHYhrR3hku0MJFRV+fN2gNbDNEh3dwKvY8XJvTxCSXeMOsCRSr+uKvDWQcbizrHgjML6ZmTE5OwMrl5wKcujCw==", "dev": true, + "license": "MIT", "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" + "@tailwindcss/node": "4.1.11", + "@tailwindcss/oxide": "4.1.11", + "tailwindcss": "4.1.11" }, - "engines": { - "node": ">=4" + "peerDependencies": { + "vite": "^5.2.0 || ^6 || ^7" } }, "node_modules/@tanstack/react-virtual": { - "version": "3.10.7", - "resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.10.7.tgz", - "integrity": "sha512-yeP+M0G8D+15ZFPivpuQ5hoM4Fa/PzERBx8P8EGcfEsXX3JOb9G9UUrqc47ZXAxvK+YqzM9T5qlJUYUFOwCZJw==", + "version": "3.13.12", + "resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.13.12.tgz", + "integrity": "sha512-Gd13QdxPSukP8ZrkbgS2RwoZseTTbQPLnQEn7HY/rqtM+8Zt95f7xKC7N0EsKs7aoz0WzZ+fditZux+F8EzYxA==", + "license": "MIT", "dependencies": { - "@tanstack/virtual-core": "3.10.7" + "@tanstack/virtual-core": "3.13.12" }, "funding": { "type": "github", "url": "https://github.com/sponsors/tannerlinsley" }, "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0", - "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "node_modules/@tanstack/virtual-core": { - "version": "3.10.7", - "resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.10.7.tgz", - "integrity": "sha512-ND5dfsU0n9F4gROzwNNDJmg6y8n9pI8YWxtgbfJ5UcNn7Hx+MxEXtXcQ189tS7sh8pmCObgz2qSiyRKTZxT4dg==", + "version": "3.13.12", + "resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.13.12.tgz", + "integrity": "sha512-1YBOJfRHV4sXUmWsFSf5rQor4Ss82G8dQWLRbnk3GA4jeP8hQt1hxXh0tmflpC0dz3VgEv/1+qwPyLeWkQuPFA==", + "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/tannerlinsley" @@ -1371,120 +1885,132 @@ "node_modules/@types/d3-array": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.1.tgz", - "integrity": "sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==" + "integrity": "sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==", + "license": "MIT" }, "node_modules/@types/d3-color": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", - "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==" + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "license": "MIT" }, "node_modules/@types/d3-ease": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", - "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==" + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", + "license": "MIT" }, "node_modules/@types/d3-interpolate": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "license": "MIT", "dependencies": { "@types/d3-color": "*" } }, "node_modules/@types/d3-path": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.0.tgz", - "integrity": "sha512-P2dlU/q51fkOc/Gfl3Ul9kicV7l+ra934qBFXCFhrZMOL6du1TM0pm1ThYvENukyOn5h9v+yMJ9Fn5JK4QozrQ==" + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", + "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", + "license": "MIT" }, "node_modules/@types/d3-scale": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.8.tgz", - "integrity": "sha512-gkK1VVTr5iNiYJ7vWDI+yUFFlszhNMtVeneJ6lUTKPjprsvLLI9/tgEGiXJOnlINJA8FyA88gfnQsHbybVZrYQ==", + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", + "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", + "license": "MIT", "dependencies": { "@types/d3-time": "*" } }, "node_modules/@types/d3-shape": { - "version": "3.1.6", - "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.6.tgz", - "integrity": "sha512-5KKk5aKGu2I+O6SONMYSNflgiP0WfZIQvVUMan50wHsLG1G94JlxEVnCpQARfTtzytuY0p/9PXXZb3I7giofIA==", + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.7.tgz", + "integrity": "sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==", + "license": "MIT", "dependencies": { "@types/d3-path": "*" } }, "node_modules/@types/d3-time": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.3.tgz", - "integrity": "sha512-2p6olUZ4w3s+07q3Tm2dbiMZy5pCDfYwtLXXHUnVzXgQlZ/OyPtUz6OL382BkOuGlLXqfT+wqv8Fw2v8/0geBw==" + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", + "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", + "license": "MIT" }, "node_modules/@types/d3-timer": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", - "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==" + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", + "license": "MIT" }, "node_modules/@types/estree": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", - "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", - "dev": true + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "license": "MIT" }, "node_modules/@types/json5": { "version": "0.0.29", "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", - "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==" - }, - "node_modules/@types/prop-types": { - "version": "15.7.12", - "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.12.tgz", - "integrity": "sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==", - "devOptional": true + "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", + "license": "MIT" }, "node_modules/@types/react": { - "version": "18.2.74", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.74.tgz", - "integrity": "sha512-9AEqNZZyBx8OdZpxzQlaFEVCSFUM2YXJH46yPOiOpm078k6ZLOCcuAzGum/zK8YBwY+dbahVNbHrbgrAwIRlqw==", - "devOptional": true, + "version": "19.1.9", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.9.tgz", + "integrity": "sha512-WmdoynAX8Stew/36uTSVMcLJJ1KRh6L3IZRx1PZ7qJtBqT3dYTgyDTx8H1qoRghErydW7xw9mSJ3wS//tCRpFA==", + "license": "MIT", "dependencies": { - "@types/prop-types": "*", "csstype": "^3.0.2" } }, "node_modules/@types/react-dom": { - "version": "18.3.0", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.0.tgz", - "integrity": "sha512-EhwApuTmMBmXuFOikhQLIBUn6uFg81SwLMOAUgodJF14SOBOCMdU04gDoYi0WOJJHD144TL32z4yDqCW3dnkQg==", - "dev": true, - "dependencies": { - "@types/react": "*" + "version": "19.1.7", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.1.7.tgz", + "integrity": "sha512-i5ZzwYpqjmrKenzkoLM2Ibzt6mAsM7pxB6BCIouEVVmgiqaMj1TjaK7hnA36hbW5aZv20kx7Lw6hWzPWg0Rurw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.0.0" } }, "node_modules/@types/semver": { - "version": "7.5.8", - "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.8.tgz", - "integrity": "sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==", - "dev": true + "version": "7.7.0", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.7.0.tgz", + "integrity": "sha512-k107IF4+Xr7UHjwDc7Cfd6PRQfbdkiRabXGRjo07b4WyPahFBZCZ1sE+BNxYIJPPg73UkfOsVOLwqVc/6ETrIA==", + "dev": true, + "license": "MIT" }, "node_modules/@types/validator": { - "version": "13.12.2", - "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.12.2.tgz", - "integrity": "sha512-6SlHBzUW8Jhf3liqrGGXyTJSIFe4nqlJ5A5KaMZ2l/vbM3Wh3KSybots/wfWVzNLK4D1NZluDlSQIbIEPx6oyA==", - "dev": true + "version": "13.15.2", + "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.15.2.tgz", + "integrity": "sha512-y7pa/oEJJ4iGYBxOpfAKn5b9+xuihvzDVnC/OSvlVnGxVg0pOqmjiMafiJ1KVNQEaPZf9HsEp5icEwGg8uIe5Q==", + "dev": true, + "license": "MIT" }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.28.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.28.0.tgz", - "integrity": "sha512-lvFK3TCGAHsItNdWZ/1FkvpzCxTHUVuFrdnOGLMa0GGCFIbCgQWVk3CzCGdA7kM3qGVc+dfW9tr0Z/sHnGDFyg==", + "version": "8.39.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.39.0.tgz", + "integrity": "sha512-bhEz6OZeUR+O/6yx9Jk6ohX6H9JSFTaiY0v9/PuKT3oGK0rn0jNplLmyFUGV+a9gfYnVNwGDwS/UkLIuXNb2Rw==", "dev": true, + "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.28.0", - "@typescript-eslint/type-utils": "8.28.0", - "@typescript-eslint/utils": "8.28.0", - "@typescript-eslint/visitor-keys": "8.28.0", + "@typescript-eslint/scope-manager": "8.39.0", + "@typescript-eslint/type-utils": "8.39.0", + "@typescript-eslint/utils": "8.39.0", + "@typescript-eslint/visitor-keys": "8.39.0", "graphemer": "^1.4.0", - "ignore": "^5.3.1", + "ignore": "^7.0.0", "natural-compare": "^1.4.0", - "ts-api-utils": "^2.0.1" + "ts-api-utils": "^2.1.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1494,21 +2020,32 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", + "@typescript-eslint/parser": "^8.39.0", "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.9.0" + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" } }, "node_modules/@typescript-eslint/parser": { - "version": "8.28.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.28.0.tgz", - "integrity": "sha512-LPcw1yHD3ToaDEoljFEfQ9j2xShY367h7FZ1sq5NJT9I3yj4LHer1Xd1yRSOdYy9BpsrxU7R+eoDokChYM53lQ==", + "version": "8.39.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.39.0.tgz", + "integrity": "sha512-g3WpVQHngx0aLXn6kfIYCZxM6rRJlWzEkVpqEFLT3SgEDsp9cpCbxxgwnE504q4H+ruSDh/VGS6nqZIDynP+vg==", "dev": true, + "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.28.0", - "@typescript-eslint/types": "8.28.0", - "@typescript-eslint/typescript-estree": "8.28.0", - "@typescript-eslint/visitor-keys": "8.28.0", + "@typescript-eslint/scope-manager": "8.39.0", + "@typescript-eslint/types": "8.39.0", + "@typescript-eslint/typescript-estree": "8.39.0", + "@typescript-eslint/visitor-keys": "8.39.0", "debug": "^4.3.4" }, "engines": { @@ -1520,17 +2057,40 @@ }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.9.0" + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.39.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.39.0.tgz", + "integrity": "sha512-CTzJqaSq30V/Z2Og9jogzZt8lJRR5TKlAdXmWgdu4hgcC9Kww5flQ+xFvMxIBWVNdxJO7OifgdOK4PokMIWPew==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.39.0", + "@typescript-eslint/types": "^8.39.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.28.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.28.0.tgz", - "integrity": "sha512-u2oITX3BJwzWCapoZ/pXw6BCOl8rJP4Ij/3wPoGvY8XwvXflOzd1kLrDUUUAIEdJSFh+ASwdTHqtan9xSg8buw==", + "version": "8.39.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.39.0.tgz", + "integrity": "sha512-8QOzff9UKxOh6npZQ/4FQu4mjdOCGSdO3p44ww0hk8Vu+IGbg0tB/H1LcTARRDzGCC8pDGbh2rissBuuoPgH8A==", "dev": true, + "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.28.0", - "@typescript-eslint/visitor-keys": "8.28.0" + "@typescript-eslint/types": "8.39.0", + "@typescript-eslint/visitor-keys": "8.39.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1540,16 +2100,35 @@ "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/@typescript-eslint/type-utils": { - "version": "8.28.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.28.0.tgz", - "integrity": "sha512-oRoXu2v0Rsy/VoOGhtWrOKDiIehvI+YNrDk5Oqj40Mwm0Yt01FC/Q7nFqg088d3yAsR1ZcZFVfPCTTFCe/KPwg==", + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.39.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.39.0.tgz", + "integrity": "sha512-Fd3/QjmFV2sKmvv3Mrj8r6N8CryYiCS8Wdb/6/rgOXAWGcFuc+VkQuG28uk/4kVNVZBQuuDHEDUpo/pQ32zsIQ==", "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.39.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.39.0.tgz", + "integrity": "sha512-6B3z0c1DXVT2vYA9+z9axjtc09rqKUPRmijD5m9iv8iQpHBRYRMBcgxSiKTZKm6FwWw1/cI4v6em35OsKCiN5Q==", + "dev": true, + "license": "MIT", "dependencies": { - "@typescript-eslint/typescript-estree": "8.28.0", - "@typescript-eslint/utils": "8.28.0", + "@typescript-eslint/types": "8.39.0", + "@typescript-eslint/typescript-estree": "8.39.0", + "@typescript-eslint/utils": "8.39.0", "debug": "^4.3.4", - "ts-api-utils": "^2.0.1" + "ts-api-utils": "^2.1.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1560,14 +2139,15 @@ }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.9.0" + "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/types": { - "version": "8.28.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.28.0.tgz", - "integrity": "sha512-bn4WS1bkKEjx7HqiwG2JNB3YJdC1q6Ue7GyGlwPHyt0TnVq6TtD/hiOdTZt71sq0s7UzqBFXD8t8o2e63tXgwA==", + "version": "8.39.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.39.0.tgz", + "integrity": "sha512-ArDdaOllnCj3yn/lzKn9s0pBQYmmyme/v1HbGIGB0GB/knFI3fWMHloC+oYTJW46tVbYnGKTMDK4ah1sC2v0Kg==", "dev": true, + "license": "MIT", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, @@ -1577,19 +2157,22 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.28.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.28.0.tgz", - "integrity": "sha512-H74nHEeBGeklctAVUvmDkxB1mk+PAZ9FiOMPFncdqeRBXxk1lWSYraHw8V12b7aa6Sg9HOBNbGdSHobBPuQSuA==", + "version": "8.39.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.39.0.tgz", + "integrity": "sha512-ndWdiflRMvfIgQRpckQQLiB5qAKQ7w++V4LlCHwp62eym1HLB/kw7D9f2e8ytONls/jt89TEasgvb+VwnRprsw==", "dev": true, + "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.28.0", - "@typescript-eslint/visitor-keys": "8.28.0", + "@typescript-eslint/project-service": "8.39.0", + "@typescript-eslint/tsconfig-utils": "8.39.0", + "@typescript-eslint/types": "8.39.0", + "@typescript-eslint/visitor-keys": "8.39.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", - "ts-api-utils": "^2.0.1" + "ts-api-utils": "^2.1.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1599,7 +2182,17 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "typescript": ">=4.8.4 <5.9.0" + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { @@ -1607,6 +2200,7 @@ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", "dev": true, + "license": "ISC", "dependencies": { "brace-expansion": "^2.0.1" }, @@ -1618,15 +2212,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.28.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.28.0.tgz", - "integrity": "sha512-OELa9hbTYciYITqgurT1u/SzpQVtDLmQMFzy/N8pQE+tefOyCWT79jHsav294aTqV1q1u+VzqDGbuujvRYaeSQ==", + "version": "8.39.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.39.0.tgz", + "integrity": "sha512-4GVSvNA0Vx1Ktwvf4sFE+exxJ3QGUorQG1/A5mRfRNZtkBT2xrA/BCO2H0eALx/PnvCS6/vmYwRdDA41EoffkQ==", "dev": true, + "license": "MIT", "dependencies": { - "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "8.28.0", - "@typescript-eslint/types": "8.28.0", - "@typescript-eslint/typescript-estree": "8.28.0" + "@eslint-community/eslint-utils": "^4.7.0", + "@typescript-eslint/scope-manager": "8.39.0", + "@typescript-eslint/types": "8.39.0", + "@typescript-eslint/typescript-estree": "8.39.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1637,17 +2232,18 @@ }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.9.0" + "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.28.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.28.0.tgz", - "integrity": "sha512-hbn8SZ8w4u2pRwgQ1GlUrPKE+t2XvcCW5tTRF7j6SMYIuYG37XuzIW44JCZPa36evi0Oy2SnM664BlIaAuQcvg==", + "version": "8.39.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.39.0.tgz", + "integrity": "sha512-ldgiJ+VAhQCfIjeOgu8Kj5nSxds0ktPOSO9p4+0VDH2R2pLvQraaM5Oen2d7NxzMCm+Sn/vJT+mv2H5u6b/3fA==", "dev": true, + "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.28.0", - "eslint-visitor-keys": "^4.2.0" + "@typescript-eslint/types": "8.39.0", + "eslint-visitor-keys": "^4.2.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1658,10 +2254,11 @@ } }, "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", - "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", "dev": true, + "license": "Apache-2.0", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, @@ -1669,27 +2266,37 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/@ungap/structured-clone": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", - "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==" - }, - "node_modules/@vitejs/plugin-react-swc": { - "version": "3.8.0", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-react-swc/-/plugin-react-swc-3.8.0.tgz", - "integrity": "sha512-T4sHPvS+DIqDP51ifPqa9XIRAz/kIvIi8oXcnOZZgHmMotgmmdxe/DD5tMFlt5nuIRzT0/QuiwmKlH0503Aapw==", - "dev": true, - "dependencies": { - "@swc/core": "^1.10.15" + "node_modules/@vitejs/plugin-basic-ssl": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-basic-ssl/-/plugin-basic-ssl-2.1.0.tgz", + "integrity": "sha512-dOxxrhgyDIEUADhb/8OlV9JIqYLgos03YorAueTIeOUskLJSEsfwCByjbu98ctXitUN3znXKp0bYD/WHSudCeA==", + "license": "MIT", + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" }, "peerDependencies": { - "vite": "^4 || ^5 || ^6" + "vite": "^6.0.0 || ^7.0.0" + } + }, + "node_modules/@vitejs/plugin-react-swc": { + "version": "3.11.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react-swc/-/plugin-react-swc-3.11.0.tgz", + "integrity": "sha512-YTJCGFdNMHCMfjODYtxRNVAYmTWQ1Lb8PulP/2/f/oEEtglw8oKxKIZmmRkyXrVrHfsKOaVkAc3NT9/dMutO5w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rolldown/pluginutils": "1.0.0-beta.27", + "@swc/core": "^1.12.11" + }, + "peerDependencies": { + "vite": "^4 || ^5 || ^6 || ^7" } }, "node_modules/@xterm/addon-clipboard": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/@xterm/addon-clipboard/-/addon-clipboard-0.1.0.tgz", "integrity": "sha512-zdoM7p53T5sv/HbRTyp4hY0kKmEQ3MZvAvEtiXqNIHc/JdpqwByCtsTaQF5DX2n4hYdXRPO4P/eOS0QEhX1nPw==", + "license": "MIT", "dependencies": { "js-base64": "^3.7.5" }, @@ -1701,6 +2308,7 @@ "version": "0.10.0", "resolved": "https://registry.npmjs.org/@xterm/addon-fit/-/addon-fit-0.10.0.tgz", "integrity": "sha512-UFYkDm4HUahf2lnEyHvio51TNGiLK66mqP2JoATy7hRZeXaGMRDr00JiSF7m63vR5WKATF605yEggJKsw0JpMQ==", + "license": "MIT", "peerDependencies": { "@xterm/xterm": "^5.0.0" } @@ -1709,6 +2317,7 @@ "version": "0.8.0", "resolved": "https://registry.npmjs.org/@xterm/addon-unicode11/-/addon-unicode11-0.8.0.tgz", "integrity": "sha512-LxinXu8SC4OmVa6FhgwsVCBZbr8WoSGzBl2+vqe8WcQ6hb1r6Gj9P99qTNdPiFPh4Ceiu2pC8xukZ6+2nnh49Q==", + "license": "MIT", "peerDependencies": { "@xterm/xterm": "^5.0.0" } @@ -1717,6 +2326,7 @@ "version": "0.11.0", "resolved": "https://registry.npmjs.org/@xterm/addon-web-links/-/addon-web-links-0.11.0.tgz", "integrity": "sha512-nIHQ38pQI+a5kXnRaTgwqSHnX7KE6+4SVoceompgHL26unAxdfP6IPqUTSYPQgSwM56hsElfoNrrW5V7BUED/Q==", + "license": "MIT", "peerDependencies": { "@xterm/xterm": "^5.0.0" } @@ -1725,6 +2335,7 @@ "version": "0.18.0", "resolved": "https://registry.npmjs.org/@xterm/addon-webgl/-/addon-webgl-0.18.0.tgz", "integrity": "sha512-xCnfMBTI+/HKPdRnSOHaJDRqEpq2Ugy8LEj9GiY4J3zJObo3joylIFaMvzBwbYRg8zLtkO0KQaStCeSfoaI2/w==", + "license": "MIT", "peerDependencies": { "@xterm/xterm": "^5.0.0" } @@ -1732,12 +2343,14 @@ "node_modules/@xterm/xterm": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.5.0.tgz", - "integrity": "sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A==" + "integrity": "sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A==", + "license": "MIT" }, "node_modules/acorn": { - "version": "8.11.3", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", - "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "license": "MIT", "bin": { "acorn": "bin/acorn" }, @@ -1749,6 +2362,7 @@ "version": "5.3.2", "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "license": "MIT", "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } @@ -1757,6 +2371,7 @@ "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -1768,18 +2383,11 @@ "url": "https://github.com/sponsors/epoberezkin" } }, - "node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "engines": { - "node": ">=8" - } - }, "node_modules/ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", "dependencies": { "color-convert": "^2.0.1" }, @@ -1790,37 +2398,17 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/any-promise": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", - "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==" - }, - "node_modules/anymatch": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", - "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", - "dependencies": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/arg": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", - "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==" - }, "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" }, "node_modules/array-buffer-byte-length": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", + "license": "MIT", "dependencies": { "call-bound": "^1.0.3", "is-array-buffer": "^3.0.5" @@ -1833,16 +2421,19 @@ } }, "node_modules/array-includes": { - "version": "3.1.8", - "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.8.tgz", - "integrity": "sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ==", + "version": "3.1.9", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.9.tgz", + "integrity": "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==", + "license": "MIT", "dependencies": { - "call-bind": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", "define-properties": "^1.2.1", - "es-abstract": "^1.23.2", - "es-object-atoms": "^1.0.0", - "get-intrinsic": "^1.2.4", - "is-string": "^1.0.7" + "es-abstract": "^1.24.0", + "es-object-atoms": "^1.1.1", + "get-intrinsic": "^1.3.0", + "is-string": "^1.1.1", + "math-intrinsics": "^1.1.0" }, "engines": { "node": ">= 0.4" @@ -1856,6 +2447,7 @@ "resolved": "https://registry.npmjs.org/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz", "integrity": "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==", "dev": true, + "license": "MIT", "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", @@ -1875,6 +2467,7 @@ "version": "1.2.6", "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.6.tgz", "integrity": "sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==", + "license": "MIT", "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.4", @@ -1892,14 +2485,15 @@ } }, "node_modules/array.prototype.flat": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.2.tgz", - "integrity": "sha512-djYB+Zx2vLewY8RWlNCUdHjDXs2XOgm602S9E7P/UpHgfeHL00cRiIF+IN/G/aUJ7kGPb6yO/ErDI5V2s8iycA==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz", + "integrity": "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==", + "license": "MIT", "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "es-shim-unscopables": "^1.0.0" + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -1912,6 +2506,7 @@ "version": "1.3.3", "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz", "integrity": "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==", + "license": "MIT", "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", @@ -1930,6 +2525,7 @@ "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz", "integrity": "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==", "dev": true, + "license": "MIT", "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", @@ -1945,6 +2541,7 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", + "license": "MIT", "dependencies": { "array-buffer-byte-length": "^1.0.1", "call-bind": "^1.0.8", @@ -1965,14 +2562,15 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", + "license": "MIT", "engines": { "node": ">= 0.4" } }, "node_modules/autoprefixer": { - "version": "10.4.20", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.20.tgz", - "integrity": "sha512-XY25y5xSv/wEoqzDyXXME4AFfkZI0P23z6Fs3YgymDnKJkCGOnkL0iTxCa85UTqaSgfcqyf3UA6+c7wUvx/16g==", + "version": "10.4.21", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.21.tgz", + "integrity": "sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ==", "dev": true, "funding": [ { @@ -1988,12 +2586,13 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "dependencies": { - "browserslist": "^4.23.3", - "caniuse-lite": "^1.0.30001646", + "browserslist": "^4.24.4", + "caniuse-lite": "^1.0.30001702", "fraction.js": "^4.3.7", "normalize-range": "^0.1.2", - "picocolors": "^1.0.1", + "picocolors": "^1.1.1", "postcss-value-parser": "^4.2.0" }, "bin": { @@ -2010,6 +2609,7 @@ "version": "1.0.7", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "license": "MIT", "dependencies": { "possible-typed-array-names": "^1.0.0" }, @@ -2023,31 +2623,25 @@ "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" - }, - "node_modules/binary-extensions": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", - "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT" }, "node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "license": "MIT", "dependencies": { - "balanced-match": "^1.0.0" + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" } }, "node_modules/braces": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", "dependencies": { "fill-range": "^7.1.1" }, @@ -2056,9 +2650,9 @@ } }, "node_modules/browserslist": { - "version": "4.24.3", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.3.tgz", - "integrity": "sha512-1CPmv8iobE2fyRMV97dAcMVegvvWKxmq94hkLiAkUGwKVTyDLw33K+ZxiFrREKmmps4rIw6grcCFCnTMSZ/YiA==", + "version": "4.25.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.1.tgz", + "integrity": "sha512-KGj0KoOMXLpSNkkEI6Z6mShmQy0bc1I+T7K9N81k4WWMrfz+6fQ6es80B/YLAeRoKvjYE1YSHHOW1qe9xIVzHw==", "dev": true, "funding": [ { @@ -2074,11 +2668,12 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "dependencies": { - "caniuse-lite": "^1.0.30001688", - "electron-to-chromium": "^1.5.73", + "caniuse-lite": "^1.0.30001726", + "electron-to-chromium": "^1.5.173", "node-releases": "^2.0.19", - "update-browserslist-db": "^1.1.1" + "update-browserslist-db": "^1.1.3" }, "bin": { "browserslist": "cli.js" @@ -2091,6 +2686,7 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.0", "es-define-property": "^1.0.0", @@ -2108,6 +2704,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" @@ -2120,6 +2717,7 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" @@ -2135,22 +2733,15 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "license": "MIT", "engines": { "node": ">=6" } }, - "node_modules/camelcase-css": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", - "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", - "engines": { - "node": ">= 6" - } - }, "node_modules/caniuse-lite": { - "version": "1.0.30001690", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001690.tgz", - "integrity": "sha512-5ExiE3qQN6oF8Clf8ifIDcMRCRE/dMGcETG/XGMD8/XiXm6HXQgQTh1yZYLXXpSOsEUlJm1Xr7kGULZTuGtP/w==", + "version": "1.0.30001731", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001731.tgz", + "integrity": "sha512-lDdp2/wrOmTRWuoB5DpfNkC0rJDU8DqRa6nYL6HK6sytw70QMopt/NIc/9SM7ylItlBWfACXk0tEn37UWM/+mg==", "dev": true, "funding": [ { @@ -2165,12 +2756,14 @@ "type": "github", "url": "https://github.com/sponsors/ai" } - ] + ], + "license": "CC-BY-4.0" }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "license": "MIT", "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -2182,44 +2775,21 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/chokidar": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", - "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", - "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" - }, + "node_modules/chownr": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "dev": true, + "license": "BlueOak-1.0.0", "engines": { - "node": ">= 8.10.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - } - }, - "node_modules/chokidar/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" + "node": ">=18" } }, "node_modules/clsx": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.0.0.tgz", - "integrity": "sha512-rQ1+kcj+ttHG0MKVGBUXwayCCF1oh39BF5COIpRzuCEv8Mwjv0XucrI2ExNTOn9IlLifGClWQcU9BrZORvtw6Q==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", "engines": { "node": ">=6" } @@ -2228,6 +2798,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", "dependencies": { "color-name": "~1.1.4" }, @@ -2238,25 +2809,20 @@ "node_modules/color-name": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" - }, - "node_modules/commander": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", - "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", - "engines": { - "node": ">= 6" - } + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "license": "MIT" }, "node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", @@ -2270,6 +2836,8 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "license": "MIT", "bin": { "cssesc": "bin/cssesc" }, @@ -2280,20 +2848,22 @@ "node_modules/csstype": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", - "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "license": "MIT" }, "node_modules/cva": { - "version": "1.0.0-beta.1", - "resolved": "https://registry.npmjs.org/cva/-/cva-1.0.0-beta.1.tgz", - "integrity": "sha512-gznFqTgERU9q4wg7jfgqtt34+RUt9S5t0xDAAEuDwQEAXEgjdDkKXpLLNjwSxsB4Ln/sqWJEH7yhE8Ny0mxd0w==", + "version": "1.0.0-beta.4", + "resolved": "https://registry.npmjs.org/cva/-/cva-1.0.0-beta.4.tgz", + "integrity": "sha512-F/JS9hScapq4DBVQXcK85l9U91M6ePeXoBMSp7vypzShoefUBxjQTo3g3935PUHgQd+IW77DjbPRIxugy4/GCQ==", + "license": "Apache-2.0", "dependencies": { - "clsx": "2.0.0" + "clsx": "^2.1.1" }, "funding": { - "url": "https://joebell.co.uk/sponsors" + "url": "https://polar.sh/cva" }, "peerDependencies": { - "typescript": ">= 4.5.5 < 6" + "typescript": ">= 4.5.5" }, "peerDependenciesMeta": { "typescript": { @@ -2305,6 +2875,7 @@ "version": "3.2.4", "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "license": "ISC", "dependencies": { "internmap": "1 - 2" }, @@ -2316,6 +2887,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", "engines": { "node": ">=12" } @@ -2324,6 +2896,7 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "license": "BSD-3-Clause", "engines": { "node": ">=12" } @@ -2332,6 +2905,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz", "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==", + "license": "ISC", "engines": { "node": ">=12" } @@ -2340,6 +2914,7 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", "dependencies": { "d3-color": "1 - 3" }, @@ -2351,6 +2926,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "license": "ISC", "engines": { "node": ">=12" } @@ -2359,6 +2935,7 @@ "version": "4.0.2", "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "license": "ISC", "dependencies": { "d3-array": "2.10.0 - 3", "d3-format": "1 - 3", @@ -2374,6 +2951,7 @@ "version": "3.2.0", "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "license": "ISC", "dependencies": { "d3-path": "^3.1.0" }, @@ -2385,6 +2963,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "license": "ISC", "dependencies": { "d3-array": "2 - 3" }, @@ -2396,6 +2975,7 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "license": "ISC", "dependencies": { "d3-time": "1 - 3" }, @@ -2407,6 +2987,7 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", "engines": { "node": ">=12" } @@ -2415,6 +2996,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", + "license": "MIT", "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", @@ -2431,6 +3013,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", + "license": "MIT", "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", @@ -2447,6 +3030,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", + "license": "MIT", "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", @@ -2459,12 +3043,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/dayjs": { + "version": "1.11.13", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz", + "integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==", + "license": "MIT" + }, "node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "license": "MIT", "dependencies": { - "ms": "2.1.2" + "ms": "^2.1.3" }, "engines": { "node": ">=6.0" @@ -2478,17 +3069,20 @@ "node_modules/decimal.js-light": { "version": "2.5.1", "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz", - "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==" + "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==", + "license": "MIT" }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==" + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "license": "MIT" }, "node_modules/define-data-property": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "license": "MIT", "dependencies": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", @@ -2505,6 +3099,7 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "license": "MIT", "dependencies": { "define-data-property": "^1.0.1", "has-property-descriptors": "^1.0.0", @@ -2517,31 +3112,33 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/didyoumean": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", - "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==" - }, - "node_modules/dlv": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", - "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==" + "node_modules/detect-libc": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz", + "integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==", + "devOptional": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } }, "node_modules/doctrine": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", - "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "license": "Apache-2.0", "dependencies": { "esutils": "^2.0.2" }, "engines": { - "node": ">=6.0.0" + "node": ">=0.10.0" } }, "node_modules/dom-helpers": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", + "license": "MIT", "dependencies": { "@babel/runtime": "^7.8.7", "csstype": "^3.0.2" @@ -2551,6 +3148,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", @@ -2560,43 +3158,49 @@ "node": ">= 0.4" } }, - "node_modules/eastasianwidth": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", - "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==" - }, "node_modules/electron-to-chromium": { - "version": "1.5.75", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.75.tgz", - "integrity": "sha512-Lf3++DumRE/QmweGjU+ZcKqQ+3bKkU/qjaKYhIJKEOhgIO9Xs6IiAQFkfFoj+RhgDk4LUeNsLo6plExHqSyu6Q==", - "dev": true + "version": "1.5.198", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.198.tgz", + "integrity": "sha512-G5COfnp3w+ydVu80yprgWSfmfQaYRh9DOxfhAxstLyetKaLyl55QrNjx8C38Pc/C+RaDmb1M0Lk8wPEMQ+bGgQ==", + "dev": true, + "license": "ISC" }, - "node_modules/emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==" + "node_modules/enhanced-resolve": { + "version": "5.18.3", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz", + "integrity": "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } }, "node_modules/es-abstract": { - "version": "1.23.9", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.9.tgz", - "integrity": "sha512-py07lI0wjxAC/DcfK1S6G7iANonniZwTISvdPzk9hzeH0IZIshbuuFxLIU96OyF89Yb9hiqWn8M/bY83KY5vzA==", + "version": "1.24.0", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.0.tgz", + "integrity": "sha512-WSzPgsdLtTcQwm4CROfS5ju2Wa1QQcVeT37jFjYzdFz1r9ahadC8B8/a4qxJxM+09F18iumCdRmlr96ZYkQvEg==", + "license": "MIT", "dependencies": { "array-buffer-byte-length": "^1.0.2", "arraybuffer.prototype.slice": "^1.0.4", "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", - "call-bound": "^1.0.3", + "call-bound": "^1.0.4", "data-view-buffer": "^1.0.2", "data-view-byte-length": "^1.0.2", "data-view-byte-offset": "^1.0.1", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0", + "es-object-atoms": "^1.1.1", "es-set-tostringtag": "^2.1.0", "es-to-primitive": "^1.3.0", "function.prototype.name": "^1.1.8", - "get-intrinsic": "^1.2.7", - "get-proto": "^1.0.0", + "get-intrinsic": "^1.3.0", + "get-proto": "^1.0.1", "get-symbol-description": "^1.1.0", "globalthis": "^1.0.4", "gopd": "^1.2.0", @@ -2608,21 +3212,24 @@ "is-array-buffer": "^3.0.5", "is-callable": "^1.2.7", "is-data-view": "^1.0.2", + "is-negative-zero": "^2.0.3", "is-regex": "^1.2.1", + "is-set": "^2.0.3", "is-shared-array-buffer": "^1.0.4", "is-string": "^1.1.1", "is-typed-array": "^1.1.15", - "is-weakref": "^1.1.0", + "is-weakref": "^1.1.1", "math-intrinsics": "^1.1.0", - "object-inspect": "^1.13.3", + "object-inspect": "^1.13.4", "object-keys": "^1.1.1", "object.assign": "^4.1.7", "own-keys": "^1.0.1", - "regexp.prototype.flags": "^1.5.3", + "regexp.prototype.flags": "^1.5.4", "safe-array-concat": "^1.1.3", "safe-push-apply": "^1.0.0", "safe-regex-test": "^1.1.0", "set-proto": "^1.0.0", + "stop-iteration-iterator": "^1.1.0", "string.prototype.trim": "^1.2.10", "string.prototype.trimend": "^1.0.9", "string.prototype.trimstart": "^1.0.8", @@ -2631,7 +3238,7 @@ "typed-array-byte-offset": "^1.0.4", "typed-array-length": "^1.0.7", "unbox-primitive": "^1.1.0", - "which-typed-array": "^1.1.18" + "which-typed-array": "^1.1.19" }, "engines": { "node": ">= 0.4" @@ -2644,6 +3251,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", "engines": { "node": ">= 0.4" } @@ -2652,6 +3260,7 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", "engines": { "node": ">= 0.4" } @@ -2661,6 +3270,7 @@ "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.2.1.tgz", "integrity": "sha512-uDn+FE1yrDzyC0pCo961B2IHbdM8y/ACZsKD4dG6WqrjV53BADjwa7D+1aom2rsNVfLyDgU/eigvlJGJ08OQ4w==", "dev": true, + "license": "MIT", "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", @@ -2687,6 +3297,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", "dependencies": { "es-errors": "^1.3.0" }, @@ -2698,6 +3309,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", @@ -2712,6 +3324,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz", "integrity": "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==", + "license": "MIT", "dependencies": { "hasown": "^2.0.2" }, @@ -2723,6 +3336,7 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz", "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==", + "license": "MIT", "dependencies": { "is-callable": "^1.2.7", "is-date-object": "^1.0.5", @@ -2736,41 +3350,44 @@ } }, "node_modules/esbuild": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.20.2.tgz", - "integrity": "sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==", - "dev": true, + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.8.tgz", + "integrity": "sha512-vVC0USHGtMi8+R4Kz8rt6JhEWLxsv9Rnu/lGYbPR8u47B+DCBksq9JarW0zOO7bs37hyOK1l2/oqtbciutL5+Q==", "hasInstallScript": true, + "license": "MIT", "bin": { "esbuild": "bin/esbuild" }, "engines": { - "node": ">=12" + "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.20.2", - "@esbuild/android-arm": "0.20.2", - "@esbuild/android-arm64": "0.20.2", - "@esbuild/android-x64": "0.20.2", - "@esbuild/darwin-arm64": "0.20.2", - "@esbuild/darwin-x64": "0.20.2", - "@esbuild/freebsd-arm64": "0.20.2", - "@esbuild/freebsd-x64": "0.20.2", - "@esbuild/linux-arm": "0.20.2", - "@esbuild/linux-arm64": "0.20.2", - "@esbuild/linux-ia32": "0.20.2", - "@esbuild/linux-loong64": "0.20.2", - "@esbuild/linux-mips64el": "0.20.2", - "@esbuild/linux-ppc64": "0.20.2", - "@esbuild/linux-riscv64": "0.20.2", - "@esbuild/linux-s390x": "0.20.2", - "@esbuild/linux-x64": "0.20.2", - "@esbuild/netbsd-x64": "0.20.2", - "@esbuild/openbsd-x64": "0.20.2", - "@esbuild/sunos-x64": "0.20.2", - "@esbuild/win32-arm64": "0.20.2", - "@esbuild/win32-ia32": "0.20.2", - "@esbuild/win32-x64": "0.20.2" + "@esbuild/aix-ppc64": "0.25.8", + "@esbuild/android-arm": "0.25.8", + "@esbuild/android-arm64": "0.25.8", + "@esbuild/android-x64": "0.25.8", + "@esbuild/darwin-arm64": "0.25.8", + "@esbuild/darwin-x64": "0.25.8", + "@esbuild/freebsd-arm64": "0.25.8", + "@esbuild/freebsd-x64": "0.25.8", + "@esbuild/linux-arm": "0.25.8", + "@esbuild/linux-arm64": "0.25.8", + "@esbuild/linux-ia32": "0.25.8", + "@esbuild/linux-loong64": "0.25.8", + "@esbuild/linux-mips64el": "0.25.8", + "@esbuild/linux-ppc64": "0.25.8", + "@esbuild/linux-riscv64": "0.25.8", + "@esbuild/linux-s390x": "0.25.8", + "@esbuild/linux-x64": "0.25.8", + "@esbuild/netbsd-arm64": "0.25.8", + "@esbuild/netbsd-x64": "0.25.8", + "@esbuild/openbsd-arm64": "0.25.8", + "@esbuild/openbsd-x64": "0.25.8", + "@esbuild/openharmony-arm64": "0.25.8", + "@esbuild/sunos-x64": "0.25.8", + "@esbuild/win32-arm64": "0.25.8", + "@esbuild/win32-ia32": "0.25.8", + "@esbuild/win32-x64": "0.25.8" } }, "node_modules/escalade": { @@ -2778,6 +3395,7 @@ "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" } @@ -2786,6 +3404,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "license": "MIT", "engines": { "node": ">=10" }, @@ -2794,67 +3413,77 @@ } }, "node_modules/eslint": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz", - "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==", + "version": "9.32.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.32.0.tgz", + "integrity": "sha512-LSehfdpgMeWcTZkWZVIJl+tkZ2nuSkyyB9C27MZqFWXuph7DvaowgcTvKqxvpLW1JZIk8PN7hFY3Rj9LQ7m7lg==", + "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.6.1", - "@eslint/eslintrc": "^2.1.4", - "@eslint/js": "8.57.0", - "@humanwhocodes/config-array": "^0.11.14", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.0", + "@eslint/config-helpers": "^0.3.0", + "@eslint/core": "^0.15.0", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.32.0", + "@eslint/plugin-kit": "^0.3.4", + "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", - "@nodelib/fs.walk": "^1.2.8", - "@ungap/structured-clone": "^1.2.0", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "@types/json-schema": "^7.0.15", "ajv": "^6.12.4", "chalk": "^4.0.0", - "cross-spawn": "^7.0.2", + "cross-spawn": "^7.0.6", "debug": "^4.3.2", - "doctrine": "^3.0.0", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^7.2.2", - "eslint-visitor-keys": "^3.4.3", - "espree": "^9.6.1", - "esquery": "^1.4.2", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^6.0.1", + "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", - "globals": "^13.19.0", - "graphemer": "^1.4.0", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", - "is-path-inside": "^3.0.3", - "js-yaml": "^4.1.0", "json-stable-stringify-without-jsonify": "^1.0.1", - "levn": "^0.4.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", - "optionator": "^0.9.3", - "strip-ansi": "^6.0.1", - "text-table": "^0.2.0" + "optionator": "^0.9.3" }, "bin": { "eslint": "bin/eslint.js" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { - "url": "https://opencollective.com/eslint" + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } } }, "node_modules/eslint-config-prettier": { - "version": "10.1.1", - "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.1.tgz", - "integrity": "sha512-4EQQr6wXwS+ZJSzaR5ZCrYgLxqvUjdXctaEtBqHcbkW944B1NQyO4qpdHQbXBONfwxXdkAY81HH4+LUfrg+zPw==", + "version": "10.1.8", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.8.tgz", + "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", "dev": true, + "license": "MIT", "bin": { "eslint-config-prettier": "bin/cli.js" }, + "funding": { + "url": "https://opencollective.com/eslint-config-prettier" + }, "peerDependencies": { "eslint": ">=7.0.0" } @@ -2863,6 +3492,7 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/eslint-import-resolver-alias/-/eslint-import-resolver-alias-1.1.2.tgz", "integrity": "sha512-WdviM1Eu834zsfjHtcGHtGfcu+F30Od3V7I9Fi57uhBEwPkjDcii7/yW8jAT+gOhn4P/vOxxNAXbFAKsrrc15w==", + "license": "MIT", "engines": { "node": ">= 4" }, @@ -2874,6 +3504,7 @@ "version": "0.3.9", "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz", "integrity": "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==", + "license": "MIT", "dependencies": { "debug": "^3.2.7", "is-core-module": "^2.13.0", @@ -2884,33 +3515,16 @@ "version": "3.2.7", "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "license": "MIT", "dependencies": { "ms": "^2.1.1" } }, - "node_modules/eslint-import-resolver-node/node_modules/resolve": { - "version": "1.22.10", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", - "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", - "dependencies": { - "is-core-module": "^2.16.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/eslint-module-utils": { - "version": "2.12.0", - "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.0.tgz", - "integrity": "sha512-wALZ0HFoytlyh/1+4wuZ9FJCD/leWHQzzrxJ8+rebyReSLk7LApMyd3WJaLVoN+D5+WIdJyDK1c6JnE65V4Zyg==", + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.1.tgz", + "integrity": "sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw==", + "license": "MIT", "dependencies": { "debug": "^3.2.7" }, @@ -2927,33 +3541,35 @@ "version": "3.2.7", "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "license": "MIT", "dependencies": { "ms": "^2.1.1" } }, "node_modules/eslint-plugin-import": { - "version": "2.31.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.31.0.tgz", - "integrity": "sha512-ixmkI62Rbc2/w8Vfxyh1jQRTdRTF52VxwRVHl/ykPAmqG+Nb7/kNn+byLP0LxPgI7zWA16Jt82SybJInmMia3A==", + "version": "2.32.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.32.0.tgz", + "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", + "license": "MIT", "dependencies": { "@rtsao/scc": "^1.1.0", - "array-includes": "^3.1.8", - "array.prototype.findlastindex": "^1.2.5", - "array.prototype.flat": "^1.3.2", - "array.prototype.flatmap": "^1.3.2", + "array-includes": "^3.1.9", + "array.prototype.findlastindex": "^1.2.6", + "array.prototype.flat": "^1.3.3", + "array.prototype.flatmap": "^1.3.3", "debug": "^3.2.7", "doctrine": "^2.1.0", "eslint-import-resolver-node": "^0.3.9", - "eslint-module-utils": "^2.12.0", + "eslint-module-utils": "^2.12.1", "hasown": "^2.0.2", - "is-core-module": "^2.15.1", + "is-core-module": "^2.16.1", "is-glob": "^4.0.3", "minimatch": "^3.1.2", "object.fromentries": "^2.0.8", "object.groupby": "^1.0.3", - "object.values": "^1.2.0", + "object.values": "^1.2.1", "semver": "^6.3.1", - "string.prototype.trimend": "^1.0.8", + "string.prototype.trimend": "^1.0.9", "tsconfig-paths": "^3.15.0" }, "engines": { @@ -2963,58 +3579,30 @@ "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9" } }, - "node_modules/eslint-plugin-import/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, "node_modules/eslint-plugin-import/node_modules/debug": { "version": "3.2.7", "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "license": "MIT", "dependencies": { "ms": "^2.1.1" } }, - "node_modules/eslint-plugin-import/node_modules/doctrine": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", - "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", - "dependencies": { - "esutils": "^2.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/eslint-plugin-import/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, "node_modules/eslint-plugin-import/node_modules/semver": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", "bin": { "semver": "bin/semver.js" } }, "node_modules/eslint-plugin-react": { - "version": "7.37.4", - "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.4.tgz", - "integrity": "sha512-BGP0jRmfYyvOyvMoRX/uoUeW+GqNj9y16bPQzqAHf3AYII/tDs+jMN0dBVkl88/OZwNGwrVFxE7riHsXVfy/LQ==", + "version": "7.37.5", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.5.tgz", + "integrity": "sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==", "dev": true, + "license": "MIT", "dependencies": { "array-includes": "^3.1.8", "array.prototype.findlast": "^1.2.5", @@ -3026,7 +3614,7 @@ "hasown": "^2.0.2", "jsx-ast-utils": "^2.4.1 || ^3.0.0", "minimatch": "^3.1.2", - "object.entries": "^1.1.8", + "object.entries": "^1.1.9", "object.fromentries": "^2.0.8", "object.values": "^1.2.1", "prop-types": "^15.8.1", @@ -3047,6 +3635,7 @@ "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.2.0.tgz", "integrity": "sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==", "dev": true, + "license": "MIT", "engines": { "node": ">=10" }, @@ -3055,46 +3644,31 @@ } }, "node_modules/eslint-plugin-react-refresh": { - "version": "0.4.19", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.19.tgz", - "integrity": "sha512-eyy8pcr/YxSYjBoqIFSrlbn9i/xvxUFa8CjzAYo9cFjgGXqq1hyjihcpZvxRLalpaWmueWR81xn7vuKmAFijDQ==", + "version": "0.4.20", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.20.tgz", + "integrity": "sha512-XpbHQ2q5gUF8BGOX4dHe+71qoirYMhApEPZ7sfhF/dNnOF1UXnCMGZf79SFTBO7Bz5YEIT4TMieSlJBWhP9WBA==", "dev": true, + "license": "MIT", "peerDependencies": { "eslint": ">=8.40" } }, - "node_modules/eslint-plugin-react/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "node_modules/eslint-plugin-react/node_modules/resolve": { + "version": "2.0.0-next.5", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.5.tgz", + "integrity": "sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==", "dev": true, + "license": "MIT", "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/eslint-plugin-react/node_modules/doctrine": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", - "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", - "dev": true, - "dependencies": { - "esutils": "^2.0.2" + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/eslint-plugin-react/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" + "bin": { + "resolve": "bin/resolve" }, - "engines": { - "node": "*" + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/eslint-plugin-react/node_modules/semver": { @@ -3102,20 +3676,22 @@ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, + "license": "ISC", "bin": { "semver": "bin/semver.js" } }, "node_modules/eslint-scope": { - "version": "7.2.2", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", - "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "license": "BSD-2-Clause", "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "url": "https://opencollective.com/eslint" @@ -3125,6 +3701,7 @@ "version": "3.4.3", "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "license": "Apache-2.0", "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, @@ -3132,46 +3709,52 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/eslint/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/eslint/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dependencies": { - "brace-expansion": "^1.1.7" - }, + "node_modules/eslint/node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "license": "Apache-2.0", "engines": { - "node": "*" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" } }, "node_modules/espree": { - "version": "9.6.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", - "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "license": "BSD-2-Clause", "dependencies": { - "acorn": "^8.9.0", + "acorn": "^8.15.0", "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^3.4.1" + "eslint-visitor-keys": "^4.2.1" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree/node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "url": "https://opencollective.com/eslint" } }, "node_modules/esquery": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", - "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "license": "BSD-3-Clause", "dependencies": { "estraverse": "^5.1.0" }, @@ -3183,6 +3766,7 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "license": "BSD-2-Clause", "dependencies": { "estraverse": "^5.2.0" }, @@ -3194,6 +3778,7 @@ "version": "5.3.0", "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "license": "BSD-2-Clause", "engines": { "node": ">=4.0" } @@ -3202,6 +3787,7 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "license": "BSD-2-Clause", "engines": { "node": ">=0.10.0" } @@ -3209,31 +3795,36 @@ "node_modules/eventemitter3": { "version": "4.0.7", "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", - "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==" + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "license": "MIT" }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" }, "node_modules/fast-equals": { "version": "5.2.2", "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.2.2.tgz", "integrity": "sha512-V7/RktU11J3I36Nwq2JnZEM7tNm17eBJz+u25qdxBZeCKiX6BkVSZQjwWIr+IobgnZy+ag73tTZgZi7tr0LrBw==", + "license": "MIT", "engines": { "node": ">=6.0.0" } }, "node_modules/fast-glob": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", - "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", - "micromatch": "^4.0.4" + "micromatch": "^4.0.8" }, "engines": { "node": ">=8.6.0" @@ -3243,6 +3834,8 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", "dependencies": { "is-glob": "^4.0.1" }, @@ -3253,36 +3846,43 @@ "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "license": "MIT" }, "node_modules/fast-levenshtein": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==" + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "license": "MIT" }, "node_modules/fastq": { - "version": "1.17.1", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", - "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "dev": true, + "license": "ISC", "dependencies": { "reusify": "^1.0.4" } }, "node_modules/file-entry-cache": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", - "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "license": "MIT", "dependencies": { - "flat-cache": "^3.0.4" + "flat-cache": "^4.0.0" }, "engines": { - "node": "^10.12.0 || >=12.0.0" + "node": ">=16.0.0" } }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", "dependencies": { "to-regex-range": "^5.0.1" }, @@ -3294,6 +3894,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "license": "MIT", "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" @@ -3306,49 +3907,54 @@ } }, "node_modules/flat-cache": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", - "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "license": "MIT", "dependencies": { "flatted": "^3.2.9", - "keyv": "^4.5.3", - "rimraf": "^3.0.2" + "keyv": "^4.5.4" }, "engines": { - "node": "^10.12.0 || >=12.0.0" + "node": ">=16" } }, "node_modules/flatted": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz", - "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==" + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "license": "ISC" }, "node_modules/focus-trap": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/focus-trap/-/focus-trap-7.5.4.tgz", - "integrity": "sha512-N7kHdlgsO/v+iD/dMoJKtsSqs5Dz/dXZVebRgJw23LDk+jMi/974zyiOYDziY2JPp8xivq9BmUGwIJMiuSBi7w==", + "version": "7.6.5", + "resolved": "https://registry.npmjs.org/focus-trap/-/focus-trap-7.6.5.tgz", + "integrity": "sha512-7Ke1jyybbbPZyZXFxEftUtxFGLMpE2n6A+z//m4CRDlj0hW+o3iYSmh8nFlYMurOiJVDmJRilUQtJr08KfIxlg==", + "license": "MIT", "dependencies": { "tabbable": "^6.2.0" } }, "node_modules/focus-trap-react": { - "version": "10.2.3", - "resolved": "https://registry.npmjs.org/focus-trap-react/-/focus-trap-react-10.2.3.tgz", - "integrity": "sha512-YXBpFu/hIeSu6NnmV2xlXzOYxuWkoOtar9jzgp3lOmjWLWY59C/b8DtDHEAV4SPU07Nd/t+nS/SBNGkhUBFmEw==", + "version": "11.0.4", + "resolved": "https://registry.npmjs.org/focus-trap-react/-/focus-trap-react-11.0.4.tgz", + "integrity": "sha512-tC7jC/yqeAqhe4irNIzdyDf9XCtGSeECHiBSYJBO/vIN0asizbKZCt8TarB6/XqIceu42ajQ/U4lQJ9pZlWjrg==", + "license": "MIT", "dependencies": { - "focus-trap": "^7.5.4", + "focus-trap": "^7.6.5", "tabbable": "^6.2.0" }, "peerDependencies": { - "prop-types": "^15.8.1", - "react": ">=16.3.0", - "react-dom": ">=16.3.0" + "@types/react": "^18.0.0 || ^19.0.0", + "@types/react-dom": "^18.0.0 || ^19.0.0", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" } }, "node_modules/for-each": { "version": "0.3.5", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "license": "MIT", "dependencies": { "is-callable": "^1.2.7" }, @@ -3359,26 +3965,12 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/foreground-child": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz", - "integrity": "sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==", - "dependencies": { - "cross-spawn": "^7.0.0", - "signal-exit": "^4.0.1" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/fraction.js": { "version": "4.3.7", "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==", "dev": true, + "license": "MIT", "engines": { "node": "*" }, @@ -3388,12 +3980,13 @@ } }, "node_modules/framer-motion": { - "version": "11.15.0", - "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-11.15.0.tgz", - "integrity": "sha512-MLk8IvZntxOMg7lDBLw2qgTHHv664bYoYmnFTmE0Gm/FW67aOJk0WM3ctMcG+Xhcv+vh5uyyXwxvxhSeJzSe+w==", + "version": "12.23.12", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.23.12.tgz", + "integrity": "sha512-6e78rdVtnBvlEVgu6eFEAgG9v3wLnYEboM8I5O5EXvfKC8gxGQB8wXJdhkMy10iVcn05jl6CNw7/HTsTCfwcWg==", + "license": "MIT", "dependencies": { - "motion-dom": "^11.14.3", - "motion-utils": "^11.14.3", + "motion-dom": "^12.23.12", + "motion-utils": "^12.23.6", "tslib": "^2.4.0" }, "peerDependencies": { @@ -3413,16 +4006,12 @@ } } }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" - }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", "hasInstallScript": true, + "license": "MIT", "optional": true, "os": [ "darwin" @@ -3435,6 +4024,7 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -3443,6 +4033,7 @@ "version": "1.1.8", "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.8.tgz", "integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==", + "license": "MIT", "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", @@ -3462,6 +4053,7 @@ "version": "1.2.3", "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -3470,6 +4062,7 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", @@ -3493,6 +4086,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" @@ -3505,6 +4099,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", + "license": "MIT", "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", @@ -3517,29 +4112,11 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "license": "ISC", "dependencies": { "is-glob": "^4.0.3" }, @@ -3547,35 +4124,14 @@ "node": ">=10.13.0" } }, - "node_modules/glob/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/glob/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, "node_modules/globals": { - "version": "13.24.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", - "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", - "dependencies": { - "type-fest": "^0.20.2" - }, + "version": "16.3.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-16.3.0.tgz", + "integrity": "sha512-bqWEnJ1Nt3neqx2q5SFfGS8r/ahumIakg3HcwtNlrVlwXIeNumWn/c7Pn/wKzGhf6SaW6H6uWXLqC30STCMchQ==", + "dev": true, + "license": "MIT", "engines": { - "node": ">=8" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -3585,6 +4141,7 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "license": "MIT", "dependencies": { "define-properties": "^1.2.1", "gopd": "^1.0.1" @@ -3600,12 +4157,14 @@ "version": "0.1.2", "resolved": "https://registry.npmjs.org/globrex/-/globrex-0.1.2.tgz", "integrity": "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/goober": { "version": "2.1.16", "resolved": "https://registry.npmjs.org/goober/-/goober-2.1.16.tgz", "integrity": "sha512-erjk19y1U33+XAMe1VTvIONHYoSqE4iS7BYUZfHaqeohLmnC0FdxEh7rQU+6MZ4OajItzjZFSRtVANrQwNq6/g==", + "license": "MIT", "peerDependencies": { "csstype": "^3.0.10" } @@ -3614,6 +4173,7 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -3621,15 +4181,25 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, "node_modules/graphemer": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", - "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==" + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true, + "license": "MIT" }, "node_modules/has-bigints": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", + "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -3641,6 +4211,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "license": "MIT", "engines": { "node": ">=8" } @@ -3649,6 +4220,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "license": "MIT", "dependencies": { "es-define-property": "^1.0.0" }, @@ -3660,6 +4232,7 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", + "license": "MIT", "dependencies": { "dunder-proto": "^1.0.0" }, @@ -3674,6 +4247,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -3685,6 +4259,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", "dependencies": { "has-symbols": "^1.0.3" }, @@ -3699,6 +4274,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", "dependencies": { "function-bind": "^1.1.2" }, @@ -3707,17 +4283,19 @@ } }, "node_modules/ignore": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", - "integrity": "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==", + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "license": "MIT", "engines": { "node": ">= 4" } }, "node_modules/import-fresh": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", - "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "license": "MIT", "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" @@ -3733,28 +4311,16 @@ "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "license": "MIT", "engines": { "node": ">=0.8.19" } }, - "node_modules/inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" - }, "node_modules/internal-slot": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", + "license": "MIT", "dependencies": { "es-errors": "^1.3.0", "hasown": "^2.0.2", @@ -3768,6 +4334,7 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "license": "ISC", "engines": { "node": ">=12" } @@ -3776,6 +4343,7 @@ "version": "3.0.5", "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", + "license": "MIT", "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", @@ -3792,6 +4360,7 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", + "license": "MIT", "dependencies": { "async-function": "^1.0.0", "call-bound": "^1.0.3", @@ -3810,6 +4379,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", + "license": "MIT", "dependencies": { "has-bigints": "^1.0.2" }, @@ -3820,21 +4390,11 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "dependencies": { - "binary-extensions": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/is-boolean-object": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", + "license": "MIT", "dependencies": { "call-bound": "^1.0.3", "has-tostringtag": "^1.0.2" @@ -3850,6 +4410,7 @@ "version": "1.2.7", "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -3861,6 +4422,7 @@ "version": "2.16.1", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "license": "MIT", "dependencies": { "hasown": "^2.0.2" }, @@ -3875,6 +4437,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz", "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", + "license": "MIT", "dependencies": { "call-bound": "^1.0.2", "get-intrinsic": "^1.2.6", @@ -3891,6 +4454,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", + "license": "MIT", "dependencies": { "call-bound": "^1.0.2", "has-tostringtag": "^1.0.2" @@ -3906,6 +4470,7 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -3914,6 +4479,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", + "license": "MIT", "dependencies": { "call-bound": "^1.0.3" }, @@ -3924,18 +4490,11 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "engines": { - "node": ">=8" - } - }, "node_modules/is-generator-function": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.0.tgz", "integrity": "sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==", + "license": "MIT", "dependencies": { "call-bound": "^1.0.3", "get-proto": "^1.0.0", @@ -3953,6 +4512,7 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "license": "MIT", "dependencies": { "is-extglob": "^2.1.1" }, @@ -3964,6 +4524,19 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-negative-zero": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", + "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", + "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -3975,6 +4548,8 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", "engines": { "node": ">=0.12.0" } @@ -3983,6 +4558,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", + "license": "MIT", "dependencies": { "call-bound": "^1.0.3", "has-tostringtag": "^1.0.2" @@ -3994,18 +4570,11 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-path-inside": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", - "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", - "engines": { - "node": ">=8" - } - }, "node_modules/is-regex": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "license": "MIT", "dependencies": { "call-bound": "^1.0.2", "gopd": "^1.2.0", @@ -4023,6 +4592,7 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", + "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -4034,6 +4604,7 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", + "license": "MIT", "dependencies": { "call-bound": "^1.0.3" }, @@ -4048,6 +4619,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", + "license": "MIT", "dependencies": { "call-bound": "^1.0.3", "has-tostringtag": "^1.0.2" @@ -4063,6 +4635,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", + "license": "MIT", "dependencies": { "call-bound": "^1.0.2", "has-symbols": "^1.1.0", @@ -4079,6 +4652,7 @@ "version": "1.1.15", "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "license": "MIT", "dependencies": { "which-typed-array": "^1.1.16" }, @@ -4093,6 +4667,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", + "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -4104,6 +4679,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz", "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", + "license": "MIT", "dependencies": { "call-bound": "^1.0.3" }, @@ -4118,6 +4694,7 @@ "version": "2.0.4", "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", + "license": "MIT", "dependencies": { "call-bound": "^1.0.3", "get-intrinsic": "^1.2.6" @@ -4132,18 +4709,21 @@ "node_modules/isarray": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", - "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==" + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "license": "MIT" }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" }, "node_modules/iterator.prototype": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.5.tgz", "integrity": "sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==", "dev": true, + "license": "MIT", "dependencies": { "define-data-property": "^1.1.4", "es-object-atoms": "^1.0.0", @@ -4156,45 +4736,33 @@ "node": ">= 0.4" } }, - "node_modules/jackspeak": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.6.tgz", - "integrity": "sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==", - "dependencies": { - "@isaacs/cliui": "^8.0.2" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - }, - "optionalDependencies": { - "@pkgjs/parseargs": "^0.11.0" - } - }, "node_modules/jiti": { - "version": "1.21.7", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", - "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.5.1.tgz", + "integrity": "sha512-twQoecYPiVA5K/h6SxtORw/Bs3ar+mLUtoPSc7iMXzQzK8d7eJ/R09wmTwAjiamETn1cXYPGfNnu7DMoHgu12w==", + "devOptional": true, + "license": "MIT", "bin": { - "jiti": "bin/jiti.js" + "jiti": "lib/jiti-cli.mjs" } }, "node_modules/js-base64": { "version": "3.7.7", "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-3.7.7.tgz", - "integrity": "sha512-7rCnleh0z2CkXhH67J8K1Ytz0b2Y+yxTPL+/KOJoa20hfnVQ/3/T6W/KflYI4bRHRagNeXeU2bkNGI3v1oS/lw==" + "integrity": "sha512-7rCnleh0z2CkXhH67J8K1Ytz0b2Y+yxTPL+/KOJoa20hfnVQ/3/T6W/KflYI4bRHRagNeXeU2bkNGI3v1oS/lw==", + "license": "BSD-3-Clause" }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" }, "node_modules/js-yaml": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "license": "MIT", "dependencies": { "argparse": "^2.0.1" }, @@ -4205,22 +4773,26 @@ "node_modules/json-buffer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==" + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "license": "MIT" }, "node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "license": "MIT" }, "node_modules/json-stable-stringify-without-jsonify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==" + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "license": "MIT" }, "node_modules/json5": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "license": "MIT", "dependencies": { "minimist": "^1.2.0" }, @@ -4233,6 +4805,7 @@ "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", "integrity": "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==", "dev": true, + "license": "MIT", "dependencies": { "array-includes": "^3.1.6", "array.prototype.flat": "^1.3.1", @@ -4247,6 +4820,7 @@ "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "license": "MIT", "dependencies": { "json-buffer": "3.0.1" } @@ -4255,6 +4829,7 @@ "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "license": "MIT", "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" @@ -4263,26 +4838,240 @@ "node": ">= 0.8.0" } }, - "node_modules/lilconfig": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", - "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "node_modules/lightningcss": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.1.tgz", + "integrity": "sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg==", + "devOptional": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, "engines": { - "node": ">=14" + "node": ">= 12.0.0" }, "funding": { - "url": "https://github.com/sponsors/antonk52" + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-darwin-arm64": "1.30.1", + "lightningcss-darwin-x64": "1.30.1", + "lightningcss-freebsd-x64": "1.30.1", + "lightningcss-linux-arm-gnueabihf": "1.30.1", + "lightningcss-linux-arm64-gnu": "1.30.1", + "lightningcss-linux-arm64-musl": "1.30.1", + "lightningcss-linux-x64-gnu": "1.30.1", + "lightningcss-linux-x64-musl": "1.30.1", + "lightningcss-win32-arm64-msvc": "1.30.1", + "lightningcss-win32-x64-msvc": "1.30.1" } }, - "node_modules/lines-and-columns": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", - "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==" + "node_modules/lightningcss-darwin-arm64": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.1.tgz", + "integrity": "sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.1.tgz", + "integrity": "sha512-k1EvjakfumAQoTfcXUcHQZhSpLlkAuEkdMBsI/ivWw9hL+7FtilQc0Cy3hrx0AAQrVtQAbMI7YjCgYgvn37PzA==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.1.tgz", + "integrity": "sha512-kmW6UGCGg2PcyUE59K5r0kWfKPAVy4SltVeut+umLCFoJ53RdCUWxcRDzO1eTaxf/7Q2H7LTquFHPL5R+Gjyig==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.1.tgz", + "integrity": "sha512-MjxUShl1v8pit+6D/zSPq9S9dQ2NPFSQwGvxBCYaBYLPlCWuPh9/t1MRS8iUaR8i+a6w7aps+B4N0S1TYP/R+Q==", + "cpu": [ + "arm" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.1.tgz", + "integrity": "sha512-gB72maP8rmrKsnKYy8XUuXi/4OctJiuQjcuqWNlJQ6jZiWqtPvqFziskH3hnajfvKB27ynbVCucKSm2rkQp4Bw==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.1.tgz", + "integrity": "sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.1.tgz", + "integrity": "sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.1.tgz", + "integrity": "sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.1.tgz", + "integrity": "sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.1.tgz", + "integrity": "sha512-PVqXh48wh4T53F/1CCu8PIPCxLzWyCnn/9T5W1Jpmdy5h9Cwd+0YQS6/LwhHXSafuc61/xg9Lv5OrCby6a++jg==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "license": "MIT", "dependencies": { "p-locate": "^5.0.0" }, @@ -4296,39 +5085,46 @@ "node_modules/lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "license": "MIT" }, "node_modules/lodash.castarray": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/lodash.castarray/-/lodash.castarray-4.4.0.tgz", "integrity": "sha512-aVx8ztPv7/2ULbArGJ2Y42bG1mEQ5mGjpdvrbJcJFU3TbYybe+QlLS4pst9zV52ymy2in1KpFPiZnAOATxD4+Q==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/lodash.debounce": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", - "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==" + "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", + "license": "MIT" }, "node_modules/lodash.isplainobject": { "version": "4.0.6", "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==" + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "license": "MIT" }, "node_modules/lodash.throttle": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/lodash.throttle/-/lodash.throttle-4.1.1.tgz", - "integrity": "sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ==" + "integrity": "sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ==", + "license": "MIT" }, "node_modules/loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, @@ -4336,10 +5132,21 @@ "loose-envify": "cli.js" } }, + "node_modules/magic-string": { + "version": "0.30.17", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", + "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", "engines": { "node": ">= 0.4" } @@ -4348,6 +5155,8 @@ "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", "engines": { "node": ">= 8" } @@ -4356,6 +5165,8 @@ "version": "4.0.8", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" @@ -4368,75 +5179,103 @@ "version": "1.4.4", "resolved": "https://registry.npmjs.org/mini-svg-data-uri/-/mini-svg-data-uri-1.4.4.tgz", "integrity": "sha512-r9deDe9p5FJUPZAk3A59wGH7Ii9YrjjWw0jmw/liSbHl2CHiyXj6FcDXDu2K3TjVAXqiJdaw3xxwlZZr9E6nHg==", + "license": "MIT", "bin": { "mini-svg-data-uri": "cli.js" } }, "node_modules/minimatch": { - "version": "9.0.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", - "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "license": "ISC", "dependencies": { - "brace-expansion": "^2.0.1" + "brace-expansion": "^1.1.7" }, "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "node": "*" } }, "node_modules/minimist": { "version": "1.2.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/minipass": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.4.tgz", - "integrity": "sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==", + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "license": "ISC", "engines": { "node": ">=16 || 14 >=14.17" } }, - "node_modules/motion-dom": { - "version": "11.14.3", - "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-11.14.3.tgz", - "integrity": "sha512-lW+D2wBy5vxLJi6aCP0xyxTxlTfiu+b+zcpVbGVFUxotwThqhdpPRSmX8xztAgtZMPMeU0WGVn/k1w4I+TbPqA==" - }, - "node_modules/motion-utils": { - "version": "11.14.3", - "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-11.14.3.tgz", - "integrity": "sha512-Xg+8xnqIJTpr0L/cidfTTBFkvRw26ZtGGuIhA94J9PQ2p4mEa06Xx7QVYZH0BP+EpMSaDlu+q0I0mmvwADPsaQ==" - }, - "node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - }, - "node_modules/mz": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", - "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "node_modules/minizlib": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.0.2.tgz", + "integrity": "sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA==", + "dev": true, + "license": "MIT", "dependencies": { - "any-promise": "^1.0.0", - "object-assign": "^4.0.1", - "thenify-all": "^1.0.0" + "minipass": "^7.1.2" + }, + "engines": { + "node": ">= 18" } }, + "node_modules/mkdirp": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz", + "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==", + "dev": true, + "license": "MIT", + "bin": { + "mkdirp": "dist/cjs/src/bin.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/motion-dom": { + "version": "12.23.12", + "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.23.12.tgz", + "integrity": "sha512-RcR4fvMCTESQBD/uKQe49D5RUeDOokkGRmz4ceaJKDBgHYtZtntC/s2vLvY38gqGaytinij/yi3hMcWVcEF5Kw==", + "license": "MIT", + "dependencies": { + "motion-utils": "^12.23.6" + } + }, + "node_modules/motion-utils": { + "version": "12.23.6", + "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.23.6.tgz", + "integrity": "sha512-eAWoPgr4eFEOFfg2WjIsMoqJTW6Z8MTUCgn/GZ3VRpClWBdnbjryiA3ZSNLyxCTmCQx4RmYX6jX1iWHbenUPNQ==", + "license": "MIT" + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, "node_modules/nanoid": { - "version": "3.3.8", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz", - "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==", + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", "funding": [ { "type": "github", "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "bin": { "nanoid": "bin/nanoid.cjs" }, @@ -4447,27 +5286,22 @@ "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==" + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "license": "MIT" }, "node_modules/node-releases": { "version": "2.0.19", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", - "dev": true - }, - "node_modules/normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "engines": { - "node": ">=0.10.0" - } + "dev": true, + "license": "MIT" }, "node_modules/normalize-range": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -4476,22 +5310,16 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", "engines": { "node": ">=0.10.0" } }, - "node_modules/object-hash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", - "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", - "engines": { - "node": ">= 6" - } - }, "node_modules/object-inspect": { "version": "1.13.4", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -4503,6 +5331,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "license": "MIT", "engines": { "node": ">= 0.4" } @@ -4511,6 +5340,7 @@ "version": "4.1.7", "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", + "license": "MIT", "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", @@ -4527,14 +5357,16 @@ } }, "node_modules/object.entries": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.8.tgz", - "integrity": "sha512-cmopxi8VwRIAw/fkijJohSfpef5PdN0pMQJN6VC/ZKvn0LIknWD8KtgY6KlQdEc4tIjcQ3HxSMmnvtzIscdaYQ==", + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.9.tgz", + "integrity": "sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", "define-properties": "^1.2.1", - "es-object-atoms": "^1.0.0" + "es-object-atoms": "^1.1.1" }, "engines": { "node": ">= 0.4" @@ -4544,6 +5376,7 @@ "version": "2.0.8", "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", + "license": "MIT", "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", @@ -4561,6 +5394,7 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.3.tgz", "integrity": "sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==", + "license": "MIT", "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", @@ -4574,6 +5408,7 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.1.tgz", "integrity": "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==", + "license": "MIT", "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", @@ -4587,25 +5422,18 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dependencies": { - "wrappy": "1" - } - }, "node_modules/optionator": { - "version": "0.9.3", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", - "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==", + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "license": "MIT", "dependencies": { - "@aashutoshrathi/word-wrap": "^1.2.3", "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", - "type-check": "^0.4.0" + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" }, "engines": { "node": ">= 0.8.0" @@ -4615,6 +5443,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==", + "license": "MIT", "dependencies": { "get-intrinsic": "^1.2.6", "object-keys": "^1.1.1", @@ -4631,6 +5460,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "license": "MIT", "dependencies": { "yocto-queue": "^0.1.0" }, @@ -4645,6 +5475,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "license": "MIT", "dependencies": { "p-limit": "^3.0.2" }, @@ -4659,6 +5490,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "license": "MIT", "dependencies": { "callsites": "^3.0.0" }, @@ -4670,22 +5502,16 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "license": "MIT", "engines": { "node": ">=8" } }, - "node_modules/path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", "engines": { "node": ">=8" } @@ -4693,40 +5519,21 @@ "node_modules/path-parse": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" - }, - "node_modules/path-scurry": { - "version": "1.10.2", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.10.2.tgz", - "integrity": "sha512-7xTavNy5RQXnsjANvVvMkEjvloOinkAjv/Z6Ildz9v2RinZ4SBKTWFOVRbaF8p0vpHnyjV/UwNDdKuUv6M5qcA==", - "dependencies": { - "lru-cache": "^10.2.0", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/path-scurry/node_modules/lru-cache": { - "version": "10.2.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.0.tgz", - "integrity": "sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q==", - "engines": { - "node": "14 || >=16.14" - } + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "license": "MIT" }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==" + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" }, "node_modules/picomatch": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", "engines": { "node": ">=8.6" }, @@ -4734,34 +5541,19 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/pify": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/pirates": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", - "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==", - "engines": { - "node": ">= 6" - } - }, "node_modules/possible-typed-array-names": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", + "license": "MIT", "engines": { "node": ">= 0.4" } }, "node_modules/postcss": { - "version": "8.5.3", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz", - "integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==", + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", "funding": [ { "type": "opencollective", @@ -4776,8 +5568,9 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "dependencies": { - "nanoid": "^3.3.8", + "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -4785,118 +5578,12 @@ "node": "^10 || ^12 || >=14" } }, - "node_modules/postcss-import": { - "version": "15.1.0", - "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", - "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", - "dependencies": { - "postcss-value-parser": "^4.0.0", - "read-cache": "^1.0.0", - "resolve": "^1.1.7" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "postcss": "^8.0.0" - } - }, - "node_modules/postcss-import/node_modules/resolve": { - "version": "1.22.8", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", - "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", - "dependencies": { - "is-core-module": "^2.13.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/postcss-js": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz", - "integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==", - "dependencies": { - "camelcase-css": "^2.0.1" - }, - "engines": { - "node": "^12 || ^14 || >= 16" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - "peerDependencies": { - "postcss": "^8.4.21" - } - }, - "node_modules/postcss-load-config": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz", - "integrity": "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "dependencies": { - "lilconfig": "^3.0.0", - "yaml": "^2.3.4" - }, - "engines": { - "node": ">= 14" - }, - "peerDependencies": { - "postcss": ">=8.0.9", - "ts-node": ">=9.0.0" - }, - "peerDependenciesMeta": { - "postcss": { - "optional": true - }, - "ts-node": { - "optional": true - } - } - }, - "node_modules/postcss-nested": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", - "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "dependencies": { - "postcss-selector-parser": "^6.1.1" - }, - "engines": { - "node": ">=12.0" - }, - "peerDependencies": { - "postcss": "^8.2.14" - } - }, "node_modules/postcss-selector-parser": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", - "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "version": "6.0.10", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz", + "integrity": "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==", + "dev": true, + "license": "MIT", "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" @@ -4908,21 +5595,25 @@ "node_modules/postcss-value-parser": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true, + "license": "MIT" }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "license": "MIT", "engines": { "node": ">= 0.8.0" } }, "node_modules/prettier": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.2.tgz", - "integrity": "sha512-lc6npv5PH7hVqozBR7lkBNOGXV9vMwROAPlumdBkX0wTbbzPu/U1hk5yL8p2pt4Xoc+2mkT8t/sow2YrV/M5qg==", + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz", + "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", "dev": true, + "license": "MIT", "bin": { "prettier": "bin/prettier.cjs" }, @@ -4934,15 +5625,18 @@ } }, "node_modules/prettier-plugin-tailwindcss": { - "version": "0.6.11", - "resolved": "https://registry.npmjs.org/prettier-plugin-tailwindcss/-/prettier-plugin-tailwindcss-0.6.11.tgz", - "integrity": "sha512-YxaYSIvZPAqhrrEpRtonnrXdghZg1irNg4qrjboCXrpybLWVs55cW2N3juhspVJiO0JBvYJT8SYsJpc8OQSnsA==", + "version": "0.6.14", + "resolved": "https://registry.npmjs.org/prettier-plugin-tailwindcss/-/prettier-plugin-tailwindcss-0.6.14.tgz", + "integrity": "sha512-pi2e/+ZygeIqntN+vC573BcW5Cve8zUB0SSAGxqpB4f96boZF4M3phPVoOFCeypwkpRYdi7+jQ5YJJUwrkGUAg==", "dev": true, + "license": "MIT", "engines": { "node": ">=14.21.3" }, "peerDependencies": { "@ianvs/prettier-plugin-sort-imports": "*", + "@prettier/plugin-hermes": "*", + "@prettier/plugin-oxc": "*", "@prettier/plugin-pug": "*", "@shopify/prettier-plugin-liquid": "*", "@trivago/prettier-plugin-sort-imports": "*", @@ -4964,6 +5658,12 @@ "@ianvs/prettier-plugin-sort-imports": { "optional": true }, + "@prettier/plugin-hermes": { + "optional": true + }, + "@prettier/plugin-oxc": { + "optional": true + }, "@prettier/plugin-pug": { "optional": true }, @@ -5015,6 +5715,7 @@ "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "license": "MIT", "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", @@ -5025,6 +5726,7 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "license": "MIT", "engines": { "node": ">=6" } @@ -5033,6 +5735,7 @@ "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, "funding": [ { "type": "github", @@ -5046,15 +5749,14 @@ "type": "consulting", "url": "https://feross.org/support" } - ] + ], + "license": "MIT" }, "node_modules/react": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", - "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==", - "dependencies": { - "loose-envify": "^1.1.0" - }, + "version": "19.1.1", + "resolved": "https://registry.npmjs.org/react/-/react-19.1.1.tgz", + "integrity": "sha512-w8nqGImo45dmMIfljjMwOGtbmC/mk4CMYhWIicdSflH91J9TyCyczcPFXJzrZ/ZXcgGRFeP6BU0BEJTw6tZdfQ==", + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -5063,6 +5765,7 @@ "version": "3.2.3", "resolved": "https://registry.npmjs.org/react-animate-height/-/react-animate-height-3.2.3.tgz", "integrity": "sha512-R6DSvr7ud07oeCixScyvXWEMJY/Mt2+GyOWC1KMaRc69gOBw+SsCg4TJmrp4rKUM1hyd6p+YKw90brjPH93Y2A==", + "license": "MIT", "engines": { "node": ">= 12.0.0" }, @@ -5072,21 +5775,22 @@ } }, "node_modules/react-dom": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", - "integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==", + "version": "19.1.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.1.tgz", + "integrity": "sha512-Dlq/5LAZgF0Gaz6yiqZCf6VCcZs1ghAJyrsu84Q/GT0gV+mCxbfmKNoGRKBYMJ8IEdGPqu49YWXD02GCknEDkw==", + "license": "MIT", "dependencies": { - "loose-envify": "^1.1.0", - "scheduler": "^0.23.0" + "scheduler": "^0.26.0" }, "peerDependencies": { - "react": "^18.2.0" + "react": "^19.1.1" } }, "node_modules/react-hot-toast": { "version": "2.5.2", "resolved": "https://registry.npmjs.org/react-hot-toast/-/react-hot-toast-2.5.2.tgz", "integrity": "sha512-Tun3BbCxzmXXM7C+NI4qiv6lT0uwGh4oAfeJyNOjYUejTsm35mK9iCaYLGv8cBz9L5YxZLx/2ii7zsIwPtPUdw==", + "license": "MIT", "dependencies": { "csstype": "^3.1.3", "goober": "^2.1.16" @@ -5103,6 +5807,7 @@ "version": "5.5.0", "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.5.0.tgz", "integrity": "sha512-MEFcXdkP3dLo8uumGI5xN3lDFNsRtrjbOEKDLD7yv76v4wpnEq2Lt2qeHaQOr34I/wPN3s3+N08WkQ+CW37Xiw==", + "license": "MIT", "peerDependencies": { "react": "*" } @@ -5110,14 +5815,16 @@ "node_modules/react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" }, "node_modules/react-router": { - "version": "6.22.3", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.22.3.tgz", - "integrity": "sha512-dr2eb3Mj5zK2YISHK++foM9w4eBnO23eKnZEDs7c880P6oKbrjz/Svg9+nxqtHQK+oMW4OtjZca0RqPglXxguQ==", + "version": "6.30.1", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.1.tgz", + "integrity": "sha512-X1m21aEmxGXqENEPG3T6u0Th7g0aS4ZmoNynhbs+Cn+q+QGTLt+d5IQ2bHAXKzKcxGJjxACpVbnYQSCRcfxHlQ==", + "license": "MIT", "dependencies": { - "@remix-run/router": "1.15.3" + "@remix-run/router": "1.23.0" }, "engines": { "node": ">=14.0.0" @@ -5127,12 +5834,13 @@ } }, "node_modules/react-router-dom": { - "version": "6.22.3", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.22.3.tgz", - "integrity": "sha512-7ZILI7HjcE+p31oQvwbokjk6OA/bnFxrhJ19n82Ex9Ph8fNAq+Hm/7KchpMGlTgWhUxRHMMCut+vEtNpWpowKw==", + "version": "6.30.1", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.1.tgz", + "integrity": "sha512-llKsgOkZdbPU1Eg3zK8lCn+sjD9wMRZZPuzmdWWX5SUs8OFkN5HnFVC0u5KMeMaC9aoancFI/KoLuKPqN+hxHw==", + "license": "MIT", "dependencies": { - "@remix-run/router": "1.15.3", - "react-router": "6.22.3" + "@remix-run/router": "1.23.0", + "react-router": "6.30.1" }, "engines": { "node": ">=14.0.0" @@ -5143,18 +5851,20 @@ } }, "node_modules/react-simple-keyboard": { - "version": "3.7.112", - "resolved": "https://registry.npmjs.org/react-simple-keyboard/-/react-simple-keyboard-3.7.112.tgz", - "integrity": "sha512-wpSgxQ6UHQcHqcC0eQgD7mZ91kpjGYj2mT+Ir1Be5jxIMxz1rQOtXLYpsQcNvYXCJyrVqntYPOPCPnpVxsxPvA==", + "version": "3.8.106", + "resolved": "https://registry.npmjs.org/react-simple-keyboard/-/react-simple-keyboard-3.8.106.tgz", + "integrity": "sha512-ItCHCdhVCzn9huhenuyuHQMOGsl3UMLu5xAO1bkjj4AAgVoktFC1DQ4HWkOS6BGPvUJejFM3Q5hVM8Bl2oX9pA==", + "license": "MIT", "peerDependencies": { - "react": "^16.0.0 || ^17.0.0 || ^18.0.0", - "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0" + "react": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "node_modules/react-smooth": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/react-smooth/-/react-smooth-4.0.4.tgz", "integrity": "sha512-gnGKTpYwqL0Iii09gHobNolvX4Kiq4PKx6eWBCYYix+8cdw+cGo3do906l1NBPKkSWx1DghC1dlWG9L2uGd61Q==", + "license": "MIT", "dependencies": { "fast-equals": "^5.0.1", "prop-types": "^15.8.1", @@ -5169,6 +5879,7 @@ "version": "4.4.5", "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", + "license": "BSD-3-Clause", "dependencies": { "@babel/runtime": "^7.5.5", "dom-helpers": "^5.0.1", @@ -5180,37 +5891,26 @@ "react-dom": ">=16.6.0" } }, + "node_modules/react-use-websocket": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/react-use-websocket/-/react-use-websocket-4.13.0.tgz", + "integrity": "sha512-anMuVoV//g2N76Wxqvqjjo1X48r9Np3y1/gMl7arX84tAPXdy5R7sB5lO5hvCzQRYjqXwV8XMAiEBOUbyrZFrw==", + "license": "MIT" + }, "node_modules/react-xtermjs": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/react-xtermjs/-/react-xtermjs-1.0.9.tgz", - "integrity": "sha512-lrK1xiWfgxAC+4shtMHh0Irxg2t5t7JbTtpP0W7GIf1gQ9SHW/djmyiLpQSA75mN1DpT0bKeqj1fOKd0XX8RBA==", + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/react-xtermjs/-/react-xtermjs-1.0.10.tgz", + "integrity": "sha512-+xpKEKbmsypWzRKE0FR1LNIGcI8gx+R6VMHe8IQW7iTbgeqp3Qg7SbiVNOzR+Ovb1QK4DPA3KqsIQV+XP0iRUA==", + "license": "ISC", "peerDependencies": { "@xterm/xterm": "^5.5.0" } }, - "node_modules/read-cache": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", - "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", - "dependencies": { - "pify": "^2.3.0" - } - }, - "node_modules/readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "dependencies": { - "picomatch": "^2.2.1" - }, - "engines": { - "node": ">=8.10.0" - } - }, "node_modules/recharts": { - "version": "2.15.1", - "resolved": "https://registry.npmjs.org/recharts/-/recharts-2.15.1.tgz", - "integrity": "sha512-v8PUTUlyiDe56qUj82w/EDVuzEFXwEHp9/xOowGAZwfLjB9uAy3GllQVIYMWF6nU+qibx85WF75zD7AjqoT54Q==", + "version": "2.15.4", + "resolved": "https://registry.npmjs.org/recharts/-/recharts-2.15.4.tgz", + "integrity": "sha512-UT/q6fwS3c1dHbXv2uFgYJ9BMFHu3fwnd7AYZaEQhXuYQ4hgsxLvsUXzGdKeZrW5xopzDCvuA2N41WJ88I7zIw==", + "license": "MIT", "dependencies": { "clsx": "^2.0.0", "eventemitter3": "^4.0.1", @@ -5233,6 +5933,7 @@ "version": "0.4.5", "resolved": "https://registry.npmjs.org/recharts-scale/-/recharts-scale-0.4.5.tgz", "integrity": "sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w==", + "license": "MIT", "dependencies": { "decimal.js-light": "^2.4.1" } @@ -5240,12 +5941,14 @@ "node_modules/recharts/node_modules/react-is": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", - "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==" + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "license": "MIT" }, "node_modules/reflect.getprototypeof": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", + "license": "MIT", "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", @@ -5263,15 +5966,11 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/regenerator-runtime": { - "version": "0.14.1", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", - "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==" - }, "node_modules/regexp.prototype.flags": { "version": "1.5.4", "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", + "license": "MIT", "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", @@ -5288,18 +5987,21 @@ } }, "node_modules/resolve": { - "version": "2.0.0-next.5", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.5.tgz", - "integrity": "sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==", - "dev": true, + "version": "1.22.10", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", + "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", + "license": "MIT", "dependencies": { - "is-core-module": "^2.13.0", + "is-core-module": "^2.16.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" }, + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -5308,40 +6010,29 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "license": "MIT", "engines": { "node": ">=4" } }, "node_modules/reusify": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", - "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", "engines": { "iojs": ">=1.0.0", "node": ">=0.10.0" } }, - "node_modules/rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/rollup": { - "version": "4.14.1", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.14.1.tgz", - "integrity": "sha512-4LnHSdd3QK2pa1J6dFbfm1HN0D7vSK/ZuZTsdyUAlA6Rr1yTouUTL13HaDOGJVgby461AhrNGBS7sCGXXtT+SA==", - "dev": true, + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.46.2.tgz", + "integrity": "sha512-WMmLFI+Boh6xbop+OAGo9cQ3OgX9MIg7xOQjn+pTCwOkk+FNDAeAemXkJ3HzDJrVXleLOFVa1ipuc1AmEx1Dwg==", + "license": "MIT", "dependencies": { - "@types/estree": "1.0.5" + "@types/estree": "1.0.8" }, "bin": { "rollup": "dist/bin/rollup" @@ -5351,21 +6042,26 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.14.1", - "@rollup/rollup-android-arm64": "4.14.1", - "@rollup/rollup-darwin-arm64": "4.14.1", - "@rollup/rollup-darwin-x64": "4.14.1", - "@rollup/rollup-linux-arm-gnueabihf": "4.14.1", - "@rollup/rollup-linux-arm64-gnu": "4.14.1", - "@rollup/rollup-linux-arm64-musl": "4.14.1", - "@rollup/rollup-linux-powerpc64le-gnu": "4.14.1", - "@rollup/rollup-linux-riscv64-gnu": "4.14.1", - "@rollup/rollup-linux-s390x-gnu": "4.14.1", - "@rollup/rollup-linux-x64-gnu": "4.14.1", - "@rollup/rollup-linux-x64-musl": "4.14.1", - "@rollup/rollup-win32-arm64-msvc": "4.14.1", - "@rollup/rollup-win32-ia32-msvc": "4.14.1", - "@rollup/rollup-win32-x64-msvc": "4.14.1", + "@rollup/rollup-android-arm-eabi": "4.46.2", + "@rollup/rollup-android-arm64": "4.46.2", + "@rollup/rollup-darwin-arm64": "4.46.2", + "@rollup/rollup-darwin-x64": "4.46.2", + "@rollup/rollup-freebsd-arm64": "4.46.2", + "@rollup/rollup-freebsd-x64": "4.46.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.46.2", + "@rollup/rollup-linux-arm-musleabihf": "4.46.2", + "@rollup/rollup-linux-arm64-gnu": "4.46.2", + "@rollup/rollup-linux-arm64-musl": "4.46.2", + "@rollup/rollup-linux-loongarch64-gnu": "4.46.2", + "@rollup/rollup-linux-ppc64-gnu": "4.46.2", + "@rollup/rollup-linux-riscv64-gnu": "4.46.2", + "@rollup/rollup-linux-riscv64-musl": "4.46.2", + "@rollup/rollup-linux-s390x-gnu": "4.46.2", + "@rollup/rollup-linux-x64-gnu": "4.46.2", + "@rollup/rollup-linux-x64-musl": "4.46.2", + "@rollup/rollup-win32-arm64-msvc": "4.46.2", + "@rollup/rollup-win32-ia32-msvc": "4.46.2", + "@rollup/rollup-win32-x64-msvc": "4.46.2", "fsevents": "~2.3.2" } }, @@ -5373,6 +6069,7 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, "funding": [ { "type": "github", @@ -5387,6 +6084,7 @@ "url": "https://feross.org/support" } ], + "license": "MIT", "dependencies": { "queue-microtask": "^1.2.2" } @@ -5395,6 +6093,7 @@ "version": "1.1.3", "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz", "integrity": "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==", + "license": "MIT", "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.2", @@ -5413,6 +6112,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", + "license": "MIT", "dependencies": { "es-errors": "^1.3.0", "isarray": "^2.0.5" @@ -5428,6 +6128,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "license": "MIT", "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", @@ -5441,18 +6142,17 @@ } }, "node_modules/scheduler": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz", - "integrity": "sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==", - "dependencies": { - "loose-envify": "^1.1.0" - } + "version": "0.26.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz", + "integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==", + "license": "MIT" }, "node_modules/semver": { - "version": "7.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", - "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", "dev": true, + "license": "ISC", "bin": { "semver": "bin/semver.js" }, @@ -5464,6 +6164,7 @@ "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "license": "MIT", "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", @@ -5480,6 +6181,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", + "license": "MIT", "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", @@ -5494,6 +6196,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz", "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", + "license": "MIT", "dependencies": { "dunder-proto": "^1.0.1", "es-errors": "^1.3.0", @@ -5507,6 +6210,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", "dependencies": { "shebang-regex": "^3.0.0" }, @@ -5518,6 +6222,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", "engines": { "node": ">=8" } @@ -5526,6 +6231,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", @@ -5544,6 +6250,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3" @@ -5559,6 +6266,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", @@ -5576,6 +6284,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", @@ -5590,83 +6299,26 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" } }, - "node_modules/string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "node_modules/stop-iteration-iterator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", + "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", + "license": "MIT", "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" + "es-errors": "^1.3.0", + "internal-slot": "^1.1.0" }, "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/string-width-cjs": { - "name": "string-width", - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/string-width-cjs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" - }, - "node_modules/string-width/node_modules/ansi-regex": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", - "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/string-width/node_modules/strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" + "node": ">= 0.4" } }, "node_modules/string.prototype.matchall": { @@ -5674,6 +6326,7 @@ "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz", "integrity": "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==", "dev": true, + "license": "MIT", "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", @@ -5701,6 +6354,7 @@ "resolved": "https://registry.npmjs.org/string.prototype.repeat/-/string.prototype.repeat-1.0.0.tgz", "integrity": "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==", "dev": true, + "license": "MIT", "dependencies": { "define-properties": "^1.1.3", "es-abstract": "^1.17.5" @@ -5710,6 +6364,7 @@ "version": "1.2.10", "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz", "integrity": "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==", + "license": "MIT", "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.2", @@ -5730,6 +6385,7 @@ "version": "1.0.9", "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz", "integrity": "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==", + "license": "MIT", "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.2", @@ -5747,6 +6403,7 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", + "license": "MIT", "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", @@ -5759,33 +6416,11 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-ansi-cjs": { - "name": "strip-ansi", - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/strip-bom": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "license": "MIT", "engines": { "node": ">=4" } @@ -5794,6 +6429,7 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "license": "MIT", "engines": { "node": ">=8" }, @@ -5801,52 +6437,11 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/sucrase": { - "version": "3.35.0", - "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz", - "integrity": "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.2", - "commander": "^4.0.0", - "glob": "^10.3.10", - "lines-and-columns": "^1.1.6", - "mz": "^2.7.0", - "pirates": "^4.0.1", - "ts-interface-checker": "^0.1.9" - }, - "bin": { - "sucrase": "bin/sucrase", - "sucrase-node": "bin/sucrase-node" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, - "node_modules/sucrase/node_modules/glob": { - "version": "10.3.12", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.12.tgz", - "integrity": "sha512-TCNv8vJ+xz4QiqTpfOJA7HvYv+tNIRHKfUWw/q+v2jdgN4ebz+KY9tGx5J4rHP0o84mNP+ApH66HRX8us3Khqg==", - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^2.3.6", - "minimatch": "^9.0.1", - "minipass": "^7.0.4", - "path-scurry": "^1.10.2" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "license": "MIT", "dependencies": { "has-flag": "^4.0.0" }, @@ -5858,6 +6453,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -5868,102 +6464,107 @@ "node_modules/tabbable": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.2.0.tgz", - "integrity": "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==" + "integrity": "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==", + "license": "MIT" }, "node_modules/tailwind-merge": { - "version": "2.5.5", - "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.5.5.tgz", - "integrity": "sha512-0LXunzzAZzo0tEPxV3I297ffKZPlKDrjj7NXphC8V5ak9yHC5zRmxnOe2m/Rd/7ivsOMJe3JZ2JVocoDdQTRBA==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.3.1.tgz", + "integrity": "sha512-gBXpgUm/3rp1lMZZrM/w7D8GKqshif0zAymAhbCyIt8KMe+0v9DQ7cdYLR4FHH/cKpdTXb+A/tKKU3eolfsI+g==", + "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/dcastil" } }, "node_modules/tailwindcss": { - "version": "3.4.17", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.17.tgz", - "integrity": "sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.11.tgz", + "integrity": "sha512-2E9TBm6MDD/xKYe+dvJZAmg3yxIEDNRc0jwlNyDg/4Fil2QcSLjFKGVff0lAf1jjeaArlG/M75Ey/EYr/OJtBA==", + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.2.tgz", + "integrity": "sha512-Re10+NauLTMCudc7T5WLFLAwDhQ0JWdrMK+9B2M8zR5hRExKmsRDCBA7/aV/pNJFltmBFO5BAMlQFi/vq3nKOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/tar": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.4.3.tgz", + "integrity": "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==", + "dev": true, + "license": "ISC", "dependencies": { - "@alloc/quick-lru": "^5.2.0", - "arg": "^5.0.2", - "chokidar": "^3.6.0", - "didyoumean": "^1.2.2", - "dlv": "^1.1.3", - "fast-glob": "^3.3.2", - "glob-parent": "^6.0.2", - "is-glob": "^4.0.3", - "jiti": "^1.21.6", - "lilconfig": "^3.1.3", - "micromatch": "^4.0.8", - "normalize-path": "^3.0.0", - "object-hash": "^3.0.0", - "picocolors": "^1.1.1", - "postcss": "^8.4.47", - "postcss-import": "^15.1.0", - "postcss-js": "^4.0.1", - "postcss-load-config": "^4.0.2", - "postcss-nested": "^6.2.0", - "postcss-selector-parser": "^6.1.2", - "resolve": "^1.22.8", - "sucrase": "^3.35.0" - }, - "bin": { - "tailwind": "lib/cli.js", - "tailwindcss": "lib/cli.js" + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.0.1", + "mkdirp": "^3.0.1", + "yallist": "^5.0.0" }, "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/tailwindcss/node_modules/resolve": { - "version": "1.22.8", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", - "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", - "dependencies": { - "is-core-module": "^2.13.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/text-table": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", - "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==" - }, - "node_modules/thenify": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", - "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", - "dependencies": { - "any-promise": "^1.0.0" - } - }, - "node_modules/thenify-all": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", - "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", - "dependencies": { - "thenify": ">= 3.1.0 < 4" - }, - "engines": { - "node": ">=0.8" + "node": ">=18" } }, "node_modules/tiny-invariant": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", - "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==" + "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.14", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz", + "integrity": "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==", + "license": "MIT", + "dependencies": { + "fdir": "^6.4.4", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.4.6", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.6.tgz", + "integrity": "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==", + "license": "MIT", + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", "dependencies": { "is-number": "^7.0.0" }, @@ -5976,6 +6577,7 @@ "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=18.12" }, @@ -5983,16 +6585,12 @@ "typescript": ">=4.8.4" } }, - "node_modules/ts-interface-checker": { - "version": "0.1.13", - "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", - "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==" - }, "node_modules/tsconfck": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/tsconfck/-/tsconfck-3.0.3.tgz", - "integrity": "sha512-4t0noZX9t6GcPTfBAbIbbIU4pfpCwh0ueq3S4O/5qXI1VwK1outmxhe9dOiEWqMz3MW2LKgDTpqWV+37IWuVbA==", + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/tsconfck/-/tsconfck-3.1.6.tgz", + "integrity": "sha512-ks6Vjr/jEw0P1gmOVwutM3B7fWxoWBL2KRDb1JfqGVawBmO5UsvmWOQFGHBPl5yxYz4eERr19E6L7NMv+Fej4w==", "dev": true, + "license": "MIT", "bin": { "tsconfck": "bin/tsconfck.js" }, @@ -6012,6 +6610,7 @@ "version": "3.15.0", "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", "integrity": "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==", + "license": "MIT", "dependencies": { "@types/json5": "^0.0.29", "json5": "^1.0.2", @@ -6020,14 +6619,16 @@ } }, "node_modules/tslib": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "license": "MIT", "dependencies": { "prelude-ls": "^1.2.1" }, @@ -6035,21 +6636,11 @@ "node": ">= 0.8.0" } }, - "node_modules/type-fest": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", - "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/typed-array-buffer": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", + "license": "MIT", "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", @@ -6063,6 +6654,7 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", + "license": "MIT", "dependencies": { "call-bind": "^1.0.8", "for-each": "^0.3.3", @@ -6081,6 +6673,7 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", + "license": "MIT", "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", @@ -6101,6 +6694,7 @@ "version": "1.0.7", "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.7.tgz", "integrity": "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==", + "license": "MIT", "dependencies": { "call-bind": "^1.0.7", "for-each": "^0.3.3", @@ -6117,10 +6711,11 @@ } }, "node_modules/typescript": { - "version": "5.7.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz", - "integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==", + "version": "5.9.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz", + "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==", "devOptional": true, + "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -6133,6 +6728,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", + "license": "MIT", "dependencies": { "call-bound": "^1.0.3", "has-bigints": "^1.0.2", @@ -6147,9 +6743,9 @@ } }, "node_modules/update-browserslist-db": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.1.tgz", - "integrity": "sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", + "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", "dev": true, "funding": [ { @@ -6165,9 +6761,10 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "dependencies": { "escalade": "^3.2.0", - "picocolors": "^1.1.0" + "picocolors": "^1.1.1" }, "bin": { "update-browserslist-db": "cli.js" @@ -6180,22 +6777,25 @@ "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "license": "BSD-2-Clause", "dependencies": { "punycode": "^2.1.0" } }, "node_modules/use-sync-external-store": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz", - "integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz", + "integrity": "sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==", + "license": "MIT", "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "node_modules/usehooks-ts": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/usehooks-ts/-/usehooks-ts-3.1.1.tgz", "integrity": "sha512-I4diPp9Cq6ieSUH2wu+fDAVQO43xwtulo+fKEidHUwZPnYImbtkTjzIJYcDcJqxgmX31GVqNFURodvcgHcW0pA==", + "license": "MIT", "dependencies": { "lodash.debounce": "^4.0.8" }, @@ -6209,12 +6809,15 @@ "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" }, "node_modules/validator": { - "version": "13.12.0", - "resolved": "https://registry.npmjs.org/validator/-/validator-13.12.0.tgz", - "integrity": "sha512-c1Q0mCiPlgdTVVVIJIrBuxNicYE+t/7oKeI9MWLj3fh/uq2Pxh/3eeWbVZ4OcGW1TUf53At0njHw5SMdA3tmMg==", + "version": "13.15.15", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.15.15.tgz", + "integrity": "sha512-BgWVbCI72aIQy937xbawcs+hrVaN/CZ2UwutgaJ36hGqRrLNM+f5LUT/YPRbo8IV/ASeFzXszezV+y2+rq3l8A==", + "license": "MIT", "engines": { "node": ">= 0.10" } @@ -6223,6 +6826,7 @@ "version": "36.9.2", "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-36.9.2.tgz", "integrity": "sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==", + "license": "MIT AND ISC", "dependencies": { "@types/d3-array": "^3.0.3", "@types/d3-ease": "^3.0.0", @@ -6241,20 +6845,23 @@ } }, "node_modules/vite": { - "version": "5.2.8", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.2.8.tgz", - "integrity": "sha512-OyZR+c1CE8yeHw5V5t59aXsUPPVTHMDjEZz8MgguLL/Q7NblxhZUlTu9xSPqlsUO/y+X7dlU05jdhvyycD55DA==", - "dev": true, + "version": "6.3.5", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz", + "integrity": "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==", + "license": "MIT", "dependencies": { - "esbuild": "^0.20.1", - "postcss": "^8.4.38", - "rollup": "^4.13.0" + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" }, "bin": { "vite": "bin/vite.js" }, "engines": { - "node": "^18.0.0 || >=20.0.0" + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" }, "funding": { "url": "https://github.com/vitejs/vite?sponsor=1" @@ -6263,18 +6870,25 @@ "fsevents": "~2.3.3" }, "peerDependencies": { - "@types/node": "^18.0.0 || >=20.0.0", + "@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.4.0" + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" }, "peerDependenciesMeta": { "@types/node": { "optional": true }, + "jiti": { + "optional": true + }, "less": { "optional": true }, @@ -6284,6 +6898,9 @@ "sass": { "optional": true }, + "sass-embedded": { + "optional": true + }, "stylus": { "optional": true }, @@ -6292,6 +6909,12 @@ }, "terser": { "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true } } }, @@ -6300,6 +6923,7 @@ "resolved": "https://registry.npmjs.org/vite-tsconfig-paths/-/vite-tsconfig-paths-5.1.4.tgz", "integrity": "sha512-cYj0LRuLV2c2sMqhqhGpaO3LretdtMn/BVX4cPLanIZuwwrkVl+lK84E/miEXkCHWXuq65rhNN4rXsBcOB3S4w==", "dev": true, + "license": "MIT", "dependencies": { "debug": "^4.1.1", "globrex": "^0.1.2", @@ -6314,10 +6938,37 @@ } } }, + "node_modules/vite/node_modules/fdir": { + "version": "6.4.6", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.6.tgz", + "integrity": "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==", + "license": "MIT", + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", "dependencies": { "isexe": "^2.0.0" }, @@ -6332,6 +6983,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", + "license": "MIT", "dependencies": { "is-bigint": "^1.1.0", "is-boolean-object": "^1.2.1", @@ -6350,6 +7002,7 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz", "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==", + "license": "MIT", "dependencies": { "call-bound": "^1.0.2", "function.prototype.name": "^1.1.6", @@ -6376,6 +7029,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", + "license": "MIT", "dependencies": { "is-map": "^2.0.3", "is-set": "^2.0.3", @@ -6393,6 +7047,7 @@ "version": "1.1.19", "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz", "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==", + "license": "MIT", "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", @@ -6409,119 +7064,30 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/wrap-ansi": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", - "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", - "dependencies": { - "ansi-styles": "^6.1.0", - "string-width": "^5.0.1", - "strip-ansi": "^7.0.1" - }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "license": "MIT", "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + "node": ">=0.10.0" } }, - "node_modules/wrap-ansi-cjs": { - "name": "wrap-ansi", - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, + "node_modules/yallist": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "dev": true, + "license": "BlueOak-1.0.0", "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" - }, - "node_modules/wrap-ansi-cjs/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi/node_modules/ansi-regex": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", - "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/wrap-ansi/node_modules/ansi-styles": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", - "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/wrap-ansi/node_modules/strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" - }, - "node_modules/xterm": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/xterm/-/xterm-5.3.0.tgz", - "integrity": "sha512-8QqjlekLUFTrU6x7xck1MsPzPA571K5zNqWm0M0oroYEWVOptZ0+ubQSkQ3uxIEhcIHRujJy6emDWX4A7qyFzg==", - "deprecated": "This package is now deprecated. Move to @xterm/xterm instead." - }, - "node_modules/yaml": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.4.1.tgz", - "integrity": "sha512-pIXzoImaqmfOrL7teGUBt/T7ZDnyeGBWyXQBvOVhLkWLN37GXv8NMLK406UY6dS51JfcQHsmcW5cJ441bHg6Lg==", - "bin": { - "yaml": "bin.mjs" - }, - "engines": { - "node": ">= 14" + "node": ">=18" } }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "license": "MIT", "engines": { "node": ">=10" }, @@ -6530,11 +7096,12 @@ } }, "node_modules/zustand": { - "version": "4.5.2", - "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.2.tgz", - "integrity": "sha512-2cN1tPkDVkwCy5ickKrI7vijSjPksFRfqS6237NzT0vqSsztTNnQdHw9mmN7uBdk3gceVXU0a+21jFzFzAc9+g==", + "version": "4.5.7", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.7.tgz", + "integrity": "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==", + "license": "MIT", "dependencies": { - "use-sync-external-store": "1.2.0" + "use-sync-external-store": "^1.2.2" }, "engines": { "node": ">=12.7.0" diff --git a/ui/package.json b/ui/package.json index f8f1c7a..9f0c298 100644 --- a/ui/package.json +++ b/ui/package.json @@ -1,13 +1,14 @@ { "name": "kvm-ui", "private": true, - "version": "0.0.0", + "version": "2025.08.07.001", "type": "module", "engines": { - "node": "21.1.0" + "node": "22.15.0" }, "scripts": { "dev": "./dev_device.sh", + "dev:ssl": "USE_SSL=true ./dev_device.sh", "dev:cloud": "vite dev --mode=cloud-development", "build": "npm run build:prod", "build:device": "tsc && vite build --mode=device --emptyOutDir", @@ -18,59 +19,67 @@ "preview": "vite preview" }, "dependencies": { - "@headlessui/react": "^2.2.0", - "@headlessui/tailwindcss": "^0.2.1", + "@headlessui/react": "^2.2.7", + "@headlessui/tailwindcss": "^0.2.2", "@heroicons/react": "^2.2.0", + "@vitejs/plugin-basic-ssl": "^2.1.0", "@xterm/addon-clipboard": "^0.1.0", "@xterm/addon-fit": "^0.10.0", "@xterm/addon-unicode11": "^0.8.0", "@xterm/addon-web-links": "^0.11.0", "@xterm/addon-webgl": "^0.18.0", "@xterm/xterm": "^5.5.0", - "cva": "^1.0.0-beta.1", + "cva": "^1.0.0-beta.4", + "dayjs": "^1.11.13", "eslint-import-resolver-alias": "^1.1.2", - "focus-trap-react": "^10.2.3", - "framer-motion": "^11.15.0", + "focus-trap-react": "^11.0.4", + "framer-motion": "^12.23.12", "lodash.throttle": "^4.1.1", "mini-svg-data-uri": "^1.4.4", - "react": "^18.2.0", + "react": "^19.1.1", "react-animate-height": "^3.2.3", - "react-dom": "^18.2.0", - "react-hot-toast": "^2.4.1", - "react-icons": "^5.4.0", + "react-dom": "^19.1.1", + "react-hot-toast": "^2.5.2", + "react-icons": "^5.5.0", "react-router-dom": "^6.22.3", - "react-simple-keyboard": "^3.7.112", - "react-xtermjs": "^1.0.9", - "recharts": "^2.15.0", - "tailwind-merge": "^2.5.5", - "usehooks-ts": "^3.1.0", - "validator": "^13.12.0", - "xterm": "^5.3.0", + "react-simple-keyboard": "^3.8.106", + "react-use-websocket": "^4.13.0", + "react-xtermjs": "^1.0.10", + "recharts": "^2.15.3", + "tailwind-merge": "^3.3.1", + "usehooks-ts": "^3.1.1", + "validator": "^13.15.15", "zustand": "^4.5.2" }, "devDependencies": { - "@tailwindcss/forms": "^0.5.9", - "@tailwindcss/typography": "^0.5.15", - "@types/react": "^18.2.66", - "@types/react-dom": "^18.3.0", - "@types/semver": "^7.5.8", - "@types/validator": "^13.12.2", - "@typescript-eslint/eslint-plugin": "^8.25.0", - "@typescript-eslint/parser": "^8.25.0", - "@vitejs/plugin-react-swc": "^3.7.2", - "autoprefixer": "^10.4.20", - "eslint": "^8.20.0", - "eslint-config-prettier": "^10.0.1", - "eslint-plugin-import": "^2.31.0", - "eslint-plugin-react": "^7.37.4", - "eslint-plugin-react-hooks": "^5.1.0", - "eslint-plugin-react-refresh": "^0.4.19", - "postcss": "^8.4.49", - "prettier": "^3.4.2", - "prettier-plugin-tailwindcss": "^0.6.11", - "tailwindcss": "^3.4.17", - "typescript": "^5.7.2", - "vite": "^5.2.0", + "@eslint/compat": "^1.3.1", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "^9.32.0", + "@tailwindcss/forms": "^0.5.10", + "@tailwindcss/postcss": "^4.1.11", + "@tailwindcss/typography": "^0.5.16", + "@tailwindcss/vite": "^4.1.11", + "@types/react": "^19.1.9", + "@types/react-dom": "^19.1.7", + "@types/semver": "^7.7.0", + "@types/validator": "^13.15.2", + "@typescript-eslint/eslint-plugin": "^8.39.0", + "@typescript-eslint/parser": "^8.39.0", + "@vitejs/plugin-react-swc": "^3.10.2", + "autoprefixer": "^10.4.21", + "eslint": "^9.32.0", + "eslint-config-prettier": "^10.1.8", + "eslint-plugin-import": "^2.32.0", + "eslint-plugin-react": "^7.37.5", + "eslint-plugin-react-hooks": "^5.2.0", + "eslint-plugin-react-refresh": "^0.4.20", + "globals": "^16.3.0", + "postcss": "^8.5.6", + "prettier": "^3.6.2", + "prettier-plugin-tailwindcss": "^0.6.14", + "tailwindcss": "^4.1.11", + "typescript": "^5.9.2", + "vite": "^6.3.5", "vite-tsconfig-paths": "^5.1.4" } } diff --git a/ui/postcss.config.js b/ui/postcss.config.js index 2e7af2b..b6dc034 100644 --- a/ui/postcss.config.js +++ b/ui/postcss.config.js @@ -1,6 +1,5 @@ export default { plugins: { - tailwindcss: {}, autoprefixer: {}, }, } diff --git a/ui/public/sse.html b/ui/public/sse.html new file mode 120000 index 0000000..0a8b4f3 --- /dev/null +++ b/ui/public/sse.html @@ -0,0 +1 @@ +../../internal/logging/sse.html \ No newline at end of file diff --git a/ui/src/components/ActionBar.tsx b/ui/src/components/ActionBar.tsx index 1afef63..801cc7a 100644 --- a/ui/src/components/ActionBar.tsx +++ b/ui/src/components/ActionBar.tsx @@ -89,8 +89,8 @@ export default function Actionbar({ anchor="bottom start" transition className={cx( - "z-10 flex w-[420px] origin-top flex-col !overflow-visible", - "flex origin-top flex-col transition duration-300 ease-out data-[closed]:translate-y-8 data-[closed]:opacity-0", + "z-10 flex w-[420px] origin-top flex-col overflow-visible!", + "flex origin-top flex-col transition duration-300 ease-out data-closed:translate-y-8 data-closed:opacity-0", )} > {({ open }) => { @@ -131,8 +131,8 @@ export default function Actionbar({ anchor="bottom start" transition className={cx( - "z-10 flex w-[420px] origin-top flex-col !overflow-visible", - "flex origin-top flex-col transition duration-300 ease-out data-[closed]:translate-y-8 data-[closed]:opacity-0", + "z-10 flex w-[420px] origin-top flex-col overflow-visible!", + "flex origin-top flex-col transition duration-300 ease-out data-closed:translate-y-8 data-closed:opacity-0", )} > {({ open }) => { @@ -183,8 +183,8 @@ export default function Actionbar({ transitionProperty: "opacity", }} className={cx( - "z-10 flex w-[420px] origin-top flex-col !overflow-visible", - "flex origin-top flex-col transition duration-300 ease-out data-[closed]:translate-y-8 data-[closed]:opacity-0", + "z-10 flex w-[420px] origin-top flex-col overflow-visible!", + "flex origin-top flex-col transition duration-300 ease-out data-closed:translate-y-8 data-closed:opacity-0", )} > {({ open }) => { @@ -226,8 +226,8 @@ export default function Actionbar({ anchor="bottom start" transition className={cx( - "z-10 flex w-[420px] flex-col !overflow-visible", - "flex origin-top flex-col transition duration-300 ease-out data-[closed]:translate-y-8 data-[closed]:opacity-0", + "z-10 flex w-[420px] flex-col overflow-visible!", + "flex origin-top flex-col transition duration-300 ease-out data-closed:translate-y-8 data-closed:opacity-0", )} > {({ open }) => { @@ -262,7 +262,6 @@ export default function Actionbar({ }} /> -
+ + + + + ); +} \ No newline at end of file diff --git a/ui/src/components/DhcpLeaseCard.tsx b/ui/src/components/DhcpLeaseCard.tsx new file mode 100644 index 0000000..8a6e59c --- /dev/null +++ b/ui/src/components/DhcpLeaseCard.tsx @@ -0,0 +1,212 @@ +import { LuRefreshCcw } from "react-icons/lu"; + +import { Button } from "@/components/Button"; +import { GridCard } from "@/components/Card"; +import { LifeTimeLabel } from "@/routes/devices.$id.settings.network"; +import { NetworkState } from "@/hooks/stores"; + +export default function DhcpLeaseCard({ + networkState, + setShowRenewLeaseConfirm, +}: { + networkState: NetworkState; + setShowRenewLeaseConfirm: (show: boolean) => void; +}) { + return ( + +
+
+

+ DHCP Lease Information +

+ +
+
+ {networkState?.dhcp_lease?.ip && ( +
+ + IP Address + + + {networkState?.dhcp_lease?.ip} + +
+ )} + + {networkState?.dhcp_lease?.netmask && ( +
+ + Subnet Mask + + + {networkState?.dhcp_lease?.netmask} + +
+ )} + + {networkState?.dhcp_lease?.dns && ( +
+ + DNS Servers + + + {networkState?.dhcp_lease?.dns.map(dns =>
{dns}
)} +
+
+ )} + + {networkState?.dhcp_lease?.broadcast && ( +
+ + Broadcast + + + {networkState?.dhcp_lease?.broadcast} + +
+ )} + + {networkState?.dhcp_lease?.domain && ( +
+ + Domain + + + {networkState?.dhcp_lease?.domain} + +
+ )} + + {networkState?.dhcp_lease?.ntp_servers && + networkState?.dhcp_lease?.ntp_servers.length > 0 && ( +
+
+ NTP Servers +
+
+ {networkState?.dhcp_lease?.ntp_servers.map(server => ( +
{server}
+ ))} +
+
+ )} + + {networkState?.dhcp_lease?.hostname && ( +
+ + Hostname + + + {networkState?.dhcp_lease?.hostname} + +
+ )} +
+ +
+ {networkState?.dhcp_lease?.routers && + networkState?.dhcp_lease?.routers.length > 0 && ( +
+ + Gateway + + + {networkState?.dhcp_lease?.routers.map(router => ( +
{router}
+ ))} +
+
+ )} + + {networkState?.dhcp_lease?.server_id && ( +
+ + DHCP Server + + + {networkState?.dhcp_lease?.server_id} + +
+ )} + + {networkState?.dhcp_lease?.lease_expiry && ( +
+ + Lease Expires + + + + +
+ )} + + {networkState?.dhcp_lease?.mtu && ( +
+ MTU + + {networkState?.dhcp_lease?.mtu} + +
+ )} + + {networkState?.dhcp_lease?.ttl && ( +
+ TTL + + {networkState?.dhcp_lease?.ttl} + +
+ )} + + {networkState?.dhcp_lease?.bootp_next_server && ( +
+ + Boot Next Server + + + {networkState?.dhcp_lease?.bootp_next_server} + +
+ )} + + {networkState?.dhcp_lease?.bootp_server_name && ( +
+ + Boot Server Name + + + {networkState?.dhcp_lease?.bootp_server_name} + +
+ )} + + {networkState?.dhcp_lease?.bootp_file && ( +
+ + Boot File + + + {networkState?.dhcp_lease?.bootp_file} + +
+ )} +
+
+ +
+
+
+
+
+ ); +} diff --git a/ui/src/components/EmptyCard.tsx b/ui/src/components/EmptyCard.tsx index d8ba782..ad3370e 100644 --- a/ui/src/components/EmptyCard.tsx +++ b/ui/src/components/EmptyCard.tsx @@ -30,7 +30,7 @@ export default function EmptyCard({
{IconElm && ( - + )}

{headline} diff --git a/ui/src/components/FieldLabel.tsx b/ui/src/components/FieldLabel.tsx index 42e6ede..f9065a1 100644 --- a/ui/src/components/FieldLabel.tsx +++ b/ui/src/components/FieldLabel.tsx @@ -49,4 +49,4 @@ export default function FieldLabel({ } else { return <>; } -} +} \ No newline at end of file diff --git a/ui/src/components/GridBackground.tsx b/ui/src/components/GridBackground.tsx index bd2f617..7b4349e 100644 --- a/ui/src/components/GridBackground.tsx +++ b/ui/src/components/GridBackground.tsx @@ -1,8 +1,8 @@ export default function GridBackground() { return ( -
+
+
@@ -78,86 +81,82 @@ export default function DashboardNavbar({
- {showConnectionStatus && ( -
-
- -
-
- -
-
- )} - {isLoggedIn ? ( - <> -
- -
- -
- - - -
- {userEmail && ( -
- -
-
Logged in as
-
- {userEmail} + ) : userEmail ? ( + + {userEmail} + + ) : null} + + + + + + + {userEmail && ( +
+
+
+
+ Logged in as +
+
+ {userEmail} +
+
- -
- )} -
- -
-
-
-
-
- - -
- - ) : null} + + + + +
+ + ) : null} +
diff --git a/ui/src/components/InfoBar.tsx b/ui/src/components/InfoBar.tsx index aa00da7..7ce67a4 100644 --- a/ui/src/components/InfoBar.tsx +++ b/ui/src/components/InfoBar.tsx @@ -28,6 +28,7 @@ export default function InfoBar() { const rpcDataChannel = useRTCStore(state => state.rpcDataChannel); const settings = useSettingsStore(); + const showPressedKeys = useSettingsStore(state => state.showPressedKeys); useEffect(() => { if (!rpcDataChannel) return; @@ -36,9 +37,9 @@ export default function InfoBar() { console.log(`Error on DataChannel '${rpcDataChannel.label}': ${e}`); }, [rpcDataChannel]); - const isCapsLockActive = useHidStore(state => state.isCapsLockActive); - const isNumLockActive = useHidStore(state => state.isNumLockActive); - const isScrollLockActive = useHidStore(state => state.isScrollLockActive); + const keyboardLedState = useHidStore(state => state.keyboardLedState); + const keyboardLedStateSyncAvailable = useHidStore(state => state.keyboardLedStateSyncAvailable); + const keyboardLedSync = useSettingsStore(state => state.keyboardLedSync); const isTurnServerInUse = useRTCStore(state => state.isTurnServerInUse); @@ -97,19 +98,21 @@ export default function InfoBar() {
)} -
- Keys: -

- {[ - ...activeKeys.map( - x => Object.entries(keys).filter(y => y[1] === x)[0][0], - ), - activeModifiers.map( - x => Object.entries(modifiers).filter(y => y[1] === x)[0][0], - ), - ].join(", ")} -

-
+ {showPressedKeys && ( +
+ Keys: +

+ {[ + ...activeKeys.map( + x => Object.entries(keys).filter(y => y[1] === x)[0][0], + ), + activeModifiers.map( + x => Object.entries(modifiers).filter(y => y[1] === x)[0][0], + ), + ].join(", ")} +

+
+ )}

@@ -118,10 +121,24 @@ export default function InfoBar() { Relayed by Cloudflare
)} + + {keyboardLedStateSyncAvailable ? ( +
+ {keyboardLedSync === "browser" ? "Browser" : "Host"} +
+ ) : null}
Scroll Lock
+ {keyboardLedState?.compose ? ( +
+ Compose +
+ ) : null} + {keyboardLedState?.kana ? ( +
+ Kana +
+ ) : null}
diff --git a/ui/src/components/InputField.tsx b/ui/src/components/InputField.tsx index 2f580a0..ff2ad55 100644 --- a/ui/src/components/InputField.tsx +++ b/ui/src/components/InputField.tsx @@ -1,5 +1,5 @@ import type { Ref } from "react"; -import React, { forwardRef } from "react"; +import React, { forwardRef, JSX } from "react"; import clsx from "clsx"; import FieldLabel from "@/components/FieldLabel"; @@ -44,7 +44,7 @@ const InputField = forwardRef(function InputF "[&:has(:user-invalid)]:ring-2 [&:has(:user-invalid)]:ring-red-600 [&:has(:user-invalid)]:ring-offset-2", // Focus Within - "focus-within:border-slate-300 dark:focus-within:border-slate-600 focus-within:outline-none focus-within:ring-2 focus-within:ring-blue-700 focus-within:ring-offset-2", + "focus-within:border-slate-300 dark:focus-within:border-slate-600 focus-within:outline-hidden focus-within:ring-2 focus-within:ring-blue-700 focus-within:ring-offset-2", // Disabled Within "disabled-within:pointer-events-none disabled-within:select-none disabled-within:bg-slate-50 dark:disabled-within:bg-slate-800 disabled-within:text-slate-500/80", diff --git a/ui/src/components/Ipv6NetworkCard.tsx b/ui/src/components/Ipv6NetworkCard.tsx new file mode 100644 index 0000000..a31b78e --- /dev/null +++ b/ui/src/components/Ipv6NetworkCard.tsx @@ -0,0 +1,93 @@ +import { NetworkState } from "../hooks/stores"; +import { LifeTimeLabel } from "../routes/devices.$id.settings.network"; + +import { GridCard } from "./Card"; + +export default function Ipv6NetworkCard({ + networkState, +}: { + networkState: NetworkState; +}) { + return ( + +
+
+

+ IPv6 Information +

+ +
+ {networkState?.dhcp_lease?.ip && ( +
+ + Link-local + + + {networkState?.ipv6_link_local} + +
+ )} +
+ +
+ {networkState?.ipv6_addresses && networkState?.ipv6_addresses.length > 0 && ( +
+

IPv6 Addresses

+ {networkState.ipv6_addresses.map( + addr => ( +
+
+
+ + Address + + {addr.address} +
+ + {addr.valid_lifetime && ( +
+ + Valid Lifetime + + + {addr.valid_lifetime === "" ? ( + + N/A + + ) : ( + + )} + +
+ )} + {addr.preferred_lifetime && ( +
+ + Preferred Lifetime + + + {addr.preferred_lifetime === "" ? ( + + N/A + + ) : ( + + )} + +
+ )} +
+
+ ), + )} +
+ )} +
+
+
+
+ ); +} diff --git a/ui/src/components/KvmCard.tsx b/ui/src/components/KvmCard.tsx index c680a37..91296c5 100644 --- a/ui/src/components/KvmCard.tsx +++ b/ui/src/components/KvmCard.tsx @@ -78,7 +78,7 @@ export default function KvmCard({ )} -
+
{online ? ( @@ -111,9 +111,9 @@ export default function KvmCard({ - +
diff --git a/ui/src/components/LoadingSpinner.tsx b/ui/src/components/LoadingSpinner.tsx index 27dac76..261d755 100644 --- a/ui/src/components/LoadingSpinner.tsx +++ b/ui/src/components/LoadingSpinner.tsx @@ -7,7 +7,7 @@ export default function LoadingSpinner({ }) { return ( { + setSendFn(send); + + if (!initialized) { + loadMacros(); + } + }, [initialized, loadMacros, setSendFn, send]); + + if (macros.length === 0) { + return null; + } + + return ( + +
+
+ +
+
+ {macros.map(macro => ( +
+
+ + ); +} \ No newline at end of file diff --git a/ui/src/components/MacroForm.tsx b/ui/src/components/MacroForm.tsx new file mode 100644 index 0000000..f74c4ae --- /dev/null +++ b/ui/src/components/MacroForm.tsx @@ -0,0 +1,290 @@ +import { useState } from "react"; +import { LuPlus } from "react-icons/lu"; + +import { KeySequence } from "@/hooks/stores"; +import { Button } from "@/components/Button"; +import { InputFieldWithLabel, FieldError } from "@/components/InputField"; +import Fieldset from "@/components/Fieldset"; +import { MacroStepCard } from "@/components/MacroStepCard"; +import { + DEFAULT_DELAY, + MAX_STEPS_PER_MACRO, + MAX_KEYS_PER_STEP, +} from "@/constants/macros"; +import FieldLabel from "@/components/FieldLabel"; + +interface ValidationErrors { + name?: string; + steps?: Record< + number, + { + keys?: string; + modifiers?: string; + delay?: string; + } + >; +} + +interface MacroFormProps { + initialData: Partial; + onSubmit: (macro: Partial) => Promise; + onCancel: () => void; + isSubmitting?: boolean; + submitText?: string; +} + +export function MacroForm({ + initialData, + onSubmit, + onCancel, + isSubmitting = false, + submitText = "Save Macro", +}: MacroFormProps) { + const [macro, setMacro] = useState>(initialData); + const [keyQueries, setKeyQueries] = useState>({}); + const [errors, setErrors] = useState({}); + const [errorMessage, setErrorMessage] = useState(null); + + const showTemporaryError = (message: string) => { + setErrorMessage(message); + setTimeout(() => setErrorMessage(null), 3000); + }; + + const validateForm = (): boolean => { + const newErrors: ValidationErrors = {}; + + // Name validation + if (!macro.name?.trim()) { + newErrors.name = "Name is required"; + } else if (macro.name.trim().length > 50) { + newErrors.name = "Name must be less than 50 characters"; + } + + if (!macro.steps?.length) { + newErrors.steps = { 0: { keys: "At least one step is required" } }; + } else { + const hasKeyOrModifier = macro.steps.some( + step => (step.keys?.length || 0) > 0 || (step.modifiers?.length || 0) > 0, + ); + + if (!hasKeyOrModifier) { + newErrors.steps = { + 0: { keys: "At least one step must have keys or modifiers" }, + }; + } + } + + setErrors(newErrors); + return Object.keys(newErrors).length === 0; + }; + + const handleSubmit = async () => { + if (!validateForm()) { + showTemporaryError("Please fix the validation errors"); + return; + } + + try { + await onSubmit(macro); + } catch (error) { + if (error instanceof Error) { + showTemporaryError(error.message); + } else { + showTemporaryError("An error occurred while saving"); + } + } + }; + + const handleKeySelect = ( + stepIndex: number, + option: { value: string | null; keys?: string[] }, + ) => { + const newSteps = [...(macro.steps || [])]; + if (!newSteps[stepIndex]) return; + + if (option.keys) { + newSteps[stepIndex].keys = option.keys; + } else if (option.value) { + if (!newSteps[stepIndex].keys) { + newSteps[stepIndex].keys = []; + } + const keysArray = Array.isArray(newSteps[stepIndex].keys) + ? newSteps[stepIndex].keys + : []; + if (keysArray.length >= MAX_KEYS_PER_STEP) { + showTemporaryError(`Maximum of ${MAX_KEYS_PER_STEP} keys per step allowed`); + return; + } + newSteps[stepIndex].keys = [...keysArray, option.value]; + } + setMacro({ ...macro, steps: newSteps }); + + if (errors.steps?.[stepIndex]?.keys) { + const newErrors = { ...errors }; + delete newErrors.steps?.[stepIndex].keys; + if (Object.keys(newErrors.steps?.[stepIndex] || {}).length === 0) { + delete newErrors.steps?.[stepIndex]; + } + if (Object.keys(newErrors.steps || {}).length === 0) { + delete newErrors.steps; + } + setErrors(newErrors); + } + }; + + const handleKeyQueryChange = (stepIndex: number, query: string) => { + setKeyQueries(prev => ({ ...prev, [stepIndex]: query })); + }; + + const handleModifierChange = (stepIndex: number, modifiers: string[]) => { + const newSteps = [...(macro.steps || [])]; + newSteps[stepIndex].modifiers = modifiers; + setMacro({ ...macro, steps: newSteps }); + + // Clear step errors when modifiers are added + if (errors.steps?.[stepIndex]?.keys && modifiers.length > 0) { + const newErrors = { ...errors }; + delete newErrors.steps?.[stepIndex].keys; + if (Object.keys(newErrors.steps?.[stepIndex] || {}).length === 0) { + delete newErrors.steps?.[stepIndex]; + } + if (Object.keys(newErrors.steps || {}).length === 0) { + delete newErrors.steps; + } + setErrors(newErrors); + } + }; + + const handleDelayChange = (stepIndex: number, delay: number) => { + const newSteps = [...(macro.steps || [])]; + newSteps[stepIndex].delay = delay; + setMacro({ ...macro, steps: newSteps }); + }; + + const handleStepMove = (stepIndex: number, direction: "up" | "down") => { + const newSteps = [...(macro.steps || [])]; + const newIndex = direction === "up" ? stepIndex - 1 : stepIndex + 1; + [newSteps[stepIndex], newSteps[newIndex]] = [newSteps[newIndex], newSteps[stepIndex]]; + setMacro({ ...macro, steps: newSteps }); + }; + + const isMaxStepsReached = (macro.steps?.length || 0) >= MAX_STEPS_PER_MACRO; + + return ( + <> +
+
+ { + setMacro(prev => ({ ...prev, name: e.target.value })); + if (errors.name) { + const newErrors = { ...errors }; + delete newErrors.name; + setErrors(newErrors); + } + }} + /> +
+ +
+
+
+ +
+ + {macro.steps?.length || 0}/{MAX_STEPS_PER_MACRO} steps + +
+ {errors.steps && errors.steps[0]?.keys && ( +
+ +
+ )} +
+
+ {(macro.steps || []).map((step, stepIndex) => ( + 1 + ? () => { + const newSteps = [...(macro.steps || [])]; + newSteps.splice(stepIndex, 1); + setMacro(prev => ({ ...prev, steps: newSteps })); + } + : undefined + } + onMoveUp={() => handleStepMove(stepIndex, "up")} + onMoveDown={() => handleStepMove(stepIndex, "down")} + onKeySelect={option => handleKeySelect(stepIndex, option)} + onKeyQueryChange={query => handleKeyQueryChange(stepIndex, query)} + keyQuery={keyQueries[stepIndex] || ""} + onModifierChange={modifiers => + handleModifierChange(stepIndex, modifiers) + } + onDelayChange={delay => handleDelayChange(stepIndex, delay)} + isLastStep={stepIndex === (macro.steps?.length || 0) - 1} + /> + ))} +
+
+ +
+
+ + {errorMessage && ( +
+ +
+ )} + +
+
+
+
+ + ); +} diff --git a/ui/src/components/MacroStepCard.tsx b/ui/src/components/MacroStepCard.tsx new file mode 100644 index 0000000..8642c28 --- /dev/null +++ b/ui/src/components/MacroStepCard.tsx @@ -0,0 +1,235 @@ +import { LuArrowUp, LuArrowDown, LuX, LuTrash2 } from "react-icons/lu"; + +import { Button } from "@/components/Button"; +import { Combobox } from "@/components/Combobox"; +import { SelectMenuBasic } from "@/components/SelectMenuBasic"; +import Card from "@/components/Card"; +import { keys, modifiers, keyDisplayMap } from "@/keyboardMappings"; +import { MAX_KEYS_PER_STEP, DEFAULT_DELAY } from "@/constants/macros"; +import FieldLabel from "@/components/FieldLabel"; + +// Filter out modifier keys since they're handled in the modifiers section +const modifierKeyPrefixes = ['Alt', 'Control', 'Shift', 'Meta']; + +const keyOptions = Object.keys(keys) + .filter(key => !modifierKeyPrefixes.some(prefix => key.startsWith(prefix))) + .map(key => ({ + value: key, + label: keyDisplayMap[key] || key, + })); + +const modifierOptions = Object.keys(modifiers).map(modifier => ({ + value: modifier, + label: modifier.replace(/^(Control|Alt|Shift|Meta)(Left|Right)$/, "$1 $2"), +})); + +const groupedModifiers: Record = { + Control: modifierOptions.filter(mod => mod.value.startsWith('Control')), + Shift: modifierOptions.filter(mod => mod.value.startsWith('Shift')), + Alt: modifierOptions.filter(mod => mod.value.startsWith('Alt')), + Meta: modifierOptions.filter(mod => mod.value.startsWith('Meta')), +}; + +const basePresetDelays = [ + { value: "50", label: "50ms" }, + { value: "100", label: "100ms" }, + { value: "200", label: "200ms" }, + { value: "300", label: "300ms" }, + { value: "500", label: "500ms" }, + { value: "750", label: "750ms" }, + { value: "1000", label: "1000ms" }, + { value: "1500", label: "1500ms" }, + { value: "2000", label: "2000ms" }, +]; + +const PRESET_DELAYS = basePresetDelays.map(delay => { + if (parseInt(delay.value, 10) === DEFAULT_DELAY) { + return { ...delay, label: "Default" }; + } + return delay; +}); + +interface MacroStep { + keys: string[]; + modifiers: string[]; + delay: number; +} + +interface MacroStepCardProps { + step: MacroStep; + stepIndex: number; + onDelete?: () => void; + onMoveUp?: () => void; + onMoveDown?: () => void; + onKeySelect: (option: { value: string | null; keys?: string[] }) => void; + onKeyQueryChange: (query: string) => void; + keyQuery: string; + onModifierChange: (modifiers: string[]) => void; + onDelayChange: (delay: number) => void; + isLastStep: boolean; +} + +const ensureArray = (arr: T[] | null | undefined): T[] => { + return Array.isArray(arr) ? arr : []; +}; + +export function MacroStepCard({ + step, + stepIndex, + onDelete, + onMoveUp, + onMoveDown, + onKeySelect, + onKeyQueryChange, + keyQuery, + onModifierChange, + onDelayChange, + isLastStep +}: MacroStepCardProps) { + const getFilteredKeys = () => { + const selectedKeys = ensureArray(step.keys); + const availableKeys = keyOptions.filter(option => !selectedKeys.includes(option.value)); + + if (keyQuery === '') { + return availableKeys; + } else { + return availableKeys.filter(option => option.label.toLowerCase().includes(keyQuery.toLowerCase())); + } + }; + + return ( + +
+
+ + {stepIndex + 1} + +
+ +
+
+
+ {onDelete && ( +
+
+ +
+
+ +
+ {Object.entries(groupedModifiers).map(([group, mods]) => ( +
+ + {group} + +
+ {mods.map(option => ( +
+
+ ))} +
+
+ +
+
+ +
+ {ensureArray(step.keys) && step.keys.length > 0 && ( +
+ {step.keys.map((key, keyIndex) => ( + + + {keyDisplayMap[key] || key} + +
+ )} +
+ { + onKeySelect(value); + onKeyQueryChange(''); + }} + displayValue={() => keyQuery} + onInputChange={onKeyQueryChange} + options={getFilteredKeys} + disabledMessage="Max keys reached" + size="SM" + immediate + disabled={ensureArray(step.keys).length >= MAX_KEYS_PER_STEP} + placeholder={ensureArray(step.keys).length >= MAX_KEYS_PER_STEP ? "Max keys reached" : "Search for key..."} + emptyMessage="No matching keys found" + /> +
+
+ +
+
+ +
+
+ onDelayChange(parseInt(e.target.value, 10))} + options={PRESET_DELAYS} + /> +
+
+
+
+ ); +} \ No newline at end of file diff --git a/ui/src/components/Modal.tsx b/ui/src/components/Modal.tsx index 039b493..d9433df 100644 --- a/ui/src/components/Modal.tsx +++ b/ui/src/components/Modal.tsx @@ -15,19 +15,21 @@ const Modal = React.memo(function Modal({ onClose: () => void; }) { return ( - + -
+
{/* TODO: This doesn't work well with other-sessions */}
diff --git a/ui/src/components/SelectMenuBasic.tsx b/ui/src/components/SelectMenuBasic.tsx index c518bfe..d5e9597 100644 --- a/ui/src/components/SelectMenuBasic.tsx +++ b/ui/src/components/SelectMenuBasic.tsx @@ -1,4 +1,4 @@ -import React from "react"; +import React, { JSX } from "react"; import clsx from "clsx"; import FieldLabel from "@/components/FieldLabel"; @@ -63,7 +63,7 @@ export const SelectMenuBasic = React.forwardRef {label && } - +