diff --git a/.github/workflows/firmware.yml b/.github/workflows/firmware.yml new file mode 100644 index 0000000..3db5374 --- /dev/null +++ b/.github/workflows/firmware.yml @@ -0,0 +1,145 @@ +# Unit tests and firmware builds on every push and pull request. A tag `v` (e.g. v2.0.0) +# additionally publishes a GitHub release with the flash images and release.json, which the +# NetIgnite server reads for its web installer and OTA updates. +# +# Optional repository secret FIRMWARE_SIGNING_KEY: a PEM ECDSA P-256 private key +# (scripts/generate_signing_key.sh). With it, releases include firmware.sig. + +name: Firmware + +on: + push: + branches: ["**"] + tags: ["v*"] + pull_request: + workflow_dispatch: + +env: + PLATFORMIO_VERSION: "6.2.0" + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + - run: pip install "platformio==${PLATFORMIO_VERSION}" + - run: pio test -e native + + build: + needs: test + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 # git describe for the version + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + - uses: actions/cache@v4 + with: + path: ~/.platformio + key: platformio-${{ runner.os }}-${{ hashFiles('platformio.ini') }} + - run: pip install "platformio==${PLATFORMIO_VERSION}" + + - name: Version + run: | + if [[ "$GITHUB_REF" == refs/tags/v* ]]; then + echo "NETIGNITE_FW_VERSION=${GITHUB_REF_NAME#v}" >> "$GITHUB_ENV" + fi + + - run: pio run -e esp32dev -e esp32dev-qemu + - run: python scripts/check_size.py + + - name: Sign + if: startsWith(github.ref, 'refs/tags/v') + env: + FIRMWARE_SIGNING_KEY: ${{ secrets.FIRMWARE_SIGNING_KEY }} + run: | + if [ -n "$FIRMWARE_SIGNING_KEY" ]; then + printf '%s' "$FIRMWARE_SIGNING_KEY" > signing-key.pem + openssl dgst -sha256 -sign signing-key.pem -out firmware.sig .pio/build/esp32dev/firmware.bin + rm signing-key.pem + echo "SIGNATURE_ARG=--signature firmware.sig" >> "$GITHUB_ENV" + else + echo "::warning::FIRMWARE_SIGNING_KEY isn't set; the release is unsigned." + fi + + - name: Collect + run: | + python scripts/collect_release.py "${NETIGNITE_FW_VERSION:-dev}" release ${SIGNATURE_ARG:-} + cp .pio/build/esp32dev/firmware.factory.bin release/firmware-factory.bin + + - uses: actions/upload-artifact@v4 + with: + name: firmware + path: release/ + retention-days: 14 + + - uses: actions/upload-artifact@v4 + with: + name: firmware-qemu + path: | + .pio/build/esp32dev-qemu/firmware.factory.bin + .pio/build/esp32dev-qemu/firmware.bin + retention-days: 3 + + # Boots the firmware in Espressif's QEMU and tests Improv, the console and OTA updates + # (test/emulator). + emulator: + needs: build + runs-on: ubuntu-latest + timeout-minutes: 45 + env: + QEMU_URL: https://github.com/espressif/qemu/releases/download/esp-develop-9.2.2-20260417/qemu-xtensa-softmmu-esp_develop_9.2.2_20260417-x86_64-linux-gnu.tar.xz + QEMU_SHA256: 0eecb2a34a5586c0e59110f77b9343b7b336e82fdb0e1a30e1dc1bab8a547e35 + steps: + - uses: actions/checkout@v4 + - uses: actions/download-artifact@v4 + with: + name: firmware-qemu + path: qemu-firmware + - uses: oven-sh/setup-bun@v2 + - name: QEMU + run: | + sudo apt-get update && sudo apt-get install -y libsdl2-2.0-0 libslirp0 + curl -sSL -o qemu.tar.xz "$QEMU_URL" + echo "$QEMU_SHA256 qemu.tar.xz" | sha256sum -c - + tar -xf qemu.tar.xz + - name: Improv and console + working-directory: test/emulator + run: | + bun install --frozen-lockfile + bun improv-test.ts "$GITHUB_WORKSPACE/qemu/bin/qemu-system-xtensa" "$GITHUB_WORKSPACE/qemu-firmware/firmware.factory.bin" "$RUNNER_TEMP" + - name: OTA updates + working-directory: test/emulator + run: | + bun ota-test.ts "$GITHUB_WORKSPACE/qemu/bin/qemu-system-xtensa" "$GITHUB_WORKSPACE/qemu-firmware/firmware.factory.bin" "$GITHUB_WORKSPACE/qemu-firmware/firmware.bin" "$RUNNER_TEMP" + - uses: actions/upload-artifact@v4 + if: failure() + with: + name: emulator-serial-log + path: | + ${{ runner.temp }}/improv-transcript.log + ${{ runner.temp }}/ota-transcript.log + retention-days: 7 + + # Tags only, after the unit, build and emulator checks passed. + release: + if: startsWith(github.ref, 'refs/tags/v') + needs: [build, emulator] + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - uses: actions/download-artifact@v4 + with: + name: firmware + path: release + - uses: softprops/action-gh-release@v2 + with: + files: release/* + prerelease: ${{ contains(github.ref_name, '-') }} + generate_release_notes: true diff --git a/.gitignore b/.gitignore index d9ec109..286ab37 100644 --- a/.gitignore +++ b/.gitignore @@ -4,9 +4,18 @@ .vscode/launch.json .vscode/ipch -src/config/* -!src/config/*sample* +# Generated at build time by data/cert/run_gen_script.py +*x509_crt_bundle.bin +# Release output (scripts/collect_release.py) +release/ + +# Never commit signing keys or other secrets +*signing-key*.pem *secret* -!*secrets.hpp.template -*x509_crt_bundle.bin \ No newline at end of file + +# Local leftovers of the v1 Electron installer (removed in v2; replaced by the web installer) +/installer/ + +# v1 configuration (compiled-in Wi-Fi password and agent secret). v2 keeps settings in NVS. +src/config/ diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..188c38c --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,57 @@ +# Changelog + +## 2.0.0 (2026-10-21) + +The first stable release of the ESP32 firmware v2, for NetIgnite server 2.0.0. No changes since +2.0.0-beta.2; everything it can do is listed below. + +### Upgrading from v1 + +Boards with the v1 firmware (built with PlatformIO and `config.h`) can't connect to a v2 server. +Install 2.0.0 once over USB with the dashboard's web installer (the agent's **Install** tab, in +Chrome, Edge or Firefox 151+): the partition table changed for updates over the network. Later +versions install over the network. + +## 2.0.0-beta.2 (2026-10-21) + +Needs NetIgnite server 2.0.0-beta.2 or newer for updates, the console and SSH. + +### New + +- **Firmware updates over the network** (`OTA_BEGIN`): the agent downloads the image from its + server, checks size and SHA-256 and, if the firmware has a public key compiled in + (`keys/firmware-signing.pub.pem`), the ECDSA P-256 signature. The new firmware is on probation + until it reaches the server; if it crashes or doesn't get there within 5 minutes, the previous + firmware starts again and the agent reports the failed version. +- **Console** for the dashboard: the last 16 KB of log lines (including ESP-IDF's messages), a live + log stream, current diagnostics, log level, server address, host name and IP settings (a new + server address or IP setting is only saved once the agent reaches the server with it), restart + and factory reset. +- **SSH logins** to devices in the agent's network (libssh): status checks that only authenticate, + and shutdown/reboot with a trusted host key and fixed commands. +- The web installer can set the agent's name. + +### Changed + +- Enrollment sends the agent kind; a token of a Linux agent is rejected without being used up. +- Firmware size with libssh: 79% of an OTA slot. + +### Tests + +- The emulator tests install updates over the serial port and check signature and SHA-256 + rejection, probation and rollback in QEMU; CI runs them. + +## 2.0.0-beta.1 (2026-10-20) + +Firmware v2 for NetIgnite server v2. Flash it once over USB (or with the server's web installer): +the partition table changed for over-the-air updates. + +- Settings in NVS instead of a compiled-in `config.h`: Wi-Fi (Improv or the serial console), + server URL, enrollment token, static IP, name, log level. +- Enrollment with a single-use token; the agent generates its own secret. +- Agent protocol v2: Wake-on-LAN, status checks by IPv6 link-local (from the MAC), IPv4, IPv6, + host name and TCP port. +- Serial console commands (`help`, `status`, `setup`, `wifi`, `server`, `enroll`, `ip`, `name`, + `log`, `reboot`, `factory-reset`). +- Two 1.875 MB app slots; pioarduino (Arduino-ESP32 3.3, ESP-IDF 5.5). +- Releases with the flash images and `release.json` for the server's web installer. diff --git a/README.md b/README.md new file mode 100644 index 0000000..a7792c2 --- /dev/null +++ b/README.md @@ -0,0 +1,128 @@ +# NetIgnite ESP32 Agent + +Firmware that turns an ESP32 into a [NetIgnite](https://netignite.dev) agent: it joins your Wi-Fi, +keeps a connection to your NetIgnite server, sends Wake-on-LAN packets, checks whether devices in +its network are online and can shut them down over SSH. It updates itself over the network. + +> **v2 (in development).** Works with NetIgnite server v2. Configuration no longer lives in +> `config.h`: the agent is set up from the browser with the server's web installer, or over the +> serial port. + +## Hardware + +- A classic ESP32 board (PlatformIO `esp32dev`, 4 MB flash). +- 2.4 GHz Wi-Fi (WPA/WPA2/WPA3-Personal). +- In the same network (subnet) as the devices it wakes: Wake-on-LAN broadcasts don't cross routers. + +## Set up an agent + +The easiest way is the server's **web installer**: in the NetIgnite dashboard, **Agents → Add +agent → ESP32**, then the agent's **Install** tab. It flashes the firmware from the browser +(Chrome, Edge or Firefox 151+, over HTTPS) together with an NVS image that holds the server URL, +the agent's name and a single-use enrollment token, and then sends your Wi-Fi settings to the +ESP32 with [Improv Wi-Fi](https://www.improv-wifi.com/serial/). The Wi-Fi password never reaches +the server. The installer also changes the Wi-Fi of an installed agent. + +Without the installer, over the serial port: + +1. In the NetIgnite dashboard: **Agents → Add agent → ESP32**. +2. Flash the firmware (see [Build and flash](#build-and-flash)) and open the serial console + (115200 baud, e.g. `pio device monitor`). +3. Connect to Wi-Fi: + ```text + wifi "My Network" "my password" + ``` +4. On the agent's page in the dashboard, click **Create setup command** and paste it: + ```text + setup https://app.netignite.dev netignite_enroll_… + ``` + +The agent enrolls once with that single-use token. It generates its own secret with the hardware +random number generator; the server only stores a hash of it, and it is never shown or printed. + +## Console commands + +| Command | | +| --- | --- | +| `status` | Firmware, network, server and connection state | +| `setup ` | Connect to a NetIgnite server | +| `wifi [password]` | Connect to a network; kept only if the connection works (quote names with spaces) | +| `server ` | Change the server address | +| `enroll ` | Set the agent up again with a new token | +| `ip dhcp` / `ip static [dns1] [dns2]` | IPv4 configuration (DHCP is the default) | +| `name ` | Host name | +| `log ` | Log level | +| `reboot` | Restart | +| `factory-reset confirm` | Erase Wi-Fi, server and credentials | + +## What the agent does + +It connects to `/ws/agent` with a WebSocket (`wss://` for `https://` servers) and speaks +NetIgnite protocol v2 (JSON frames; see `lib/netignite_core/src/netignite/Protocol.h`): + +- `WAKE`: sends the magic packet 3 times to the subnet broadcast address and `255.255.255.255`. +- `STATUS`: pings the device (IPv4, IPv6, or the IPv6 link-local address derived from its MAC + address) and/or tries a TCP port. A refused TCP connection also counts as online. Host names + resolve to IPv4 and IPv6 addresses. +- `PING`: keep-alive. Without any message from the server for 100 s, the agent reconnects. +- `OTA_BEGIN`: downloads a firmware image from its server (with its own credential), checks its + size and SHA-256 and, if the firmware has a public key compiled in, its signature, writes it to + the other app slot and restarts. The new firmware is on probation until it reaches the server; + if it crashes or doesn't get there within 5 minutes, the bootloader starts the previous firmware + again, and the agent reports the failed version in its HELLO. +- `GET_LOGS`, `LOG_SUBSCRIBE`: the last 16 KB of log lines (also ESP-IDF's own messages), and new + lines as `LOG` events, for the dashboard's console. +- `CONFIG_SET`: server address, host name, log level and IP settings. A new server address or IP + setting is tried first and only saved once the agent reaches the server with it; otherwise the + agent restarts with the previous settings after 2 minutes. +- `SSH_CHECK`, `SSH_EXEC`: logs in to a device in its network over SSH (libssh) with the login the + server stores for that device. A check only authenticates and never opens a session; a command + needs the trusted host key and runs only `[sudo -n ]shutdown -h|-r now` or + `shutdown /s|/r /t 0`. The password or key is wiped from memory after each login. +- `GET_INFO`, `REBOOT`, `FACTORY_RESET`. + +HTTPS servers need a certificate from a public CA. For a private CA, put its certificate (`.pem` +or `.der`) into `data/cert/custom/` before building. + +## Build and flash + +Requires [PlatformIO](https://platformio.org) Core **6.2 or newer** (`pio upgrade`). The build +uses [pioarduino](https://github.com/pioarduino/platform-espressif32) (Arduino-ESP32 3.3, +ESP-IDF 5.5), pinned in `platformio.ini`. + +```bash +pio run -e esp32dev # build +pio run -e esp32dev -t upload # flash over USB +pio device monitor # serial console +``` + +The partition table (`partitions.csv`) has two 1.875 MB app slots for over-the-air updates. If the +board ran v1 firmware, flash v2 once over USB. + +## Tests + +```bash +pio test -e native # parsers and encoders, on your computer (needs gcc) +python scripts/check_size.py # the firmware must stay below 85 % of an OTA slot +``` + +`test/emulator/` boots the firmware in Espressif's QEMU and tests Improv and the console with the +same client ESP Web Tools uses, and OTA updates (signature checks, probation, rollback) with images +sent over the serial port. QEMU has no Wi-Fi, so networking needs a real board. + +## Releases + +Pushing a tag `v` runs `.github/workflows/firmware.yml`: unit tests, build, size check and +emulator test, then a GitHub release with `bootloader.bin`, `partitions.bin`, `boot_app0.bin`, +`firmware.bin`, a merged `firmware-factory.bin` (flash at `0x0`) and `release.json`, which +describes the parts for the NetIgnite server's web installer. + +Releases are signed when the repository secret `FIRMWARE_SIGNING_KEY` is set; create the key pair +with `sh scripts/generate_signing_key.sh` and commit only the public key +(`keys/firmware-signing.pub.pem`). Once that file exists, `scripts/signing_key.py` compiles it into +the firmware, and the agent only installs OTA updates with a valid signature. Set the same public +key as `NETIGNITE_FIRMWARE_PUBLIC_KEY` on the server, so it only offers signed releases. + +## License + +[AGPL-3.0](LICENSE) diff --git a/data/cert/run_gen_script.py b/data/cert/run_gen_script.py index 175c9c4..0047b8b 100644 --- a/data/cert/run_gen_script.py +++ b/data/cert/run_gen_script.py @@ -10,11 +10,14 @@ import os import shutil import subprocess +import sys import tempfile Import("env") # noqa: F821 - provided by PlatformIO (SCons) -PYTHON = env.subst("$PYTHONEXE") # noqa: F821 +# The interpreter running this script: the cryptography check below and the generator must use the +# same one (pioarduino runs the build in its own environment, separate from $PYTHONEXE). +PYTHON = sys.executable CERT_DIR = os.path.join(env.subst("$PROJECT_DIR"), "data", "cert") # noqa: F821 CUSTOM_DIR = os.path.join(CERT_DIR, "custom") BUNDLE_PATH = os.path.join(CERT_DIR, "x509_crt_bundle.bin") @@ -49,7 +52,8 @@ def generate_bundle(): subprocess.check_call( [PYTHON, os.path.join(CERT_DIR, "gen_crt_bundle.py"), "--quiet", "--input", *certificate_inputs()], cwd=work_dir, - env=dict(os.environ, PYTHONWARNINGS="ignore"), + # The same import path as this process (pioarduino adds its own packages to it). + env=dict(os.environ, PYTHONWARNINGS="ignore", PYTHONPATH=os.pathsep.join(p for p in sys.path if p)), ) with open(os.path.join(work_dir, "x509_crt_bundle"), "rb") as f: bundle = f.read() diff --git a/include/README b/include/README deleted file mode 100644 index 49819c0..0000000 --- a/include/README +++ /dev/null @@ -1,37 +0,0 @@ - -This directory is intended for project header files. - -A header file is a file containing C declarations and macro definitions -to be shared between several project source files. You request the use of a -header file in your project source file (C, C++, etc) located in `src` folder -by including it, with the C preprocessing directive `#include'. - -```src/main.c - -#include "header.h" - -int main (void) -{ - ... -} -``` - -Including a header file produces the same results as copying the header file -into each source file that needs it. Such copying would be time-consuming -and error-prone. With a header file, the related declarations appear -in only one place. If they need to be changed, they can be changed in one -place, and programs that include the header file will automatically use the -new version when next recompiled. The header file eliminates the labor of -finding and changing all the copies as well as the risk that a failure to -find one copy will result in inconsistencies within a program. - -In C, the convention is to give header files names that end with `.h'. - -Read more about using header files in official GCC documentation: - -* Include Syntax -* Include Operation -* Once-Only Headers -* Computed Includes - -https://gcc.gnu.org/onlinedocs/cpp/Header-Files.html diff --git a/installer/.editorconfig b/installer/.editorconfig deleted file mode 100644 index 3dce414..0000000 --- a/installer/.editorconfig +++ /dev/null @@ -1,9 +0,0 @@ -root = true - -[*] -charset = utf-8 -indent_style = space -indent_size = 2 -end_of_line = lf -insert_final_newline = true -trim_trailing_whitespace = true \ No newline at end of file diff --git a/installer/.eslintignore b/installer/.eslintignore deleted file mode 100644 index a6f34fe..0000000 --- a/installer/.eslintignore +++ /dev/null @@ -1,4 +0,0 @@ -node_modules -dist -out -.gitignore diff --git a/installer/.gitignore b/installer/.gitignore deleted file mode 100644 index 42bd71b..0000000 --- a/installer/.gitignore +++ /dev/null @@ -1,5 +0,0 @@ -node_modules -dist -out -.DS_Store -*.log* diff --git a/installer/.npmrc b/installer/.npmrc deleted file mode 100644 index 2ced952..0000000 --- a/installer/.npmrc +++ /dev/null @@ -1,3 +0,0 @@ -electron_mirror=https://npmmirror.com/mirrors/electron/ -electron_builder_binaries_mirror=https://npmmirror.com/mirrors/electron-builder-binaries/ -shamefully-hoist=true diff --git a/installer/.vscode/extensions.json b/installer/.vscode/extensions.json deleted file mode 100644 index 940260d..0000000 --- a/installer/.vscode/extensions.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "recommendations": ["dbaeumer.vscode-eslint"] -} diff --git a/installer/.vscode/launch.json b/installer/.vscode/launch.json deleted file mode 100644 index e3ae63f..0000000 --- a/installer/.vscode/launch.json +++ /dev/null @@ -1,39 +0,0 @@ -{ - "version": "0.2.0", - "configurations": [ - { - "name": "Debug Main Process", - "type": "node", - "request": "launch", - "cwd": "${workspaceRoot}", - "runtimeExecutable": "${workspaceRoot}/in/node_modules/.bin/electron-vite", - "windows": { - "runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron-vite.cmd" - }, - "runtimeArgs": ["--sourcemap"], - "env": { - "REMOTE_DEBUGGING_PORT": "9222" - } - }, - { - "name": "Debug Renderer Process", - "port": 9222, - "request": "attach", - "type": "chrome", - "webRoot": "${workspaceFolder}/src/renderer", - "timeout": 60000, - "presentation": { - "hidden": true - } - } - ], - "compounds": [ - { - "name": "Debug All", - "configurations": ["Debug Main Process", "Debug Renderer Process"], - "presentation": { - "order": 1 - } - } - ] -} diff --git a/installer/.vscode/settings.json b/installer/.vscode/settings.json deleted file mode 100644 index 8ebbfcf..0000000 --- a/installer/.vscode/settings.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "[typescript]": { - "editor.defaultFormatter": "esbenp.prettier-vscode" - }, - "[javascript]": { - "editor.defaultFormatter": "esbenp.prettier-vscode" - }, - "[json]": { - "editor.defaultFormatter": "esbenp.prettier-vscode" - } -} diff --git a/installer/README.md b/installer/README.md deleted file mode 100644 index 7a556a5..0000000 --- a/installer/README.md +++ /dev/null @@ -1,76 +0,0 @@ -

electron-app

- -

An Electron application with Vue3 and TypeScript

- -

-electron-version -electron-vite-version -electron-builder-version -vite-version -vue-version -typescript-version -

- -

- -

- -## Features - -- 💡 Optimize asset handling -- 🚀 Fast HMR for renderer processes -- 🔥 Hot reloading for main process and preload scripts -- 🔌 Easy to debug -- 🔒 Compile to v8 bytecode to protect source code - -## Getting Started - -Read [documentation](https://electron-vite.org/) for more details. - -- [Configuring](https://electron-vite.org/config/) -- [Development](https://electron-vite.org/guide/dev.html) -- [Asset Handling](https://electron-vite.org/guide/assets.html) -- [HMR](https://electron-vite.org/guide/hmr.html) & [Hot Reloading](https://electron-vite.org/guide/hot-reloading.html) -- [Debugging](https://electron-vite.org/guide/debugging.html) -- [Source code protection](https://electron-vite.org/guide/source-code-protection.html) -- [Distribution](https://electron-vite.org/guide/distribution.html) -- [Troubleshooting](https://electron-vite.org/guide/troubleshooting.html) - -You can also use the [create-electron](https://github.com/alex8088/quick-start/tree/master/packages/create-electron) tool to scaffold your project for other frameworks (e.g. `React`, `Svelte` or `Solid`). - -## Recommended IDE Setup - -- [VSCode](https://code.visualstudio.com/) + [ESLint](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint) + [Prettier](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) + [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin) - -## Project Setup - -### Install - -```bash -$ npm install -``` - -### Development - -```bash -$ npm run dev -``` - -### Build - -```bash -# For windows -$ npm run build:win - -# For macOS -$ npm run build:mac - -# For Linux -$ npm run build:linux -``` - -## Examples - -- [electron-vite-bytecode-example](https://github.com/alex8088/electron-vite-bytecode-example), source code protection -- [electron-vite-decorator-example](https://github.com/alex8088/electron-vite-decorator-example), typescipt decorator -- [electron-vite-worker-example](https://github.com/alex8088/electron-vite-worker-example), worker and fork diff --git a/installer/build/electron-vite-vue-ts.png b/installer/build/electron-vite-vue-ts.png deleted file mode 100644 index a4de429..0000000 Binary files a/installer/build/electron-vite-vue-ts.png and /dev/null differ diff --git a/installer/build/entitlements.mac.plist b/installer/build/entitlements.mac.plist deleted file mode 100644 index 38c887b..0000000 --- a/installer/build/entitlements.mac.plist +++ /dev/null @@ -1,12 +0,0 @@ - - - - - com.apple.security.cs.allow-jit - - com.apple.security.cs.allow-unsigned-executable-memory - - com.apple.security.cs.allow-dyld-environment-variables - - - diff --git a/installer/build/icon.icns b/installer/build/icon.icns deleted file mode 100644 index 28644aa..0000000 Binary files a/installer/build/icon.icns and /dev/null differ diff --git a/installer/build/icon.ico b/installer/build/icon.ico deleted file mode 100644 index 72c391e..0000000 Binary files a/installer/build/icon.ico and /dev/null differ diff --git a/installer/build/icon.png b/installer/build/icon.png deleted file mode 100644 index cf9e8b2..0000000 Binary files a/installer/build/icon.png and /dev/null differ diff --git a/installer/bun.lock b/installer/bun.lock deleted file mode 100644 index c6da334..0000000 --- a/installer/bun.lock +++ /dev/null @@ -1,1117 +0,0 @@ -{ - "lockfileVersion": 1, - "workspaces": { - "": { - "name": "electron-app", - "dependencies": { - "@electron-toolkit/preload": "^3.0.0", - "@electron-toolkit/utils": "^3.0.0", - }, - "devDependencies": { - "@electron-toolkit/eslint-config": "^1.0.2", - "@electron-toolkit/eslint-config-ts": "^1.0.1", - "@electron-toolkit/tsconfig": "^1.0.1", - "@rushstack/eslint-patch": "^1.7.1", - "@types/node": "^18.19.9", - "@vitejs/plugin-vue": "^5.0.3", - "@vue/eslint-config-prettier": "^9.0.0", - "@vue/eslint-config-typescript": "^12.0.0", - "electron": "^28.2.0", - "electron-builder": "^24.9.1", - "electron-vite": "^2.0.0", - "eslint": "^8.56.0", - "eslint-plugin-vue": "^9.20.1", - "prettier": "^3.2.4", - "typescript": "^5.3.3", - "vite": "^5.0.12", - "vue": "^3.4.15", - "vue-tsc": "^1.8.27", - }, - }, - }, - "packages": { - "7zip-bin": ["7zip-bin@5.2.0", "", {}, "sha512-ukTPVhqG4jNzMro2qA9HSCSSVJN3aN7tlb+hfqYCt3ER0yWroeA2VR38MNrOHLQ/cVj+DaIMad0kFCtWWowh/A=="], - - "@aashutoshrathi/word-wrap": ["@aashutoshrathi/word-wrap@1.2.6", "", {}, "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA=="], - - "@ampproject/remapping": ["@ampproject/remapping@2.2.1", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.0", "@jridgewell/trace-mapping": "^0.3.9" } }, "sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg=="], - - "@babel/code-frame": ["@babel/code-frame@7.23.5", "", { "dependencies": { "@babel/highlight": "^7.23.4", "chalk": "^2.4.2" } }, "sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA=="], - - "@babel/compat-data": ["@babel/compat-data@7.23.5", "", {}, "sha512-uU27kfDRlhfKl+w1U6vp16IuvSLtjAxdArVXPa9BvLkrr7CYIsxH5adpHObeAGY/41+syctUWOZ140a2Rvkgjw=="], - - "@babel/core": ["@babel/core@7.23.9", "", { "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.23.5", "@babel/generator": "^7.23.6", "@babel/helper-compilation-targets": "^7.23.6", "@babel/helper-module-transforms": "^7.23.3", "@babel/helpers": "^7.23.9", "@babel/parser": "^7.23.9", "@babel/template": "^7.23.9", "@babel/traverse": "^7.23.9", "@babel/types": "^7.23.9", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-5q0175NOjddqpvvzU+kDiSOAk4PfdO6FvwCWoQ6RO7rTzEe8vlo+4HVfcnAREhD4npMs0e9uZypjTwzZPCf/cw=="], - - "@babel/generator": ["@babel/generator@7.23.6", "", { "dependencies": { "@babel/types": "^7.23.6", "@jridgewell/gen-mapping": "^0.3.2", "@jridgewell/trace-mapping": "^0.3.17", "jsesc": "^2.5.1" } }, "sha512-qrSfCYxYQB5owCmGLbl8XRpX1ytXlpueOb0N0UmQwA073KZxejgQTzAmJezxvpwQD9uGtK2shHdi55QT+MbjIw=="], - - "@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.23.6", "", { "dependencies": { "@babel/compat-data": "^7.23.5", "@babel/helper-validator-option": "^7.23.5", "browserslist": "^4.22.2", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-9JB548GZoQVmzrFgp8o7KxdgkTGm6xs9DW0o/Pim72UDjzr5ObUQ6ZzYPqA+g9OTS2bBQoctLJrky0RDCAWRgQ=="], - - "@babel/helper-environment-visitor": ["@babel/helper-environment-visitor@7.22.20", "", {}, "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA=="], - - "@babel/helper-function-name": ["@babel/helper-function-name@7.23.0", "", { "dependencies": { "@babel/template": "^7.22.15", "@babel/types": "^7.23.0" } }, "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw=="], - - "@babel/helper-hoist-variables": ["@babel/helper-hoist-variables@7.22.5", "", { "dependencies": { "@babel/types": "^7.22.5" } }, "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw=="], - - "@babel/helper-module-imports": ["@babel/helper-module-imports@7.22.15", "", { "dependencies": { "@babel/types": "^7.22.15" } }, "sha512-0pYVBnDKZO2fnSPCrgM/6WMc7eS20Fbok+0r88fp+YtWVLZrp4CkafFGIp+W0VKw4a22sgebPT99y+FDNMdP4w=="], - - "@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.23.3", "", { "dependencies": { "@babel/helper-environment-visitor": "^7.22.20", "@babel/helper-module-imports": "^7.22.15", "@babel/helper-simple-access": "^7.22.5", "@babel/helper-split-export-declaration": "^7.22.6", "@babel/helper-validator-identifier": "^7.22.20" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-7bBs4ED9OmswdfDzpz4MpWgSrV7FXlc3zIagvLFjS5H+Mk7Snr21vQ6QwrsoCGMfNC4e4LQPdoULEt4ykz0SRQ=="], - - "@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.22.5", "", {}, "sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg=="], - - "@babel/helper-simple-access": ["@babel/helper-simple-access@7.22.5", "", { "dependencies": { "@babel/types": "^7.22.5" } }, "sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w=="], - - "@babel/helper-split-export-declaration": ["@babel/helper-split-export-declaration@7.22.6", "", { "dependencies": { "@babel/types": "^7.22.5" } }, "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g=="], - - "@babel/helper-string-parser": ["@babel/helper-string-parser@7.23.4", "", {}, "sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ=="], - - "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.22.20", "", {}, "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A=="], - - "@babel/helper-validator-option": ["@babel/helper-validator-option@7.23.5", "", {}, "sha512-85ttAOMLsr53VgXkTbkx8oA6YTfT4q7/HzXSLEYmjcSTJPMPQtvq1BD79Byep5xMUYbGRzEpDsjUf3dyp54IKw=="], - - "@babel/helpers": ["@babel/helpers@7.23.9", "", { "dependencies": { "@babel/template": "^7.23.9", "@babel/traverse": "^7.23.9", "@babel/types": "^7.23.9" } }, "sha512-87ICKgU5t5SzOT7sBMfCOZQ2rHjRU+Pcb9BoILMYz600W6DkVRLFBPwQ18gwUVvggqXivaUakpnxWQGbpywbBQ=="], - - "@babel/highlight": ["@babel/highlight@7.23.4", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.22.20", "chalk": "^2.4.2", "js-tokens": "^4.0.0" } }, "sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A=="], - - "@babel/parser": ["@babel/parser@7.23.9", "", { "bin": { "parser": "bin/babel-parser.js" } }, "sha512-9tcKgqKbs3xGJ+NtKF2ndOBBLVwPjl1SHxPQkd36r3Dlirw3xWUeGaTbqr7uGZcTaxkVNwc+03SVP7aCdWrTlA=="], - - "@babel/plugin-transform-arrow-functions": ["@babel/plugin-transform-arrow-functions@7.23.3", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-NzQcQrzaQPkaEwoTm4Mhyl8jI1huEL/WWIEvudjTCMJ9aBZNpsJbMASx7EQECtQQPS/DcnFpo0FIh3LvEO9cxQ=="], - - "@babel/template": ["@babel/template@7.23.9", "", { "dependencies": { "@babel/code-frame": "^7.23.5", "@babel/parser": "^7.23.9", "@babel/types": "^7.23.9" } }, "sha512-+xrD2BWLpvHKNmX2QbpdpsBaWnRxahMwJjO+KZk2JOElj5nSmKezyS1B4u+QbHMTX69t4ukm6hh9lsYQ7GHCKA=="], - - "@babel/traverse": ["@babel/traverse@7.23.9", "", { "dependencies": { "@babel/code-frame": "^7.23.5", "@babel/generator": "^7.23.6", "@babel/helper-environment-visitor": "^7.22.20", "@babel/helper-function-name": "^7.23.0", "@babel/helper-hoist-variables": "^7.22.5", "@babel/helper-split-export-declaration": "^7.22.6", "@babel/parser": "^7.23.9", "@babel/types": "^7.23.9", "debug": "^4.3.1", "globals": "^11.1.0" } }, "sha512-I/4UJ9vs90OkBtY6iiiTORVMyIhJ4kAVmsKo9KFc8UOxMeUfi2hvtIBsET5u9GizXE6/GFSuKCTNfgCswuEjRg=="], - - "@babel/types": ["@babel/types@7.23.9", "", { "dependencies": { "@babel/helper-string-parser": "^7.23.4", "@babel/helper-validator-identifier": "^7.22.20", "to-fast-properties": "^2.0.0" } }, "sha512-dQjSq/7HaSjRM43FFGnv5keM2HsxpmyV1PfaSVm0nzzjwwTmjOe6J4bC8e3+pTEIgHaHj+1ZlLThRJ2auc/w1Q=="], - - "@develar/schema-utils": ["@develar/schema-utils@2.6.5", "", { "dependencies": { "ajv": "^6.12.0", "ajv-keywords": "^3.4.1" } }, "sha512-0cp4PsWQ/9avqTVMCtZ+GirikIA36ikvjtHweU4/j8yLtgObI0+JUPhYFScgwlteveGB1rt3Cm8UhN04XayDig=="], - - "@electron-toolkit/eslint-config": ["@electron-toolkit/eslint-config@1.0.2", "", { "peerDependencies": { "eslint": ">= 8.0.0" } }, "sha512-GJVuMsxBHfVARfmUoSTCHT0e/QfWlVbXcGk3tgoku0ad6tLjydbv2LpvKi02+Sy2WiEz9L9SkGSw090ukT/F0A=="], - - "@electron-toolkit/eslint-config-ts": ["@electron-toolkit/eslint-config-ts@1.0.1", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "^6.14.0", "@typescript-eslint/parser": "^6.14.0" }, "peerDependencies": { "eslint": ">=8.0.0", "typescript": "*" } }, "sha512-6RkoGE/W/WVlDozy/u6/lo5THNP5CCAPxdKpdQuYaopUfObs3o+JyxdlSb45imh+nL8nv875970EoyplntXaug=="], - - "@electron-toolkit/preload": ["@electron-toolkit/preload@3.0.0", "", { "peerDependencies": { "electron": ">=13.0.0" } }, "sha512-DTyvNGits43J1RiQGFN00/OxjjcTcozLWMjgQBANt97FFptEYiLVasq7kMC24nMwwoOpdDYY9CE6C+4wQJ55/w=="], - - "@electron-toolkit/tsconfig": ["@electron-toolkit/tsconfig@1.0.1", "", { "peerDependencies": { "@types/node": "*" } }, "sha512-M0Mol3odspvtCuheyujLNAW7bXq7KFNYVMRtpjFa4ZfES4MuklXBC7Nli/omvc+PRKlrklgAGx3l4VakjNo8jg=="], - - "@electron-toolkit/utils": ["@electron-toolkit/utils@3.0.0", "", { "peerDependencies": { "electron": ">=13.0.0" } }, "sha512-GaXHDhiT7KCvMJjXdp/QqpYinq69T/Pdl49Z1XLf8mKGf63dnsODMWyrmIjEQ0z/vG7dO8qF3fvmI6Eb2lUNZA=="], - - "@electron/asar": ["@electron/asar@3.2.8", "", { "dependencies": { "commander": "^5.0.0", "glob": "^7.1.6", "minimatch": "^3.0.4" }, "bin": { "asar": "bin/asar.js" } }, "sha512-cmskk5M06ewHMZAplSiF4AlME3IrnnZhKnWbtwKVLRkdJkKyUVjMLhDIiPIx/+6zQWVlKX/LtmK9xDme7540Sg=="], - - "@electron/get": ["@electron/get@2.0.3", "", { "dependencies": { "debug": "^4.1.1", "env-paths": "^2.2.0", "fs-extra": "^8.1.0", "got": "^11.8.5", "progress": "^2.0.3", "semver": "^6.2.0", "sumchecker": "^3.0.1" }, "optionalDependencies": { "global-agent": "^3.0.0" } }, "sha512-Qkzpg2s9GnVV2I2BjRksUi43U5e6+zaQMcjoJy0C+C5oxaKl+fmckGDQFtRpZpZV0NQekuZZ+tGz7EA9TVnQtQ=="], - - "@electron/notarize": ["@electron/notarize@2.1.0", "", { "dependencies": { "debug": "^4.1.1", "fs-extra": "^9.0.1", "promise-retry": "^2.0.1" } }, "sha512-Q02xem1D0sg4v437xHgmBLxI2iz/fc0D4K7fiVWHa/AnW8o7D751xyKNXgziA6HrTOme9ul1JfWN5ark8WH1xA=="], - - "@electron/osx-sign": ["@electron/osx-sign@1.0.5", "", { "dependencies": { "compare-version": "^0.1.2", "debug": "^4.3.4", "fs-extra": "^10.0.0", "isbinaryfile": "^4.0.8", "minimist": "^1.2.6", "plist": "^3.0.5" }, "bin": { "electron-osx-flat": "bin/electron-osx-flat.js", "electron-osx-sign": "bin/electron-osx-sign.js" } }, "sha512-k9ZzUQtamSoweGQDV2jILiRIHUu7lYlJ3c6IEmjv1hC17rclE+eb9U+f6UFlOOETo0JzY1HNlXy4YOlCvl+Lww=="], - - "@electron/universal": ["@electron/universal@1.4.1", "", { "dependencies": { "@electron/asar": "^3.2.1", "@malept/cross-spawn-promise": "^1.1.0", "debug": "^4.3.1", "dir-compare": "^3.0.0", "fs-extra": "^9.0.1", "minimatch": "^3.0.4", "plist": "^3.0.4" } }, "sha512-lE/U3UNw1YHuowNbTmKNs9UlS3En3cPgwM5MI+agIgr/B1hSze9NdOP0qn7boZaI9Lph8IDv3/24g9IxnJP7aQ=="], - - "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.19.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA=="], - - "@esbuild/android-arm": ["@esbuild/android-arm@0.19.12", "", { "os": "android", "cpu": "arm" }, "sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w=="], - - "@esbuild/android-arm64": ["@esbuild/android-arm64@0.19.12", "", { "os": "android", "cpu": "arm64" }, "sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA=="], - - "@esbuild/android-x64": ["@esbuild/android-x64@0.19.12", "", { "os": "android", "cpu": "x64" }, "sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew=="], - - "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.19.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g=="], - - "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.19.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A=="], - - "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.19.12", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA=="], - - "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.19.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg=="], - - "@esbuild/linux-arm": ["@esbuild/linux-arm@0.19.12", "", { "os": "linux", "cpu": "arm" }, "sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w=="], - - "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.19.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA=="], - - "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.19.12", "", { "os": "linux", "cpu": "ia32" }, "sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA=="], - - "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.19.12", "", { "os": "linux", "cpu": "none" }, "sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA=="], - - "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.19.12", "", { "os": "linux", "cpu": "none" }, "sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w=="], - - "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.19.12", "", { "os": "linux", "cpu": "ppc64" }, "sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg=="], - - "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.19.12", "", { "os": "linux", "cpu": "none" }, "sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg=="], - - "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.19.12", "", { "os": "linux", "cpu": "s390x" }, "sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg=="], - - "@esbuild/linux-x64": ["@esbuild/linux-x64@0.19.12", "", { "os": "linux", "cpu": "x64" }, "sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg=="], - - "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.19.12", "", { "os": "none", "cpu": "x64" }, "sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA=="], - - "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.19.12", "", { "os": "openbsd", "cpu": "x64" }, "sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw=="], - - "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.19.12", "", { "os": "sunos", "cpu": "x64" }, "sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA=="], - - "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.19.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A=="], - - "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.19.12", "", { "os": "win32", "cpu": "ia32" }, "sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ=="], - - "@esbuild/win32-x64": ["@esbuild/win32-x64@0.19.12", "", { "os": "win32", "cpu": "x64" }, "sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA=="], - - "@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.4.0", "", { "dependencies": { "eslint-visitor-keys": "^3.3.0" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA=="], - - "@eslint-community/regexpp": ["@eslint-community/regexpp@4.10.0", "", {}, "sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA=="], - - "@eslint/eslintrc": ["@eslint/eslintrc@2.1.4", "", { "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", "espree": "^9.6.0", "globals": "^13.19.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.0", "minimatch": "^3.1.2", "strip-json-comments": "^3.1.1" } }, "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ=="], - - "@eslint/js": ["@eslint/js@8.56.0", "", {}, "sha512-gMsVel9D7f2HLkBma9VbtzZRehRogVRfbr++f06nL2vnCGCNlzOD+/MUov/F4p8myyAHspEhVobgjpX64q5m6A=="], - - "@humanwhocodes/config-array": ["@humanwhocodes/config-array@0.11.14", "", { "dependencies": { "@humanwhocodes/object-schema": "^2.0.2", "debug": "^4.3.1", "minimatch": "^3.0.5" } }, "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg=="], - - "@humanwhocodes/module-importer": ["@humanwhocodes/module-importer@1.0.1", "", {}, "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA=="], - - "@humanwhocodes/object-schema": ["@humanwhocodes/object-schema@2.0.2", "", {}, "sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw=="], - - "@isaacs/cliui": ["@isaacs/cliui@8.0.2", "", { "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" } }, "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA=="], - - "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.3", "", { "dependencies": { "@jridgewell/set-array": "^1.0.1", "@jridgewell/sourcemap-codec": "^1.4.10", "@jridgewell/trace-mapping": "^0.3.9" } }, "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ=="], - - "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.1", "", {}, "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA=="], - - "@jridgewell/set-array": ["@jridgewell/set-array@1.1.2", "", {}, "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw=="], - - "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.4.15", "", {}, "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg=="], - - "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.22", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-Wf963MzWtA2sjrNt+g18IAln9lKnlRp+K2eH4jjIoF1wYeq3aMREpG09xhlhdzS0EjwU7qmUJYangWa+151vZw=="], - - "@malept/cross-spawn-promise": ["@malept/cross-spawn-promise@1.1.1", "", { "dependencies": { "cross-spawn": "^7.0.1" } }, "sha512-RTBGWL5FWQcg9orDOCcp4LvItNzUPcyEU9bwaeJX0rJ1IQxzucC48Y0/sQLp/g6t99IQgAlGIaesJS+gTn7tVQ=="], - - "@malept/flatpak-bundler": ["@malept/flatpak-bundler@0.4.0", "", { "dependencies": { "debug": "^4.1.1", "fs-extra": "^9.0.0", "lodash": "^4.17.15", "tmp-promise": "^3.0.2" } }, "sha512-9QOtNffcOF/c1seMCDnjckb3R9WHcG34tky+FHpNKKCW0wc/scYLwMtO+ptyGUfMW0/b/n4qRiALlaFHc9Oj7Q=="], - - "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="], - - "@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="], - - "@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="], - - "@pkgjs/parseargs": ["@pkgjs/parseargs@0.11.0", "", {}, "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg=="], - - "@pkgr/core": ["@pkgr/core@0.1.1", "", {}, "sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA=="], - - "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.9.6", "", { "os": "android", "cpu": "arm" }, "sha512-MVNXSSYN6QXOulbHpLMKYi60ppyO13W9my1qogeiAqtjb2yR4LSmfU2+POvDkLzhjYLXz9Rf9+9a3zFHW1Lecg=="], - - "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.9.6", "", { "os": "android", "cpu": "arm64" }, "sha512-T14aNLpqJ5wzKNf5jEDpv5zgyIqcpn1MlwCrUXLrwoADr2RkWA0vOWP4XxbO9aiO3dvMCQICZdKeDrFl7UMClw=="], - - "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.9.6", "", { "os": "darwin", "cpu": "arm64" }, "sha512-CqNNAyhRkTbo8VVZ5R85X73H3R5NX9ONnKbXuHisGWC0qRbTTxnF1U4V9NafzJbgGM0sHZpdO83pLPzq8uOZFw=="], - - "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.9.6", "", { "os": "darwin", "cpu": "x64" }, "sha512-zRDtdJuRvA1dc9Mp6BWYqAsU5oeLixdfUvkTHuiYOHwqYuQ4YgSmi6+/lPvSsqc/I0Omw3DdICx4Tfacdzmhog=="], - - "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.9.6", "", { "os": "linux", "cpu": "arm" }, "sha512-oNk8YXDDnNyG4qlNb6is1ojTOGL/tRhbbKeE/YuccItzerEZT68Z9gHrY3ROh7axDc974+zYAPxK5SH0j/G+QQ=="], - - "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.9.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-Z3O60yxPtuCYobrtzjo0wlmvDdx2qZfeAWTyfOjEDqd08kthDKexLpV97KfAeUXPosENKd8uyJMRDfFMxcYkDQ=="], - - "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.9.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-gpiG0qQJNdYEVad+1iAsGAbgAnZ8j07FapmnIAQgODKcOTjLEWM9sRb+MbQyVsYCnA0Im6M6QIq6ax7liws6eQ=="], - - "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.9.6", "", { "os": "linux", "cpu": "none" }, "sha512-+uCOcvVmFUYvVDr27aiyun9WgZk0tXe7ThuzoUTAukZJOwS5MrGbmSlNOhx1j80GdpqbOty05XqSl5w4dQvcOA=="], - - "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.9.6", "", { "os": "linux", "cpu": "x64" }, "sha512-HUNqM32dGzfBKuaDUBqFB7tP6VMN74eLZ33Q9Y1TBqRDn+qDonkAUyKWwF9BR9unV7QUzffLnz9GrnKvMqC/fw=="], - - "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.9.6", "", { "os": "linux", "cpu": "x64" }, "sha512-ch7M+9Tr5R4FK40FHQk8VnML0Szi2KRujUgHXd/HjuH9ifH72GUmw6lStZBo3c3GB82vHa0ZoUfjfcM7JiiMrQ=="], - - "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.9.6", "", { "os": "win32", "cpu": "arm64" }, "sha512-VD6qnR99dhmTQ1mJhIzXsRcTBvTjbfbGGwKAHcu+52cVl15AC/kplkhxzW/uT0Xl62Y/meBKDZvoJSJN+vTeGA=="], - - "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.9.6", "", { "os": "win32", "cpu": "ia32" }, "sha512-J9AFDq/xiRI58eR2NIDfyVmTYGyIZmRcvcAoJ48oDld/NTR8wyiPUu2X/v1navJ+N/FGg68LEbX3Ejd6l8B7MQ=="], - - "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.9.6", "", { "os": "win32", "cpu": "x64" }, "sha512-jqzNLhNDvIZOrt69Ce4UjGRpXJBzhUBzawMwnaDAwyHriki3XollsewxWzOzz+4yOFDkuJHtTsZFwMxhYJWmLQ=="], - - "@rushstack/eslint-patch": ["@rushstack/eslint-patch@1.7.2", "", {}, "sha512-RbhOOTCNoCrbfkRyoXODZp75MlpiHMgbE5MEBZAnnnLyQNgrigEj4p0lzsMDyc1zVsJDLrivB58tgg3emX0eEA=="], - - "@sindresorhus/is": ["@sindresorhus/is@4.6.0", "", {}, "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw=="], - - "@szmarczak/http-timer": ["@szmarczak/http-timer@4.0.6", "", { "dependencies": { "defer-to-connect": "^2.0.0" } }, "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w=="], - - "@tootallnate/once": ["@tootallnate/once@2.0.0", "", {}, "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A=="], - - "@types/cacheable-request": ["@types/cacheable-request@6.0.3", "", { "dependencies": { "@types/http-cache-semantics": "*", "@types/keyv": "^3.1.4", "@types/node": "*", "@types/responselike": "^1.0.0" } }, "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw=="], - - "@types/debug": ["@types/debug@4.1.12", "", { "dependencies": { "@types/ms": "*" } }, "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ=="], - - "@types/estree": ["@types/estree@1.0.5", "", {}, "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw=="], - - "@types/fs-extra": ["@types/fs-extra@9.0.13", "", { "dependencies": { "@types/node": "*" } }, "sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA=="], - - "@types/http-cache-semantics": ["@types/http-cache-semantics@4.0.4", "", {}, "sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA=="], - - "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="], - - "@types/keyv": ["@types/keyv@3.1.4", "", { "dependencies": { "@types/node": "*" } }, "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg=="], - - "@types/ms": ["@types/ms@0.7.34", "", {}, "sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g=="], - - "@types/node": ["@types/node@18.19.10", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-IZD8kAM02AW1HRDTPOlz3npFava678pr8Ie9Vp8uRhBROXAv8MXT2pCnGZZAKYdromsNQLHQcfWQ6EOatVLtqA=="], - - "@types/plist": ["@types/plist@3.0.5", "", { "dependencies": { "@types/node": "*", "xmlbuilder": ">=11.0.1" } }, "sha512-E6OCaRmAe4WDmWNsL/9RMqdkkzDCY1etutkflWk4c+AcjDU07Pcz1fQwTX0TQz+Pxqn9i4L1TU3UFpjnrcDgxA=="], - - "@types/responselike": ["@types/responselike@1.0.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw=="], - - "@types/semver": ["@types/semver@7.5.6", "", {}, "sha512-dn1l8LaMea/IjDoHNd9J52uBbInB796CDffS6VdIxvqYCPSG0V0DzHp76GpaWnlhg88uYyPbXCDIowa86ybd5A=="], - - "@types/verror": ["@types/verror@1.10.9", "", {}, "sha512-MLx9Z+9lGzwEuW16ubGeNkpBDE84RpB/NyGgg6z2BTpWzKkGU451cAY3UkUzZEp72RHF585oJ3V8JVNqIplcAQ=="], - - "@types/yauzl": ["@types/yauzl@2.10.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q=="], - - "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@6.19.1", "", { "dependencies": { "@eslint-community/regexpp": "^4.5.1", "@typescript-eslint/scope-manager": "6.19.1", "@typescript-eslint/type-utils": "6.19.1", "@typescript-eslint/utils": "6.19.1", "@typescript-eslint/visitor-keys": "6.19.1", "debug": "^4.3.4", "graphemer": "^1.4.0", "ignore": "^5.2.4", "natural-compare": "^1.4.0", "semver": "^7.5.4", "ts-api-utils": "^1.0.1" }, "peerDependencies": { "@typescript-eslint/parser": "^6.0.0 || ^6.0.0-alpha", "eslint": "^7.0.0 || ^8.0.0" } }, "sha512-roQScUGFruWod9CEyoV5KlCYrubC/fvG8/1zXuT0WTcxX87GnMMmnksMwSg99lo1xiKrBzw2icsJPMAw1OtKxg=="], - - "@typescript-eslint/parser": ["@typescript-eslint/parser@6.19.1", "", { "dependencies": { "@typescript-eslint/scope-manager": "6.19.1", "@typescript-eslint/types": "6.19.1", "@typescript-eslint/typescript-estree": "6.19.1", "@typescript-eslint/visitor-keys": "6.19.1", "debug": "^4.3.4" }, "peerDependencies": { "eslint": "^7.0.0 || ^8.0.0" } }, "sha512-WEfX22ziAh6pRE9jnbkkLGp/4RhTpffr2ZK5bJ18M8mIfA8A+k97U9ZyaXCEJRlmMHh7R9MJZWXp/r73DzINVQ=="], - - "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@6.19.1", "", { "dependencies": { "@typescript-eslint/types": "6.19.1", "@typescript-eslint/visitor-keys": "6.19.1" } }, "sha512-4CdXYjKf6/6aKNMSly/BP4iCSOpvMmqtDzRtqFyyAae3z5kkqEjKndR5vDHL8rSuMIIWP8u4Mw4VxLyxZW6D5w=="], - - "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@6.19.1", "", { "dependencies": { "@typescript-eslint/typescript-estree": "6.19.1", "@typescript-eslint/utils": "6.19.1", "debug": "^4.3.4", "ts-api-utils": "^1.0.1" }, "peerDependencies": { "eslint": "^7.0.0 || ^8.0.0" } }, "sha512-0vdyld3ecfxJuddDjACUvlAeYNrHP/pDeQk2pWBR2ESeEzQhg52DF53AbI9QCBkYE23lgkhLCZNkHn2hEXXYIg=="], - - "@typescript-eslint/types": ["@typescript-eslint/types@6.19.1", "", {}, "sha512-6+bk6FEtBhvfYvpHsDgAL3uo4BfvnTnoge5LrrCj2eJN8g3IJdLTD4B/jK3Q6vo4Ql/Hoip9I8aB6fF+6RfDqg=="], - - "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@6.19.1", "", { "dependencies": { "@typescript-eslint/types": "6.19.1", "@typescript-eslint/visitor-keys": "6.19.1", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", "minimatch": "9.0.3", "semver": "^7.5.4", "ts-api-utils": "^1.0.1" } }, "sha512-aFdAxuhzBFRWhy+H20nYu19+Km+gFfwNO4TEqyszkMcgBDYQjmPJ61erHxuT2ESJXhlhrO7I5EFIlZ+qGR8oVA=="], - - "@typescript-eslint/utils": ["@typescript-eslint/utils@6.19.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", "@types/json-schema": "^7.0.12", "@types/semver": "^7.5.0", "@typescript-eslint/scope-manager": "6.19.1", "@typescript-eslint/types": "6.19.1", "@typescript-eslint/typescript-estree": "6.19.1", "semver": "^7.5.4" }, "peerDependencies": { "eslint": "^7.0.0 || ^8.0.0" } }, "sha512-JvjfEZuP5WoMqwh9SPAPDSHSg9FBHHGhjPugSRxu5jMfjvBpq5/sGTD+9M9aQ5sh6iJ8AY/Kk/oUYVEMAPwi7w=="], - - "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@6.19.1", "", { "dependencies": { "@typescript-eslint/types": "6.19.1", "eslint-visitor-keys": "^3.4.1" } }, "sha512-gkdtIO+xSO/SmI0W68DBg4u1KElmIUo3vXzgHyGPs6cxgB0sa3TlptRAAE0hUY1hM6FcDKEv7aIwiTGm76cXfQ=="], - - "@ungap/structured-clone": ["@ungap/structured-clone@1.2.0", "", {}, "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ=="], - - "@vitejs/plugin-vue": ["@vitejs/plugin-vue@5.0.3", "", { "peerDependencies": { "vite": "^5.0.0", "vue": "^3.2.25" } }, "sha512-b8S5dVS40rgHdDrw+DQi/xOM9ed+kSRZzfm1T74bMmBDCd8XO87NKlFYInzCtwvtWwXZvo1QxE2OSspTATWrbA=="], - - "@volar/language-core": ["@volar/language-core@1.11.1", "", { "dependencies": { "@volar/source-map": "1.11.1" } }, "sha512-dOcNn3i9GgZAcJt43wuaEykSluAuOkQgzni1cuxLxTV0nJKanQztp7FxyswdRILaKH+P2XZMPRp2S4MV/pElCw=="], - - "@volar/source-map": ["@volar/source-map@1.11.1", "", { "dependencies": { "muggle-string": "^0.3.1" } }, "sha512-hJnOnwZ4+WT5iupLRnuzbULZ42L7BWWPMmruzwtLhJfpDVoZLjNBxHDi2sY2bgZXCKlpU5XcsMFoYrsQmPhfZg=="], - - "@volar/typescript": ["@volar/typescript@1.11.1", "", { "dependencies": { "@volar/language-core": "1.11.1", "path-browserify": "^1.0.1" } }, "sha512-iU+t2mas/4lYierSnoFOeRFQUhAEMgsFuQxoxvwn5EdQopw43j+J27a4lt9LMInx1gLJBC6qL14WYGlgymaSMQ=="], - - "@vue/compiler-core": ["@vue/compiler-core@3.4.15", "", { "dependencies": { "@babel/parser": "^7.23.6", "@vue/shared": "3.4.15", "entities": "^4.5.0", "estree-walker": "^2.0.2", "source-map-js": "^1.0.2" } }, "sha512-XcJQVOaxTKCnth1vCxEChteGuwG6wqnUHxAm1DO3gCz0+uXKaJNx8/digSz4dLALCy8n2lKq24jSUs8segoqIw=="], - - "@vue/compiler-dom": ["@vue/compiler-dom@3.4.15", "", { "dependencies": { "@vue/compiler-core": "3.4.15", "@vue/shared": "3.4.15" } }, "sha512-wox0aasVV74zoXyblarOM3AZQz/Z+OunYcIHe1OsGclCHt8RsRm04DObjefaI82u6XDzv+qGWZ24tIsRAIi5MQ=="], - - "@vue/compiler-sfc": ["@vue/compiler-sfc@3.4.15", "", { "dependencies": { "@babel/parser": "^7.23.6", "@vue/compiler-core": "3.4.15", "@vue/compiler-dom": "3.4.15", "@vue/compiler-ssr": "3.4.15", "@vue/shared": "3.4.15", "estree-walker": "^2.0.2", "magic-string": "^0.30.5", "postcss": "^8.4.33", "source-map-js": "^1.0.2" } }, "sha512-LCn5M6QpkpFsh3GQvs2mJUOAlBQcCco8D60Bcqmf3O3w5a+KWS5GvYbrrJBkgvL1BDnTp+e8q0lXCLgHhKguBA=="], - - "@vue/compiler-ssr": ["@vue/compiler-ssr@3.4.15", "", { "dependencies": { "@vue/compiler-dom": "3.4.15", "@vue/shared": "3.4.15" } }, "sha512-1jdeQyiGznr8gjFDadVmOJqZiLNSsMa5ZgqavkPZ8O2wjHv0tVuAEsw5hTdUoUW4232vpBbL/wJhzVW/JwY1Uw=="], - - "@vue/eslint-config-prettier": ["@vue/eslint-config-prettier@9.0.0", "", { "dependencies": { "eslint-config-prettier": "^9.0.0", "eslint-plugin-prettier": "^5.0.0" }, "peerDependencies": { "eslint": ">= 8.0.0", "prettier": ">= 3.0.0" } }, "sha512-z1ZIAAUS9pKzo/ANEfd2sO+v2IUalz7cM/cTLOZ7vRFOPk5/xuRKQteOu1DErFLAh/lYGXMVZ0IfYKlyInuDVg=="], - - "@vue/eslint-config-typescript": ["@vue/eslint-config-typescript@12.0.0", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "^6.7.0", "@typescript-eslint/parser": "^6.7.0", "vue-eslint-parser": "^9.3.1" }, "peerDependencies": { "eslint": "^6.2.0 || ^7.0.0 || ^8.0.0", "eslint-plugin-vue": "^9.0.0", "typescript": "*" } }, "sha512-StxLFet2Qe97T8+7L8pGlhYBBr8Eg05LPuTDVopQV6il+SK6qqom59BA/rcFipUef2jD8P2X44Vd8tMFytfvlg=="], - - "@vue/language-core": ["@vue/language-core@1.8.27", "", { "dependencies": { "@volar/language-core": "~1.11.1", "@volar/source-map": "~1.11.1", "@vue/compiler-dom": "^3.3.0", "@vue/shared": "^3.3.0", "computeds": "^0.0.1", "minimatch": "^9.0.3", "muggle-string": "^0.3.1", "path-browserify": "^1.0.1", "vue-template-compiler": "^2.7.14" }, "peerDependencies": { "typescript": "*" } }, "sha512-L8Kc27VdQserNaCUNiSFdDl9LWT24ly8Hpwf1ECy3aFb9m6bDhBGQYOujDm21N7EW3moKIOKEanQwe1q5BK+mA=="], - - "@vue/reactivity": ["@vue/reactivity@3.4.15", "", { "dependencies": { "@vue/shared": "3.4.15" } }, "sha512-55yJh2bsff20K5O84MxSvXKPHHt17I2EomHznvFiJCAZpJTNW8IuLj1xZWMLELRhBK3kkFV/1ErZGHJfah7i7w=="], - - "@vue/runtime-core": ["@vue/runtime-core@3.4.15", "", { "dependencies": { "@vue/reactivity": "3.4.15", "@vue/shared": "3.4.15" } }, "sha512-6E3by5m6v1AkW0McCeAyhHTw+3y17YCOKG0U0HDKDscV4Hs0kgNT5G+GCHak16jKgcCDHpI9xe5NKb8sdLCLdw=="], - - "@vue/runtime-dom": ["@vue/runtime-dom@3.4.15", "", { "dependencies": { "@vue/runtime-core": "3.4.15", "@vue/shared": "3.4.15", "csstype": "^3.1.3" } }, "sha512-EVW8D6vfFVq3V/yDKNPBFkZKGMFSvZrUQmx196o/v2tHKdwWdiZjYUBS+0Ez3+ohRyF8Njwy/6FH5gYJ75liUw=="], - - "@vue/server-renderer": ["@vue/server-renderer@3.4.15", "", { "dependencies": { "@vue/compiler-ssr": "3.4.15", "@vue/shared": "3.4.15" }, "peerDependencies": { "vue": "3.4.15" } }, "sha512-3HYzaidu9cHjrT+qGUuDhFYvF/j643bHC6uUN9BgM11DVy+pM6ATsG6uPBLnkwOgs7BpJABReLmpL3ZPAsUaqw=="], - - "@vue/shared": ["@vue/shared@3.4.15", "", {}, "sha512-KzfPTxVaWfB+eGcGdbSf4CWdaXcGDqckoeXUh7SB3fZdEtzPCK2Vq9B/lRRL3yutax/LWITz+SwvgyOxz5V75g=="], - - "@xmldom/xmldom": ["@xmldom/xmldom@0.8.10", "", {}, "sha512-2WALfTl4xo2SkGCYRt6rDTFfk9R1czmBvUQy12gK2KuRKIpWEhcbbzy8EZXtz/jkRqHX8bFEc6FC1HjX4TUWYw=="], - - "acorn": ["acorn@8.11.3", "", { "bin": "bin/acorn" }, "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg=="], - - "acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="], - - "agent-base": ["agent-base@6.0.2", "", { "dependencies": { "debug": "4" } }, "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ=="], - - "ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="], - - "ajv-keywords": ["ajv-keywords@3.5.2", "", { "peerDependencies": { "ajv": "^6.9.1" } }, "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ=="], - - "ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], - - "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], - - "app-builder-bin": ["app-builder-bin@4.0.0", "", {}, "sha512-xwdG0FJPQMe0M0UA4Tz0zEB8rBJTRA5a476ZawAqiBkMv16GRK5xpXThOjMaEOFnZ6zabejjG4J3da0SXG63KA=="], - - "app-builder-lib": ["app-builder-lib@24.9.1", "", { "dependencies": { "7zip-bin": "~5.2.0", "@develar/schema-utils": "~2.6.5", "@electron/notarize": "2.1.0", "@electron/osx-sign": "1.0.5", "@electron/universal": "1.4.1", "@malept/flatpak-bundler": "^0.4.0", "@types/fs-extra": "9.0.13", "async-exit-hook": "^2.0.1", "bluebird-lst": "^1.0.9", "builder-util": "24.8.1", "builder-util-runtime": "9.2.3", "chromium-pickle-js": "^0.2.0", "debug": "^4.3.4", "ejs": "^3.1.8", "electron-publish": "24.8.1", "form-data": "^4.0.0", "fs-extra": "^10.1.0", "hosted-git-info": "^4.1.0", "is-ci": "^3.0.0", "isbinaryfile": "^5.0.0", "js-yaml": "^4.1.0", "lazy-val": "^1.0.5", "minimatch": "^5.1.1", "read-config-file": "6.3.2", "sanitize-filename": "^1.6.3", "semver": "^7.3.8", "tar": "^6.1.12", "temp-file": "^3.4.0" } }, "sha512-Q1nYxZcio4r+W72cnIRVYofEAyjBd3mG47o+zms8HlD51zWtA/YxJb01Jei5F+jkWhge/PTQK+uldsPh6d0/4g=="], - - "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], - - "array-union": ["array-union@2.1.0", "", {}, "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw=="], - - "assert-plus": ["assert-plus@1.0.0", "", {}, "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw=="], - - "astral-regex": ["astral-regex@2.0.0", "", {}, "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ=="], - - "async": ["async@3.2.5", "", {}, "sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg=="], - - "async-exit-hook": ["async-exit-hook@2.0.1", "", {}, "sha512-NW2cX8m1Q7KPA7a5M2ULQeZ2wR5qI5PAbw5L0UOMxdioVk9PMZ0h1TmyZEkPYrCvYjDlFICusOu1dlEKAAeXBw=="], - - "asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="], - - "at-least-node": ["at-least-node@1.0.0", "", {}, "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg=="], - - "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], - - "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], - - "bluebird": ["bluebird@3.7.2", "", {}, "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg=="], - - "bluebird-lst": ["bluebird-lst@1.0.9", "", { "dependencies": { "bluebird": "^3.5.5" } }, "sha512-7B1Rtx82hjnSD4PGLAjVWeYH3tHAcVUmChh85a3lltKQm6FresXh9ErQo6oAv6CqxttczC3/kEg8SY5NluPuUw=="], - - "boolbase": ["boolbase@1.0.0", "", {}, "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww=="], - - "boolean": ["boolean@3.2.0", "", {}, "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw=="], - - "brace-expansion": ["brace-expansion@1.1.11", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA=="], - - "braces": ["braces@3.0.2", "", { "dependencies": { "fill-range": "^7.0.1" } }, "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A=="], - - "browserslist": ["browserslist@4.22.3", "", { "dependencies": { "caniuse-lite": "^1.0.30001580", "electron-to-chromium": "^1.4.648", "node-releases": "^2.0.14", "update-browserslist-db": "^1.0.13" }, "bin": "cli.js" }, "sha512-UAp55yfwNv0klWNapjs/ktHoguxuQNGnOzxYmfnXIS+8AsRDZkSDxg7R1AX3GKzn078SBI5dzwzj/Yx0Or0e3A=="], - - "buffer": ["buffer@5.7.1", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="], - - "buffer-crc32": ["buffer-crc32@0.2.13", "", {}, "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ=="], - - "buffer-equal": ["buffer-equal@1.0.1", "", {}, "sha512-QoV3ptgEaQpvVwbXdSO39iqPQTCxSF7A5U99AxbHYqUdCizL/lH2Z0A2y6nbZucxMEOtNyZfG2s6gsVugGpKkg=="], - - "buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="], - - "builder-util": ["builder-util@24.8.1", "", { "dependencies": { "7zip-bin": "~5.2.0", "@types/debug": "^4.1.6", "app-builder-bin": "4.0.0", "bluebird-lst": "^1.0.9", "builder-util-runtime": "9.2.3", "chalk": "^4.1.2", "cross-spawn": "^7.0.3", "debug": "^4.3.4", "fs-extra": "^10.1.0", "http-proxy-agent": "^5.0.0", "https-proxy-agent": "^5.0.1", "is-ci": "^3.0.0", "js-yaml": "^4.1.0", "source-map-support": "^0.5.19", "stat-mode": "^1.0.0", "temp-file": "^3.4.0" } }, "sha512-ibmQ4BnnqCnJTNrdmdNlnhF48kfqhNzSeqFMXHLIl+o9/yhn6QfOaVrloZ9YUu3m0k3rexvlT5wcki6LWpjTZw=="], - - "builder-util-runtime": ["builder-util-runtime@9.2.3", "", { "dependencies": { "debug": "^4.3.4", "sax": "^1.2.4" } }, "sha512-FGhkqXdFFZ5dNC4C+yuQB9ak311rpGAw+/ASz8ZdxwODCv1GGMWgLDeofRkdi0F3VCHQEWy/aXcJQozx2nOPiw=="], - - "cac": ["cac@6.7.14", "", {}, "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ=="], - - "cacheable-lookup": ["cacheable-lookup@5.0.4", "", {}, "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA=="], - - "cacheable-request": ["cacheable-request@7.0.4", "", { "dependencies": { "clone-response": "^1.0.2", "get-stream": "^5.1.0", "http-cache-semantics": "^4.0.0", "keyv": "^4.0.0", "lowercase-keys": "^2.0.0", "normalize-url": "^6.0.1", "responselike": "^2.0.0" } }, "sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg=="], - - "callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="], - - "caniuse-lite": ["caniuse-lite@1.0.30001580", "", {}, "sha512-mtj5ur2FFPZcCEpXFy8ADXbDACuNFXg6mxVDqp7tqooX6l3zwm+d8EPoeOSIFRDvHs8qu7/SLFOGniULkcH2iA=="], - - "chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], - - "chownr": ["chownr@2.0.0", "", {}, "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ=="], - - "chromium-pickle-js": ["chromium-pickle-js@0.2.0", "", {}, "sha512-1R5Fho+jBq0DDydt+/vHWj5KJNJCKdARKOCwZUen84I5BreWoLqRLANH1U87eJy1tiASPtMnGqJJq0ZsLoRPOw=="], - - "ci-info": ["ci-info@3.9.0", "", {}, "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ=="], - - "cli-truncate": ["cli-truncate@2.1.0", "", { "dependencies": { "slice-ansi": "^3.0.0", "string-width": "^4.2.0" } }, "sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg=="], - - "cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="], - - "clone-response": ["clone-response@1.0.3", "", { "dependencies": { "mimic-response": "^1.0.0" } }, "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA=="], - - "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], - - "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], - - "combined-stream": ["combined-stream@1.0.8", "", { "dependencies": { "delayed-stream": "~1.0.0" } }, "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg=="], - - "commander": ["commander@5.1.0", "", {}, "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg=="], - - "compare-version": ["compare-version@0.1.2", "", {}, "sha512-pJDh5/4wrEnXX/VWRZvruAGHkzKdr46z11OlTPN+VrATlWWhSKewNCJ1futCO5C7eJB3nPMFZA1LeYtcFboZ2A=="], - - "computeds": ["computeds@0.0.1", "", {}, "sha512-7CEBgcMjVmitjYo5q8JTJVra6X5mQ20uTThdK+0kR7UEaDrAWEQcRiBtWJzga4eRpP6afNwwLsX2SET2JhVB1Q=="], - - "concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="], - - "config-file-ts": ["config-file-ts@0.2.6", "", { "dependencies": { "glob": "^10.3.10", "typescript": "^5.3.3" } }, "sha512-6boGVaglwblBgJqGyxm4+xCmEGcWgnWHSWHY5jad58awQhB6gftq0G8HbzU39YqCIYHMLAiL1yjwiZ36m/CL8w=="], - - "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="], - - "core-util-is": ["core-util-is@1.0.2", "", {}, "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ=="], - - "crc": ["crc@3.8.0", "", { "dependencies": { "buffer": "^5.1.0" } }, "sha512-iX3mfgcTMIq3ZKLIsVFAbv7+Mc10kxabAGQb8HvjA1o3T1PIYprbakQ65d3I+2HGHt6nSKkM9PYjgoJO2KcFBQ=="], - - "cross-spawn": ["cross-spawn@7.0.3", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w=="], - - "cssesc": ["cssesc@3.0.0", "", { "bin": "bin/cssesc" }, "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg=="], - - "csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="], - - "de-indent": ["de-indent@1.0.2", "", {}, "sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg=="], - - "debug": ["debug@4.3.4", "", { "dependencies": { "ms": "2.1.2" } }, "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ=="], - - "decompress-response": ["decompress-response@6.0.0", "", { "dependencies": { "mimic-response": "^3.1.0" } }, "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ=="], - - "deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="], - - "defer-to-connect": ["defer-to-connect@2.0.1", "", {}, "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg=="], - - "define-data-property": ["define-data-property@1.1.1", "", { "dependencies": { "get-intrinsic": "^1.2.1", "gopd": "^1.0.1", "has-property-descriptors": "^1.0.0" } }, "sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ=="], - - "define-properties": ["define-properties@1.2.1", "", { "dependencies": { "define-data-property": "^1.0.1", "has-property-descriptors": "^1.0.0", "object-keys": "^1.1.1" } }, "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg=="], - - "delayed-stream": ["delayed-stream@1.0.0", "", {}, "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="], - - "detect-node": ["detect-node@2.1.0", "", {}, "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g=="], - - "dir-compare": ["dir-compare@3.3.0", "", { "dependencies": { "buffer-equal": "^1.0.0", "minimatch": "^3.0.4" } }, "sha512-J7/et3WlGUCxjdnD3HAAzQ6nsnc0WL6DD7WcwJb7c39iH1+AWfg+9OqzJNaI6PkBwBvm1mhZNL9iY/nRiZXlPg=="], - - "dir-glob": ["dir-glob@3.0.1", "", { "dependencies": { "path-type": "^4.0.0" } }, "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA=="], - - "dmg-builder": ["dmg-builder@24.9.1", "", { "dependencies": { "app-builder-lib": "24.9.1", "builder-util": "24.8.1", "builder-util-runtime": "9.2.3", "fs-extra": "^10.1.0", "iconv-lite": "^0.6.2", "js-yaml": "^4.1.0" }, "optionalDependencies": { "dmg-license": "^1.0.11" } }, "sha512-huC+O6hvHd24Ubj3cy2GMiGLe2xGFKN3klqVMLAdcbB6SWMd1yPSdZvV8W1O01ICzCCRlZDHiv4VrNUgnPUfbQ=="], - - "dmg-license": ["dmg-license@1.0.11", "", { "dependencies": { "@types/plist": "^3.0.1", "@types/verror": "^1.10.3", "ajv": "^6.10.0", "crc": "^3.8.0", "iconv-corefoundation": "^1.1.7", "plist": "^3.0.4", "smart-buffer": "^4.0.2", "verror": "^1.10.0" }, "os": "darwin", "bin": "bin/dmg-license.js" }, "sha512-ZdzmqwKmECOWJpqefloC5OJy1+WZBBse5+MR88z9g9Zn4VY+WYUkAyojmhzJckH5YbbZGcYIuGAkY5/Ys5OM2Q=="], - - "doctrine": ["doctrine@3.0.0", "", { "dependencies": { "esutils": "^2.0.2" } }, "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w=="], - - "dotenv": ["dotenv@9.0.2", "", {}, "sha512-I9OvvrHp4pIARv4+x9iuewrWycX6CcZtoAu1XrzPxc5UygMJXJZYmBsynku8IkrJwgypE5DGNjDPmPRhDCptUg=="], - - "dotenv-expand": ["dotenv-expand@5.1.0", "", {}, "sha512-YXQl1DSa4/PQyRfgrv6aoNjhasp/p4qs9FjJ4q4cQk+8m4r6k4ZSiEyytKG8f8W9gi8WsQtIObNmKd+tMzNTmA=="], - - "eastasianwidth": ["eastasianwidth@0.2.0", "", {}, "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="], - - "ejs": ["ejs@3.1.9", "", { "dependencies": { "jake": "^10.8.5" }, "bin": "bin/cli.js" }, "sha512-rC+QVNMJWv+MtPgkt0y+0rVEIdbtxVADApW9JXrUVlzHetgcyczP/E7DJmWJ4fJCZF2cPcBk0laWO9ZHMG3DmQ=="], - - "electron": ["electron@28.2.0", "", { "dependencies": { "@electron/get": "^2.0.0", "@types/node": "^18.11.18", "extract-zip": "^2.0.1" }, "bin": "cli.js" }, "sha512-22SylXQQ9IHtwLw4D+Z4Si7OUpeDtpHfJVTjy3yv53iLg5zJKKPOCWT4ZwgYGHQZ0eldyBrYBHF/P9FPd2CcVQ=="], - - "electron-builder": ["electron-builder@24.9.1", "", { "dependencies": { "app-builder-lib": "24.9.1", "builder-util": "24.8.1", "builder-util-runtime": "9.2.3", "chalk": "^4.1.2", "dmg-builder": "24.9.1", "fs-extra": "^10.1.0", "is-ci": "^3.0.0", "lazy-val": "^1.0.5", "read-config-file": "6.3.2", "simple-update-notifier": "2.0.0", "yargs": "^17.6.2" }, "bin": { "electron-builder": "cli.js", "install-app-deps": "install-app-deps.js" } }, "sha512-v7BuakDuY6sKMUYM8mfQGrwyjBpZ/ObaqnenU0H+igEL10nc6ht049rsCw2HghRBdEwJxGIBuzs3jbEhNaMDmg=="], - - "electron-publish": ["electron-publish@24.8.1", "", { "dependencies": { "@types/fs-extra": "^9.0.11", "builder-util": "24.8.1", "builder-util-runtime": "9.2.3", "chalk": "^4.1.2", "fs-extra": "^10.1.0", "lazy-val": "^1.0.5", "mime": "^2.5.2" } }, "sha512-IFNXkdxMVzUdweoLJNXSupXkqnvgbrn3J4vognuOY06LaS/m0xvfFYIf+o1CM8if6DuWYWoQFKPcWZt/FUjZPw=="], - - "electron-to-chromium": ["electron-to-chromium@1.4.648", "", {}, "sha512-EmFMarXeqJp9cUKu/QEciEApn0S/xRcpZWuAm32U7NgoZCimjsilKXHRO9saeEW55eHZagIDg6XTUOv32w9pjg=="], - - "electron-vite": ["electron-vite@2.0.0", "", { "dependencies": { "@babel/core": "^7.23.5", "@babel/plugin-transform-arrow-functions": "^7.23.3", "cac": "^6.7.14", "esbuild": "^0.19.8", "magic-string": "^0.30.5", "picocolors": "^1.0.0" }, "peerDependencies": { "@swc/core": "^1.0.0", "vite": "^4.0.0 || ^5.0.0" }, "optionalPeers": ["@swc/core"], "bin": "bin/electron-vite.js" }, "sha512-EQiuPVSwJQRPGbZiVrCsCFMDVfNyNtSpDiyUV4fNwPKNadTjBfYMOudnLUpSeBTocKUZYf58SXSzSCAkNg0GTQ=="], - - "emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], - - "end-of-stream": ["end-of-stream@1.4.4", "", { "dependencies": { "once": "^1.4.0" } }, "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q=="], - - "entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="], - - "env-paths": ["env-paths@2.2.1", "", {}, "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A=="], - - "err-code": ["err-code@2.0.3", "", {}, "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA=="], - - "es6-error": ["es6-error@4.1.1", "", {}, "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg=="], - - "esbuild": ["esbuild@0.19.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.19.12", "@esbuild/android-arm": "0.19.12", "@esbuild/android-arm64": "0.19.12", "@esbuild/android-x64": "0.19.12", "@esbuild/darwin-arm64": "0.19.12", "@esbuild/darwin-x64": "0.19.12", "@esbuild/freebsd-arm64": "0.19.12", "@esbuild/freebsd-x64": "0.19.12", "@esbuild/linux-arm": "0.19.12", "@esbuild/linux-arm64": "0.19.12", "@esbuild/linux-ia32": "0.19.12", "@esbuild/linux-loong64": "0.19.12", "@esbuild/linux-mips64el": "0.19.12", "@esbuild/linux-ppc64": "0.19.12", "@esbuild/linux-riscv64": "0.19.12", "@esbuild/linux-s390x": "0.19.12", "@esbuild/linux-x64": "0.19.12", "@esbuild/netbsd-x64": "0.19.12", "@esbuild/openbsd-x64": "0.19.12", "@esbuild/sunos-x64": "0.19.12", "@esbuild/win32-arm64": "0.19.12", "@esbuild/win32-ia32": "0.19.12", "@esbuild/win32-x64": "0.19.12" }, "bin": "bin/esbuild" }, "sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg=="], - - "escalade": ["escalade@3.1.1", "", {}, "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw=="], - - "escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], - - "eslint": ["eslint@8.56.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", "@eslint/eslintrc": "^2.1.4", "@eslint/js": "8.56.0", "@humanwhocodes/config-array": "^0.11.13", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", "@ungap/structured-clone": "^1.2.0", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.2", "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", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^6.0.1", "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" }, "bin": "bin/eslint.js" }, "sha512-Go19xM6T9puCOWntie1/P997aXxFsOi37JIHRWI514Hc6ZnaHGKY9xFhrU65RT6CcBEzZoGG1e6Nq+DT04ZtZQ=="], - - "eslint-config-prettier": ["eslint-config-prettier@9.1.0", "", { "peerDependencies": { "eslint": ">=7.0.0" }, "bin": "bin/cli.js" }, "sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw=="], - - "eslint-plugin-prettier": ["eslint-plugin-prettier@5.1.3", "", { "dependencies": { "prettier-linter-helpers": "^1.0.0", "synckit": "^0.8.6" }, "peerDependencies": { "@types/eslint": ">=8.0.0", "eslint": ">=8.0.0", "eslint-config-prettier": "*", "prettier": ">=3.0.0" }, "optionalPeers": ["@types/eslint"] }, "sha512-C9GCVAs4Eq7ZC/XFQHITLiHJxQngdtraXaM+LoUFoFp/lHNl2Zn8f3WQbe9HvTBBQ9YnKFB0/2Ajdqwo5D1EAw=="], - - "eslint-plugin-vue": ["eslint-plugin-vue@9.20.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", "natural-compare": "^1.4.0", "nth-check": "^2.1.1", "postcss-selector-parser": "^6.0.13", "semver": "^7.5.4", "vue-eslint-parser": "^9.4.0", "xml-name-validator": "^4.0.0" }, "peerDependencies": { "eslint": "^6.2.0 || ^7.0.0 || ^8.0.0" } }, "sha512-GyCs8K3lkEvoyC1VV97GJhP1SvqsKCiWGHnbn0gVUYiUhaH2+nB+Dv1uekv1THFMPbBfYxukrzQdltw950k+LQ=="], - - "eslint-scope": ["eslint-scope@7.2.2", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg=="], - - "eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], - - "espree": ["espree@9.6.1", "", { "dependencies": { "acorn": "^8.9.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^3.4.1" } }, "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ=="], - - "esquery": ["esquery@1.5.0", "", { "dependencies": { "estraverse": "^5.1.0" } }, "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg=="], - - "esrecurse": ["esrecurse@4.3.0", "", { "dependencies": { "estraverse": "^5.2.0" } }, "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag=="], - - "estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="], - - "estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="], - - "esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="], - - "extract-zip": ["extract-zip@2.0.1", "", { "dependencies": { "debug": "^4.1.1", "get-stream": "^5.1.0", "yauzl": "^2.10.0" }, "optionalDependencies": { "@types/yauzl": "^2.9.1" }, "bin": "cli.js" }, "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg=="], - - "extsprintf": ["extsprintf@1.4.1", "", {}, "sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA=="], - - "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], - - "fast-diff": ["fast-diff@1.3.0", "", {}, "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw=="], - - "fast-glob": ["fast-glob@3.3.2", "", { "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" } }, "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow=="], - - "fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="], - - "fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="], - - "fastq": ["fastq@1.17.0", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-zGygtijUMT7jnk3h26kUms3BkSDp4IfIKjmnqI2tvx6nuBfiF1UqOxbnLfzdv+apBy+53oaImsKtMw/xYbW+1w=="], - - "fd-slicer": ["fd-slicer@1.1.0", "", { "dependencies": { "pend": "~1.2.0" } }, "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g=="], - - "file-entry-cache": ["file-entry-cache@6.0.1", "", { "dependencies": { "flat-cache": "^3.0.4" } }, "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg=="], - - "filelist": ["filelist@1.0.4", "", { "dependencies": { "minimatch": "^5.0.1" } }, "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q=="], - - "fill-range": ["fill-range@7.0.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ=="], - - "find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="], - - "flat-cache": ["flat-cache@3.2.0", "", { "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.3", "rimraf": "^3.0.2" } }, "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw=="], - - "flatted": ["flatted@3.2.9", "", {}, "sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ=="], - - "foreground-child": ["foreground-child@3.1.1", "", { "dependencies": { "cross-spawn": "^7.0.0", "signal-exit": "^4.0.1" } }, "sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg=="], - - "form-data": ["form-data@4.0.0", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "mime-types": "^2.1.12" } }, "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww=="], - - "fs-extra": ["fs-extra@10.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ=="], - - "fs-minipass": ["fs-minipass@2.1.0", "", { "dependencies": { "minipass": "^3.0.0" } }, "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg=="], - - "fs.realpath": ["fs.realpath@1.0.0", "", {}, "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="], - - "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], - - "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], - - "gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="], - - "get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="], - - "get-intrinsic": ["get-intrinsic@1.2.2", "", { "dependencies": { "function-bind": "^1.1.2", "has-proto": "^1.0.1", "has-symbols": "^1.0.3", "hasown": "^2.0.0" } }, "sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA=="], - - "get-stream": ["get-stream@5.2.0", "", { "dependencies": { "pump": "^3.0.0" } }, "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA=="], - - "glob": ["glob@10.3.10", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^2.3.5", "minimatch": "^9.0.1", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0", "path-scurry": "^1.10.1" }, "bin": "dist/esm/bin.mjs" }, "sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g=="], - - "glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="], - - "global-agent": ["global-agent@3.0.0", "", { "dependencies": { "boolean": "^3.0.1", "es6-error": "^4.1.1", "matcher": "^3.0.0", "roarr": "^2.15.3", "semver": "^7.3.2", "serialize-error": "^7.0.1" } }, "sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q=="], - - "globals": ["globals@13.24.0", "", { "dependencies": { "type-fest": "^0.20.2" } }, "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ=="], - - "globalthis": ["globalthis@1.0.3", "", { "dependencies": { "define-properties": "^1.1.3" } }, "sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA=="], - - "globby": ["globby@11.1.0", "", { "dependencies": { "array-union": "^2.1.0", "dir-glob": "^3.0.1", "fast-glob": "^3.2.9", "ignore": "^5.2.0", "merge2": "^1.4.1", "slash": "^3.0.0" } }, "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g=="], - - "gopd": ["gopd@1.0.1", "", { "dependencies": { "get-intrinsic": "^1.1.3" } }, "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA=="], - - "got": ["got@11.8.6", "", { "dependencies": { "@sindresorhus/is": "^4.0.0", "@szmarczak/http-timer": "^4.0.5", "@types/cacheable-request": "^6.0.1", "@types/responselike": "^1.0.0", "cacheable-lookup": "^5.0.3", "cacheable-request": "^7.0.2", "decompress-response": "^6.0.0", "http2-wrapper": "^1.0.0-beta.5.2", "lowercase-keys": "^2.0.0", "p-cancelable": "^2.0.0", "responselike": "^2.0.0" } }, "sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g=="], - - "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], - - "graphemer": ["graphemer@1.4.0", "", {}, "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag=="], - - "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], - - "has-property-descriptors": ["has-property-descriptors@1.0.1", "", { "dependencies": { "get-intrinsic": "^1.2.2" } }, "sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg=="], - - "has-proto": ["has-proto@1.0.1", "", {}, "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg=="], - - "has-symbols": ["has-symbols@1.0.3", "", {}, "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A=="], - - "hasown": ["hasown@2.0.0", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA=="], - - "he": ["he@1.2.0", "", { "bin": "bin/he" }, "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw=="], - - "hosted-git-info": ["hosted-git-info@4.1.0", "", { "dependencies": { "lru-cache": "^6.0.0" } }, "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA=="], - - "http-cache-semantics": ["http-cache-semantics@4.1.1", "", {}, "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ=="], - - "http-proxy-agent": ["http-proxy-agent@5.0.0", "", { "dependencies": { "@tootallnate/once": "2", "agent-base": "6", "debug": "4" } }, "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w=="], - - "http2-wrapper": ["http2-wrapper@1.0.3", "", { "dependencies": { "quick-lru": "^5.1.1", "resolve-alpn": "^1.0.0" } }, "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg=="], - - "https-proxy-agent": ["https-proxy-agent@5.0.1", "", { "dependencies": { "agent-base": "6", "debug": "4" } }, "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA=="], - - "iconv-corefoundation": ["iconv-corefoundation@1.1.7", "", { "dependencies": { "cli-truncate": "^2.1.0", "node-addon-api": "^1.6.3" }, "os": "darwin" }, "sha512-T10qvkw0zz4wnm560lOEg0PovVqUXuOFhhHAkixw8/sycy7TJt7v/RrkEKEQnAw2viPSJu6iAkErxnzR0g8PpQ=="], - - "iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="], - - "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], - - "ignore": ["ignore@5.3.0", "", {}, "sha512-g7dmpshy+gD7mh88OC9NwSGTKoc3kyLAZQRU1mt53Aw/vnvfXnbC+F/7F7QoYVKbV+KNvJx8wArewKy1vXMtlg=="], - - "import-fresh": ["import-fresh@3.3.0", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw=="], - - "imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="], - - "inflight": ["inflight@1.0.6", "", { "dependencies": { "once": "^1.3.0", "wrappy": "1" } }, "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA=="], - - "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], - - "is-ci": ["is-ci@3.0.1", "", { "dependencies": { "ci-info": "^3.2.0" }, "bin": "bin.js" }, "sha512-ZYvCgrefwqoQ6yTyYUbQu64HsITZ3NfKX1lzaEYdkTDcfKzzCI/wthRRYKkdjHKFVgNiXKAKm65Zo1pk2as/QQ=="], - - "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="], - - "is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], - - "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="], - - "is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="], - - "is-path-inside": ["is-path-inside@3.0.3", "", {}, "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ=="], - - "isbinaryfile": ["isbinaryfile@5.0.0", "", {}, "sha512-UDdnyGvMajJUWCkib7Cei/dvyJrrvo4FIrsvSFWdPpXSUorzXrDJ0S+X5Q4ZlasfPjca4yqCNNsjbCeiy8FFeg=="], - - "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], - - "jackspeak": ["jackspeak@2.3.6", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ=="], - - "jake": ["jake@10.8.7", "", { "dependencies": { "async": "^3.2.3", "chalk": "^4.0.2", "filelist": "^1.0.4", "minimatch": "^3.1.2" }, "bin": "bin/cli.js" }, "sha512-ZDi3aP+fG/LchyBzUM804VjddnwfSfsdeYkwt8NcbKRvo4rFkjhs456iLFn3k2ZUWvNe4i48WACDbza8fhq2+w=="], - - "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], - - "js-yaml": ["js-yaml@4.1.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": "bin/js-yaml.js" }, "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA=="], - - "jsesc": ["jsesc@2.5.2", "", { "bin": "bin/jsesc" }, "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA=="], - - "json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="], - - "json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], - - "json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="], - - "json-stringify-safe": ["json-stringify-safe@5.0.1", "", {}, "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA=="], - - "json5": ["json5@2.2.3", "", { "bin": "lib/cli.js" }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="], - - "jsonfile": ["jsonfile@6.1.0", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ=="], - - "keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="], - - "lazy-val": ["lazy-val@1.0.5", "", {}, "sha512-0/BnGCCfyUMkBpeDgWihanIAF9JmZhHBgUhEqzvf+adhNGLoP6TaiI5oF8oyb3I45P+PcnrqihSf01M0l0G5+Q=="], - - "levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="], - - "locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="], - - "lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="], - - "lodash.merge": ["lodash.merge@4.6.2", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="], - - "lowercase-keys": ["lowercase-keys@2.0.0", "", {}, "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA=="], - - "lru-cache": ["lru-cache@6.0.0", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA=="], - - "magic-string": ["magic-string@0.30.5", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.4.15" } }, "sha512-7xlpfBaQaP/T6Vh8MO/EqXSW5En6INHEvEXQiuff7Gku0PWjU3uf6w/j9o7O+SpB5fOAkrI5HeoNgwjEO0pFsA=="], - - "matcher": ["matcher@3.0.0", "", { "dependencies": { "escape-string-regexp": "^4.0.0" } }, "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng=="], - - "merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="], - - "micromatch": ["micromatch@4.0.5", "", { "dependencies": { "braces": "^3.0.2", "picomatch": "^2.3.1" } }, "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA=="], - - "mime": ["mime@2.6.0", "", { "bin": "cli.js" }, "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg=="], - - "mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], - - "mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], - - "mimic-response": ["mimic-response@3.1.0", "", {}, "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ=="], - - "minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], - - "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], - - "minipass": ["minipass@5.0.0", "", {}, "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ=="], - - "minizlib": ["minizlib@2.1.2", "", { "dependencies": { "minipass": "^3.0.0", "yallist": "^4.0.0" } }, "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg=="], - - "mkdirp": ["mkdirp@1.0.4", "", { "bin": "bin/cmd.js" }, "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw=="], - - "ms": ["ms@2.1.2", "", {}, "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="], - - "muggle-string": ["muggle-string@0.3.1", "", {}, "sha512-ckmWDJjphvd/FvZawgygcUeQCxzvohjFO5RxTjj4eq8kw359gFF3E1brjfI+viLMxss5JrHTDRHZvu2/tuy0Qg=="], - - "nanoid": ["nanoid@3.3.7", "", { "bin": "bin/nanoid.cjs" }, "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g=="], - - "natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="], - - "node-addon-api": ["node-addon-api@1.7.2", "", {}, "sha512-ibPK3iA+vaY1eEjESkQkM0BbCqFOaZMiXRTtdB0u7b4djtY6JnsjvPdUHVMg6xQt3B8fpTTWHI9A+ADjM9frzg=="], - - "node-releases": ["node-releases@2.0.14", "", {}, "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw=="], - - "normalize-url": ["normalize-url@6.1.0", "", {}, "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A=="], - - "nth-check": ["nth-check@2.1.1", "", { "dependencies": { "boolbase": "^1.0.0" } }, "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w=="], - - "object-keys": ["object-keys@1.1.1", "", {}, "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA=="], - - "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], - - "optionator": ["optionator@0.9.3", "", { "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" } }, "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg=="], - - "p-cancelable": ["p-cancelable@2.1.1", "", {}, "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg=="], - - "p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="], - - "p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="], - - "parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="], - - "path-browserify": ["path-browserify@1.0.1", "", {}, "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g=="], - - "path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="], - - "path-is-absolute": ["path-is-absolute@1.0.1", "", {}, "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg=="], - - "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], - - "path-scurry": ["path-scurry@1.10.1", "", { "dependencies": { "lru-cache": "^9.1.1 || ^10.0.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ=="], - - "path-type": ["path-type@4.0.0", "", {}, "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw=="], - - "pend": ["pend@1.2.0", "", {}, "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg=="], - - "picocolors": ["picocolors@1.0.0", "", {}, "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ=="], - - "picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], - - "plist": ["plist@3.1.0", "", { "dependencies": { "@xmldom/xmldom": "^0.8.8", "base64-js": "^1.5.1", "xmlbuilder": "^15.1.1" } }, "sha512-uysumyrvkUX0rX/dEVqt8gC3sTBzd4zoWfLeS29nb53imdaXVvLINYXTI2GNqzaMuvacNx4uJQ8+b3zXR0pkgQ=="], - - "postcss": ["postcss@8.4.33", "", { "dependencies": { "nanoid": "^3.3.7", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } }, "sha512-Kkpbhhdjw2qQs2O2DGX+8m5OVqEcbB9HRBvuYM9pgrjEFUg30A9LmXNlTAUj4S9kgtGyrMbTzVjH7E+s5Re2yg=="], - - "postcss-selector-parser": ["postcss-selector-parser@6.0.15", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-rEYkQOMUCEMhsKbK66tbEU9QVIxbhN18YiniAwA7XQYTVBqrBy+P2p5JcdqsHgKM2zWylp8d7J6eszocfds5Sw=="], - - "prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="], - - "prettier": ["prettier@3.2.4", "", { "bin": "bin/prettier.cjs" }, "sha512-FWu1oLHKCrtpO1ypU6J0SbK2d9Ckwysq6bHj/uaCP26DxrPpppCLQRGVuqAxSTvhF00AcvDRyYrLNW7ocBhFFQ=="], - - "prettier-linter-helpers": ["prettier-linter-helpers@1.0.0", "", { "dependencies": { "fast-diff": "^1.1.2" } }, "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w=="], - - "progress": ["progress@2.0.3", "", {}, "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA=="], - - "promise-retry": ["promise-retry@2.0.1", "", { "dependencies": { "err-code": "^2.0.2", "retry": "^0.12.0" } }, "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g=="], - - "pump": ["pump@3.0.0", "", { "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww=="], - - "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], - - "queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="], - - "quick-lru": ["quick-lru@5.1.1", "", {}, "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA=="], - - "read-config-file": ["read-config-file@6.3.2", "", { "dependencies": { "config-file-ts": "^0.2.4", "dotenv": "^9.0.2", "dotenv-expand": "^5.1.0", "js-yaml": "^4.1.0", "json5": "^2.2.0", "lazy-val": "^1.0.4" } }, "sha512-M80lpCjnE6Wt6zb98DoW8WHR09nzMSpu8XHtPkiTHrJ5Az9CybfeQhTJ8D7saeBHpGhLPIVyA8lcL6ZmdKwY6Q=="], - - "require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="], - - "resolve-alpn": ["resolve-alpn@1.2.1", "", {}, "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g=="], - - "resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="], - - "responselike": ["responselike@2.0.1", "", { "dependencies": { "lowercase-keys": "^2.0.0" } }, "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw=="], - - "retry": ["retry@0.12.0", "", {}, "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow=="], - - "reusify": ["reusify@1.0.4", "", {}, "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw=="], - - "rimraf": ["rimraf@3.0.2", "", { "dependencies": { "glob": "^7.1.3" }, "bin": "bin.js" }, "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA=="], - - "roarr": ["roarr@2.15.4", "", { "dependencies": { "boolean": "^3.0.1", "detect-node": "^2.0.4", "globalthis": "^1.0.1", "json-stringify-safe": "^5.0.1", "semver-compare": "^1.0.0", "sprintf-js": "^1.1.2" } }, "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A=="], - - "rollup": ["rollup@4.9.6", "", { "dependencies": { "@types/estree": "1.0.5" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.9.6", "@rollup/rollup-android-arm64": "4.9.6", "@rollup/rollup-darwin-arm64": "4.9.6", "@rollup/rollup-darwin-x64": "4.9.6", "@rollup/rollup-linux-arm-gnueabihf": "4.9.6", "@rollup/rollup-linux-arm64-gnu": "4.9.6", "@rollup/rollup-linux-arm64-musl": "4.9.6", "@rollup/rollup-linux-riscv64-gnu": "4.9.6", "@rollup/rollup-linux-x64-gnu": "4.9.6", "@rollup/rollup-linux-x64-musl": "4.9.6", "@rollup/rollup-win32-arm64-msvc": "4.9.6", "@rollup/rollup-win32-ia32-msvc": "4.9.6", "@rollup/rollup-win32-x64-msvc": "4.9.6", "fsevents": "~2.3.2" }, "bin": "dist/bin/rollup" }, "sha512-05lzkCS2uASX0CiLFybYfVkwNbKZG5NFQ6Go0VWyogFTXXbR039UVsegViTntkk4OglHBdF54ccApXRRuXRbsg=="], - - "run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="], - - "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], - - "sanitize-filename": ["sanitize-filename@1.6.3", "", { "dependencies": { "truncate-utf8-bytes": "^1.0.0" } }, "sha512-y/52Mcy7aw3gRm7IrcGDFx/bCk4AhRh2eI9luHOQM86nZsqwiRkkq2GekHXBBD+SmPidc8i2PqtYZl+pWJ8Oeg=="], - - "sax": ["sax@1.3.0", "", {}, "sha512-0s+oAmw9zLl1V1cS9BtZN7JAd0cW5e0QH4W3LWEK6a4LaLEA2OTpGYWDY+6XasBLtz6wkm3u1xRw95mRuJ59WA=="], - - "semver": ["semver@7.5.4", "", { "dependencies": { "lru-cache": "^6.0.0" }, "bin": "bin/semver.js" }, "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA=="], - - "semver-compare": ["semver-compare@1.0.0", "", {}, "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow=="], - - "serialize-error": ["serialize-error@7.0.1", "", { "dependencies": { "type-fest": "^0.13.1" } }, "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw=="], - - "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], - - "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], - - "signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], - - "simple-update-notifier": ["simple-update-notifier@2.0.0", "", { "dependencies": { "semver": "^7.5.3" } }, "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w=="], - - "slash": ["slash@3.0.0", "", {}, "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q=="], - - "slice-ansi": ["slice-ansi@3.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "astral-regex": "^2.0.0", "is-fullwidth-code-point": "^3.0.0" } }, "sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ=="], - - "smart-buffer": ["smart-buffer@4.2.0", "", {}, "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg=="], - - "source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], - - "source-map-js": ["source-map-js@1.0.2", "", {}, "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw=="], - - "source-map-support": ["source-map-support@0.5.21", "", { "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" } }, "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w=="], - - "sprintf-js": ["sprintf-js@1.1.3", "", {}, "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA=="], - - "stat-mode": ["stat-mode@1.0.0", "", {}, "sha512-jH9EhtKIjuXZ2cWxmXS8ZP80XyC3iasQxMDV8jzhNJpfDb7VbQLVW4Wvsxz9QZvzV+G4YoSfBUVKDOyxLzi/sg=="], - - "string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], - - "string-width-cjs": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], - - "strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - - "strip-ansi-cjs": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - - "strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="], - - "sumchecker": ["sumchecker@3.0.1", "", { "dependencies": { "debug": "^4.1.0" } }, "sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg=="], - - "supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], - - "synckit": ["synckit@0.8.8", "", { "dependencies": { "@pkgr/core": "^0.1.0", "tslib": "^2.6.2" } }, "sha512-HwOKAP7Wc5aRGYdKH+dw0PRRpbO841v2DENBtjnR5HFWoiNByAl7vrx3p0G/rCyYXQsrxqtX48TImFtPcIHSpQ=="], - - "tar": ["tar@6.2.0", "", { "dependencies": { "chownr": "^2.0.0", "fs-minipass": "^2.0.0", "minipass": "^5.0.0", "minizlib": "^2.1.1", "mkdirp": "^1.0.3", "yallist": "^4.0.0" } }, "sha512-/Wo7DcT0u5HUV486xg675HtjNd3BXZ6xDbzsCUZPt5iw8bTQ63bP0Raut3mvro9u+CUyq7YQd8Cx55fsZXxqLQ=="], - - "temp-file": ["temp-file@3.4.0", "", { "dependencies": { "async-exit-hook": "^2.0.1", "fs-extra": "^10.0.0" } }, "sha512-C5tjlC/HCtVUOi3KWVokd4vHVViOmGjtLwIh4MuzPo/nMYTV/p1urt3RnMz2IWXDdKEGJH3k5+KPxtqRsUYGtg=="], - - "text-table": ["text-table@0.2.0", "", {}, "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw=="], - - "tmp": ["tmp@0.2.1", "", { "dependencies": { "rimraf": "^3.0.0" } }, "sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ=="], - - "tmp-promise": ["tmp-promise@3.0.3", "", { "dependencies": { "tmp": "^0.2.0" } }, "sha512-RwM7MoPojPxsOBYnyd2hy0bxtIlVrihNs9pj5SUvY8Zz1sQcQG2tG1hSr8PDxfgEB8RNKDhqbIlroIarSNDNsQ=="], - - "to-fast-properties": ["to-fast-properties@2.0.0", "", {}, "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog=="], - - "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], - - "truncate-utf8-bytes": ["truncate-utf8-bytes@1.0.2", "", { "dependencies": { "utf8-byte-length": "^1.0.1" } }, "sha512-95Pu1QXQvruGEhv62XCMO3Mm90GscOCClvrIUwCM0PYOXK3kaF3l3sIHxx71ThJfcbM2O5Au6SO3AWCSEfW4mQ=="], - - "ts-api-utils": ["ts-api-utils@1.0.3", "", { "peerDependencies": { "typescript": ">=4.2.0" } }, "sha512-wNMeqtMz5NtwpT/UZGY5alT+VoKdSsOOP/kqHFcUW1P/VRhH2wJ48+DN2WwUliNbQ976ETwDL0Ifd2VVvgonvg=="], - - "tslib": ["tslib@2.6.2", "", {}, "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q=="], - - "type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="], - - "type-fest": ["type-fest@0.20.2", "", {}, "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ=="], - - "typescript": ["typescript@5.3.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw=="], - - "undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], - - "universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="], - - "update-browserslist-db": ["update-browserslist-db@1.0.13", "", { "dependencies": { "escalade": "^3.1.1", "picocolors": "^1.0.0" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": "cli.js" }, "sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg=="], - - "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="], - - "utf8-byte-length": ["utf8-byte-length@1.0.4", "", {}, "sha512-4+wkEYLBbWxqTahEsWrhxepcoVOJ+1z5PGIjPZxRkytcdSUaNjIjBM7Xn8E+pdSuV7SzvWovBFA54FO0JSoqhA=="], - - "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], - - "verror": ["verror@1.10.1", "", { "dependencies": { "assert-plus": "^1.0.0", "core-util-is": "1.0.2", "extsprintf": "^1.2.0" } }, "sha512-veufcmxri4e3XSrT0xwfUR7kguIkaxBeosDg00yDWhk49wdwkSUrvvsm7nc75e1PUyvIeZj6nS8VQRYz2/S4Xg=="], - - "vite": ["vite@5.0.12", "", { "dependencies": { "esbuild": "^0.19.3", "postcss": "^8.4.32", "rollup": "^4.2.0" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || >=20.0.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "stylus": "*", "sugarss": "*", "terser": "^5.4.0" }, "optionalPeers": ["less", "lightningcss", "sass", "stylus", "sugarss", "terser"], "bin": "bin/vite.js" }, "sha512-4hsnEkG3q0N4Tzf1+t6NdN9dg/L3BM+q8SWgbSPnJvrgH2kgdyzfVJwbR1ic69/4uMJJ/3dqDZZE5/WwqW8U1w=="], - - "vue": ["vue@3.4.15", "", { "dependencies": { "@vue/compiler-dom": "3.4.15", "@vue/compiler-sfc": "3.4.15", "@vue/runtime-dom": "3.4.15", "@vue/server-renderer": "3.4.15", "@vue/shared": "3.4.15" }, "peerDependencies": { "typescript": "*" } }, "sha512-jC0GH4KkWLWJOEQjOpkqU1bQsBwf4R1rsFtw5GQJbjHVKWDzO6P0nWWBTmjp1xSemAioDFj1jdaK1qa3DnMQoQ=="], - - "vue-eslint-parser": ["vue-eslint-parser@9.4.2", "", { "dependencies": { "debug": "^4.3.4", "eslint-scope": "^7.1.1", "eslint-visitor-keys": "^3.3.0", "espree": "^9.3.1", "esquery": "^1.4.0", "lodash": "^4.17.21", "semver": "^7.3.6" }, "peerDependencies": { "eslint": ">=6.0.0" } }, "sha512-Ry9oiGmCAK91HrKMtCrKFWmSFWvYkpGglCeFAIqDdr9zdXmMMpJOmUJS7WWsW7fX81h6mwHmUZCQQ1E0PkSwYQ=="], - - "vue-template-compiler": ["vue-template-compiler@2.7.16", "", { "dependencies": { "de-indent": "^1.0.2", "he": "^1.2.0" } }, "sha512-AYbUWAJHLGGQM7+cNTELw+KsOG9nl2CnSv467WobS5Cv9uk3wFcnr1Etsz2sEIHEZvw1U+o9mRlEO6QbZvUPGQ=="], - - "vue-tsc": ["vue-tsc@1.8.27", "", { "dependencies": { "@volar/typescript": "~1.11.1", "@vue/language-core": "1.8.27", "semver": "^7.5.4" }, "peerDependencies": { "typescript": "*" }, "bin": "bin/vue-tsc.js" }, "sha512-WesKCAZCRAbmmhuGl3+VrdWItEvfoFIPXOvUJkjULi+x+6G/Dy69yO3TBRJDr9eUlmsNAwVmxsNZxvHKzbkKdg=="], - - "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], - - "wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], - - "wrap-ansi-cjs": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], - - "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], - - "xml-name-validator": ["xml-name-validator@4.0.0", "", {}, "sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw=="], - - "xmlbuilder": ["xmlbuilder@15.1.1", "", {}, "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg=="], - - "y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="], - - "yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="], - - "yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="], - - "yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="], - - "yauzl": ["yauzl@2.10.0", "", { "dependencies": { "buffer-crc32": "~0.2.3", "fd-slicer": "~1.1.0" } }, "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g=="], - - "yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], - - "@babel/code-frame/chalk": ["chalk@2.4.2", "", { "dependencies": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", "supports-color": "^5.3.0" } }, "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ=="], - - "@babel/core/semver": ["semver@6.3.1", "", { "bin": "bin/semver.js" }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], - - "@babel/helper-compilation-targets/lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], - - "@babel/helper-compilation-targets/semver": ["semver@6.3.1", "", { "bin": "bin/semver.js" }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], - - "@babel/highlight/chalk": ["chalk@2.4.2", "", { "dependencies": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", "supports-color": "^5.3.0" } }, "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ=="], - - "@babel/traverse/globals": ["globals@11.12.0", "", {}, "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA=="], - - "@electron/asar/glob": ["glob@7.2.3", "", { "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" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], - - "@electron/asar/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], - - "@electron/get/fs-extra": ["fs-extra@8.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^4.0.0", "universalify": "^0.1.0" } }, "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g=="], - - "@electron/get/semver": ["semver@6.3.1", "", { "bin": "bin/semver.js" }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], - - "@electron/notarize/fs-extra": ["fs-extra@9.1.0", "", { "dependencies": { "at-least-node": "^1.0.0", "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ=="], - - "@electron/osx-sign/fs-extra": ["fs-extra@10.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ=="], - - "@electron/osx-sign/isbinaryfile": ["isbinaryfile@4.0.10", "", {}, "sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw=="], - - "@electron/universal/fs-extra": ["fs-extra@9.1.0", "", { "dependencies": { "at-least-node": "^1.0.0", "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ=="], - - "@electron/universal/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], - - "@eslint/eslintrc/globals": ["globals@13.24.0", "", { "dependencies": { "type-fest": "^0.20.2" } }, "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ=="], - - "@eslint/eslintrc/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], - - "@humanwhocodes/config-array/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], - - "@isaacs/cliui/string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="], - - "@isaacs/cliui/strip-ansi": ["strip-ansi@7.1.0", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ=="], - - "@isaacs/cliui/wrap-ansi": ["wrap-ansi@8.1.0", "", { "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", "strip-ansi": "^7.0.1" } }, "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ=="], - - "@malept/flatpak-bundler/fs-extra": ["fs-extra@9.1.0", "", { "dependencies": { "at-least-node": "^1.0.0", "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ=="], - - "@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.3", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg=="], - - "@vue/language-core/minimatch": ["minimatch@9.0.3", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg=="], - - "app-builder-lib/fs-extra": ["fs-extra@10.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ=="], - - "app-builder-lib/minimatch": ["minimatch@5.1.6", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g=="], - - "builder-util/fs-extra": ["fs-extra@10.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ=="], - - "clone-response/mimic-response": ["mimic-response@1.0.1", "", {}, "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ=="], - - "dir-compare/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], - - "dmg-builder/fs-extra": ["fs-extra@10.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ=="], - - "electron-publish/fs-extra": ["fs-extra@10.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ=="], - - "fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], - - "filelist/minimatch": ["minimatch@5.1.6", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g=="], - - "fs-minipass/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="], - - "glob/minimatch": ["minimatch@9.0.3", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg=="], - - "hosted-git-info/lru-cache": ["lru-cache@6.0.0", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA=="], - - "jake/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], - - "lru-cache/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="], - - "minizlib/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="], - - "minizlib/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="], - - "path-scurry/lru-cache": ["lru-cache@10.2.0", "", {}, "sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q=="], - - "rimraf/glob": ["glob@7.2.3", "", { "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" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], - - "serialize-error/type-fest": ["type-fest@0.13.1", "", {}, "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg=="], - - "temp-file/fs-extra": ["fs-extra@10.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ=="], - - "@babel/code-frame/chalk/ansi-styles": ["ansi-styles@3.2.1", "", { "dependencies": { "color-convert": "^1.9.0" } }, "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA=="], - - "@babel/code-frame/chalk/escape-string-regexp": ["escape-string-regexp@1.0.5", "", {}, "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg=="], - - "@babel/code-frame/chalk/supports-color": ["supports-color@5.5.0", "", { "dependencies": { "has-flag": "^3.0.0" } }, "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow=="], - - "@babel/helper-compilation-targets/lru-cache/yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], - - "@babel/highlight/chalk/ansi-styles": ["ansi-styles@3.2.1", "", { "dependencies": { "color-convert": "^1.9.0" } }, "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA=="], - - "@babel/highlight/chalk/escape-string-regexp": ["escape-string-regexp@1.0.5", "", {}, "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg=="], - - "@babel/highlight/chalk/supports-color": ["supports-color@5.5.0", "", { "dependencies": { "has-flag": "^3.0.0" } }, "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow=="], - - "@electron/asar/glob/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], - - "@electron/asar/minimatch/brace-expansion": ["brace-expansion@1.1.11", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA=="], - - "@electron/get/fs-extra/jsonfile": ["jsonfile@4.0.0", "", { "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg=="], - - "@electron/get/fs-extra/universalify": ["universalify@0.1.2", "", {}, "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg=="], - - "@electron/notarize/fs-extra/jsonfile": ["jsonfile@6.1.0", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ=="], - - "@electron/notarize/fs-extra/universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="], - - "@electron/osx-sign/fs-extra/jsonfile": ["jsonfile@6.1.0", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ=="], - - "@electron/osx-sign/fs-extra/universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="], - - "@electron/universal/fs-extra/jsonfile": ["jsonfile@6.1.0", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ=="], - - "@electron/universal/fs-extra/universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="], - - "@electron/universal/minimatch/brace-expansion": ["brace-expansion@1.1.11", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA=="], - - "@eslint/eslintrc/globals/type-fest": ["type-fest@0.20.2", "", {}, "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ=="], - - "@eslint/eslintrc/minimatch/brace-expansion": ["brace-expansion@1.1.11", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA=="], - - "@humanwhocodes/config-array/minimatch/brace-expansion": ["brace-expansion@1.1.11", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA=="], - - "@isaacs/cliui/string-width/emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="], - - "@isaacs/cliui/strip-ansi/ansi-regex": ["ansi-regex@6.0.1", "", {}, "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA=="], - - "@isaacs/cliui/wrap-ansi/ansi-styles": ["ansi-styles@6.2.1", "", {}, "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug=="], - - "@malept/flatpak-bundler/fs-extra/jsonfile": ["jsonfile@6.1.0", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ=="], - - "@malept/flatpak-bundler/fs-extra/universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="], - - "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="], - - "@vue/language-core/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="], - - "app-builder-lib/fs-extra/jsonfile": ["jsonfile@6.1.0", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ=="], - - "app-builder-lib/fs-extra/universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="], - - "app-builder-lib/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="], - - "builder-util/fs-extra/jsonfile": ["jsonfile@6.1.0", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ=="], - - "builder-util/fs-extra/universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="], - - "dir-compare/minimatch/brace-expansion": ["brace-expansion@1.1.11", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA=="], - - "dmg-builder/fs-extra/jsonfile": ["jsonfile@6.1.0", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ=="], - - "dmg-builder/fs-extra/universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="], - - "electron-publish/fs-extra/jsonfile": ["jsonfile@6.1.0", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ=="], - - "electron-publish/fs-extra/universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="], - - "filelist/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="], - - "fs-minipass/minipass/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="], - - "glob/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="], - - "hosted-git-info/lru-cache/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="], - - "jake/minimatch/brace-expansion": ["brace-expansion@1.1.11", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA=="], - - "rimraf/glob/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], - - "temp-file/fs-extra/jsonfile": ["jsonfile@6.1.0", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ=="], - - "temp-file/fs-extra/universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="], - - "@babel/code-frame/chalk/ansi-styles/color-convert": ["color-convert@1.9.3", "", { "dependencies": { "color-name": "1.1.3" } }, "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg=="], - - "@babel/code-frame/chalk/supports-color/has-flag": ["has-flag@3.0.0", "", {}, "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw=="], - - "@babel/highlight/chalk/ansi-styles/color-convert": ["color-convert@1.9.3", "", { "dependencies": { "color-name": "1.1.3" } }, "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg=="], - - "@babel/highlight/chalk/supports-color/has-flag": ["has-flag@3.0.0", "", {}, "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw=="], - - "@electron/asar/glob/minimatch/brace-expansion": ["brace-expansion@1.1.11", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA=="], - - "rimraf/glob/minimatch/brace-expansion": ["brace-expansion@1.1.11", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA=="], - - "@babel/code-frame/chalk/ansi-styles/color-convert/color-name": ["color-name@1.1.3", "", {}, "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="], - - "@babel/highlight/chalk/ansi-styles/color-convert/color-name": ["color-name@1.1.3", "", {}, "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="], - } -} diff --git a/installer/electron-builder.yml b/installer/electron-builder.yml deleted file mode 100644 index c2f8b04..0000000 --- a/installer/electron-builder.yml +++ /dev/null @@ -1,45 +0,0 @@ -appId: com.electron.app -productName: electron-app -directories: - buildResources: build -files: - - '!**/.vscode/*' - - '!src/*' - - '!electron.vite.config.{js,ts,mjs,cjs}' - - '!{.eslintignore,.eslintrc.cjs,.prettierignore,.prettierrc.yaml,dev-app-update.yml,CHANGELOG.md,README.md}' - - '!{.env,.env.*,.npmrc,pnpm-lock.yaml}' - - '!{tsconfig.json,tsconfig.node.json,tsconfig.web.json}' -asarUnpack: - - resources/** -win: - executableName: electron-app -nsis: - artifactName: ${name}-${version}-setup.${ext} - shortcutName: ${productName} - uninstallDisplayName: ${productName} - createDesktopShortcut: always -mac: - entitlementsInherit: build/entitlements.mac.plist - extendInfo: - - NSCameraUsageDescription: Application requests access to the device's camera. - - NSMicrophoneUsageDescription: Application requests access to the device's microphone. - - NSDocumentsFolderUsageDescription: Application requests access to the user's Documents folder. - - NSDownloadsFolderUsageDescription: Application requests access to the user's Downloads folder. - notarize: false -dmg: - artifactName: ${name}-${version}.${ext} -linux: - target: - - AppImage - - snap - - deb - maintainer: electronjs.org - category: Utility -appImage: - artifactName: ${name}-${version}.${ext} -npmRebuild: false -publish: - provider: generic - url: https://example.com/auto-updates -electronDownload: - mirror: https://npmmirror.com/mirrors/electron/ diff --git a/installer/electron.vite.config.ts b/installer/electron.vite.config.ts deleted file mode 100644 index 6381439..0000000 --- a/installer/electron.vite.config.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { resolve } from 'path' -import { defineConfig, externalizeDepsPlugin, bytecodePlugin } from 'electron-vite' -import vue from '@vitejs/plugin-vue' - -export default defineConfig({ - main: { - plugins: [externalizeDepsPlugin(), bytecodePlugin()] - }, - preload: { - plugins: [externalizeDepsPlugin(), bytecodePlugin()] - }, - renderer: { - resolve: { - alias: { - '@renderer': resolve('src/renderer/src') - } - }, - plugins: [vue()] - } -}) diff --git a/installer/package-lock.json b/installer/package-lock.json deleted file mode 100644 index 8e555e7..0000000 --- a/installer/package-lock.json +++ /dev/null @@ -1,6525 +0,0 @@ -{ - "name": "electron-app", - "version": "1.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "electron-app", - "version": "1.0.0", - "hasInstallScript": true, - "dependencies": { - "@electron-toolkit/preload": "^3.0.0", - "@electron-toolkit/utils": "^3.0.0" - }, - "devDependencies": { - "@electron-toolkit/eslint-config": "^1.0.2", - "@electron-toolkit/eslint-config-ts": "^1.0.1", - "@electron-toolkit/tsconfig": "^1.0.1", - "@rushstack/eslint-patch": "^1.7.1", - "@types/node": "^18.19.9", - "@vitejs/plugin-vue": "^5.0.3", - "@vue/eslint-config-prettier": "^9.0.0", - "@vue/eslint-config-typescript": "^12.0.0", - "electron": "^28.2.0", - "electron-builder": "^24.9.1", - "electron-vite": "^2.0.0", - "eslint": "^8.56.0", - "eslint-plugin-vue": "^9.20.1", - "prettier": "^3.2.4", - "typescript": "^5.3.3", - "vite": "^5.0.12", - "vue": "^3.4.15", - "vue-tsc": "^1.8.27" - } - }, - "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==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/@ampproject/remapping": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.1.tgz", - "integrity": "sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==", - "dev": true, - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.0", - "@jridgewell/trace-mapping": "^0.3.9" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@babel/code-frame": { - "version": "7.23.5", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.23.5.tgz", - "integrity": "sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA==", - "dev": true, - "dependencies": { - "@babel/highlight": "^7.23.4", - "chalk": "^2.4.2" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/code-frame/node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/code-frame/node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/code-frame/node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/@babel/code-frame/node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true - }, - "node_modules/@babel/code-frame/node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/@babel/code-frame/node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/code-frame/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/compat-data": { - "version": "7.23.5", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.23.5.tgz", - "integrity": "sha512-uU27kfDRlhfKl+w1U6vp16IuvSLtjAxdArVXPa9BvLkrr7CYIsxH5adpHObeAGY/41+syctUWOZ140a2Rvkgjw==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/core": { - "version": "7.23.9", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.23.9.tgz", - "integrity": "sha512-5q0175NOjddqpvvzU+kDiSOAk4PfdO6FvwCWoQ6RO7rTzEe8vlo+4HVfcnAREhD4npMs0e9uZypjTwzZPCf/cw==", - "dev": true, - "dependencies": { - "@ampproject/remapping": "^2.2.0", - "@babel/code-frame": "^7.23.5", - "@babel/generator": "^7.23.6", - "@babel/helper-compilation-targets": "^7.23.6", - "@babel/helper-module-transforms": "^7.23.3", - "@babel/helpers": "^7.23.9", - "@babel/parser": "^7.23.9", - "@babel/template": "^7.23.9", - "@babel/traverse": "^7.23.9", - "@babel/types": "^7.23.9", - "convert-source-map": "^2.0.0", - "debug": "^4.1.0", - "gensync": "^1.0.0-beta.2", - "json5": "^2.2.3", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/babel" - } - }, - "node_modules/@babel/core/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/@babel/generator": { - "version": "7.23.6", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.6.tgz", - "integrity": "sha512-qrSfCYxYQB5owCmGLbl8XRpX1ytXlpueOb0N0UmQwA073KZxejgQTzAmJezxvpwQD9uGtK2shHdi55QT+MbjIw==", - "dev": true, - "dependencies": { - "@babel/types": "^7.23.6", - "@jridgewell/gen-mapping": "^0.3.2", - "@jridgewell/trace-mapping": "^0.3.17", - "jsesc": "^2.5.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-compilation-targets": { - "version": "7.23.6", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.23.6.tgz", - "integrity": "sha512-9JB548GZoQVmzrFgp8o7KxdgkTGm6xs9DW0o/Pim72UDjzr5ObUQ6ZzYPqA+g9OTS2bBQoctLJrky0RDCAWRgQ==", - "dev": true, - "dependencies": { - "@babel/compat-data": "^7.23.5", - "@babel/helper-validator-option": "^7.23.5", - "browserslist": "^4.22.2", - "lru-cache": "^5.1.1", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-compilation-targets/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/@babel/helper-environment-visitor": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", - "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-function-name": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz", - "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==", - "dev": true, - "dependencies": { - "@babel/template": "^7.22.15", - "@babel/types": "^7.23.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-hoist-variables": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", - "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", - "dev": true, - "dependencies": { - "@babel/types": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-imports": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.22.15.tgz", - "integrity": "sha512-0pYVBnDKZO2fnSPCrgM/6WMc7eS20Fbok+0r88fp+YtWVLZrp4CkafFGIp+W0VKw4a22sgebPT99y+FDNMdP4w==", - "dev": true, - "dependencies": { - "@babel/types": "^7.22.15" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-transforms": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.23.3.tgz", - "integrity": "sha512-7bBs4ED9OmswdfDzpz4MpWgSrV7FXlc3zIagvLFjS5H+Mk7Snr21vQ6QwrsoCGMfNC4e4LQPdoULEt4ykz0SRQ==", - "dev": true, - "dependencies": { - "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-module-imports": "^7.22.15", - "@babel/helper-simple-access": "^7.22.5", - "@babel/helper-split-export-declaration": "^7.22.6", - "@babel/helper-validator-identifier": "^7.22.20" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-plugin-utils": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.22.5.tgz", - "integrity": "sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-simple-access": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.22.5.tgz", - "integrity": "sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==", - "dev": true, - "dependencies": { - "@babel/types": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-split-export-declaration": { - "version": "7.22.6", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", - "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", - "dev": true, - "dependencies": { - "@babel/types": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-string-parser": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.23.4.tgz", - "integrity": "sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", - "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-option": { - "version": "7.23.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.23.5.tgz", - "integrity": "sha512-85ttAOMLsr53VgXkTbkx8oA6YTfT4q7/HzXSLEYmjcSTJPMPQtvq1BD79Byep5xMUYbGRzEpDsjUf3dyp54IKw==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helpers": { - "version": "7.23.9", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.23.9.tgz", - "integrity": "sha512-87ICKgU5t5SzOT7sBMfCOZQ2rHjRU+Pcb9BoILMYz600W6DkVRLFBPwQ18gwUVvggqXivaUakpnxWQGbpywbBQ==", - "dev": true, - "dependencies": { - "@babel/template": "^7.23.9", - "@babel/traverse": "^7.23.9", - "@babel/types": "^7.23.9" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/highlight": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.23.4.tgz", - "integrity": "sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A==", - "dev": true, - "dependencies": { - "@babel/helper-validator-identifier": "^7.22.20", - "chalk": "^2.4.2", - "js-tokens": "^4.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/highlight/node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/highlight/node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/highlight/node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/@babel/highlight/node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true - }, - "node_modules/@babel/highlight/node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/@babel/highlight/node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/highlight/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/parser": { - "version": "7.23.9", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.9.tgz", - "integrity": "sha512-9tcKgqKbs3xGJ+NtKF2ndOBBLVwPjl1SHxPQkd36r3Dlirw3xWUeGaTbqr7uGZcTaxkVNwc+03SVP7aCdWrTlA==", - "dev": true, - "bin": { - "parser": "bin/babel-parser.js" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@babel/plugin-transform-arrow-functions": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.23.3.tgz", - "integrity": "sha512-NzQcQrzaQPkaEwoTm4Mhyl8jI1huEL/WWIEvudjTCMJ9aBZNpsJbMASx7EQECtQQPS/DcnFpo0FIh3LvEO9cxQ==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/template": { - "version": "7.23.9", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.23.9.tgz", - "integrity": "sha512-+xrD2BWLpvHKNmX2QbpdpsBaWnRxahMwJjO+KZk2JOElj5nSmKezyS1B4u+QbHMTX69t4ukm6hh9lsYQ7GHCKA==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.23.5", - "@babel/parser": "^7.23.9", - "@babel/types": "^7.23.9" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/traverse": { - "version": "7.23.9", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.9.tgz", - "integrity": "sha512-I/4UJ9vs90OkBtY6iiiTORVMyIhJ4kAVmsKo9KFc8UOxMeUfi2hvtIBsET5u9GizXE6/GFSuKCTNfgCswuEjRg==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.23.5", - "@babel/generator": "^7.23.6", - "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-function-name": "^7.23.0", - "@babel/helper-hoist-variables": "^7.22.5", - "@babel/helper-split-export-declaration": "^7.22.6", - "@babel/parser": "^7.23.9", - "@babel/types": "^7.23.9", - "debug": "^4.3.1", - "globals": "^11.1.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/types": { - "version": "7.23.9", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.9.tgz", - "integrity": "sha512-dQjSq/7HaSjRM43FFGnv5keM2HsxpmyV1PfaSVm0nzzjwwTmjOe6J4bC8e3+pTEIgHaHj+1ZlLThRJ2auc/w1Q==", - "dev": true, - "dependencies": { - "@babel/helper-string-parser": "^7.23.4", - "@babel/helper-validator-identifier": "^7.22.20", - "to-fast-properties": "^2.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@develar/schema-utils": { - "version": "2.6.5", - "resolved": "https://registry.npmjs.org/@develar/schema-utils/-/schema-utils-2.6.5.tgz", - "integrity": "sha512-0cp4PsWQ/9avqTVMCtZ+GirikIA36ikvjtHweU4/j8yLtgObI0+JUPhYFScgwlteveGB1rt3Cm8UhN04XayDig==", - "dev": true, - "dependencies": { - "ajv": "^6.12.0", - "ajv-keywords": "^3.4.1" - }, - "engines": { - "node": ">= 8.9.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, - "node_modules/@electron-toolkit/eslint-config": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@electron-toolkit/eslint-config/-/eslint-config-1.0.2.tgz", - "integrity": "sha512-GJVuMsxBHfVARfmUoSTCHT0e/QfWlVbXcGk3tgoku0ad6tLjydbv2LpvKi02+Sy2WiEz9L9SkGSw090ukT/F0A==", - "dev": true, - "peerDependencies": { - "eslint": ">= 8.0.0" - } - }, - "node_modules/@electron-toolkit/eslint-config-ts": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@electron-toolkit/eslint-config-ts/-/eslint-config-ts-1.0.1.tgz", - "integrity": "sha512-6RkoGE/W/WVlDozy/u6/lo5THNP5CCAPxdKpdQuYaopUfObs3o+JyxdlSb45imh+nL8nv875970EoyplntXaug==", - "dev": true, - "dependencies": { - "@typescript-eslint/eslint-plugin": "^6.14.0", - "@typescript-eslint/parser": "^6.14.0" - }, - "peerDependencies": { - "eslint": ">=8.0.0", - "typescript": "*" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@electron-toolkit/preload": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@electron-toolkit/preload/-/preload-3.0.0.tgz", - "integrity": "sha512-DTyvNGits43J1RiQGFN00/OxjjcTcozLWMjgQBANt97FFptEYiLVasq7kMC24nMwwoOpdDYY9CE6C+4wQJ55/w==", - "peerDependencies": { - "electron": ">=13.0.0" - } - }, - "node_modules/@electron-toolkit/tsconfig": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@electron-toolkit/tsconfig/-/tsconfig-1.0.1.tgz", - "integrity": "sha512-M0Mol3odspvtCuheyujLNAW7bXq7KFNYVMRtpjFa4ZfES4MuklXBC7Nli/omvc+PRKlrklgAGx3l4VakjNo8jg==", - "dev": true, - "peerDependencies": { - "@types/node": "*" - } - }, - "node_modules/@electron-toolkit/utils": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@electron-toolkit/utils/-/utils-3.0.0.tgz", - "integrity": "sha512-GaXHDhiT7KCvMJjXdp/QqpYinq69T/Pdl49Z1XLf8mKGf63dnsODMWyrmIjEQ0z/vG7dO8qF3fvmI6Eb2lUNZA==", - "peerDependencies": { - "electron": ">=13.0.0" - } - }, - "node_modules/@electron/asar": { - "version": "3.2.8", - "resolved": "https://registry.npmjs.org/@electron/asar/-/asar-3.2.8.tgz", - "integrity": "sha512-cmskk5M06ewHMZAplSiF4AlME3IrnnZhKnWbtwKVLRkdJkKyUVjMLhDIiPIx/+6zQWVlKX/LtmK9xDme7540Sg==", - "dev": true, - "dependencies": { - "commander": "^5.0.0", - "glob": "^7.1.6", - "minimatch": "^3.0.4" - }, - "bin": { - "asar": "bin/asar.js" - }, - "engines": { - "node": ">=10.12.0" - } - }, - "node_modules/@electron/asar/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==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/@electron/asar/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" - }, - "engines": { - "node": "*" - } - }, - "node_modules/@electron/get": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@electron/get/-/get-2.0.3.tgz", - "integrity": "sha512-Qkzpg2s9GnVV2I2BjRksUi43U5e6+zaQMcjoJy0C+C5oxaKl+fmckGDQFtRpZpZV0NQekuZZ+tGz7EA9TVnQtQ==", - "dependencies": { - "debug": "^4.1.1", - "env-paths": "^2.2.0", - "fs-extra": "^8.1.0", - "got": "^11.8.5", - "progress": "^2.0.3", - "semver": "^6.2.0", - "sumchecker": "^3.0.1" - }, - "engines": { - "node": ">=12" - }, - "optionalDependencies": { - "global-agent": "^3.0.0" - } - }, - "node_modules/@electron/get/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/@electron/notarize": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@electron/notarize/-/notarize-2.1.0.tgz", - "integrity": "sha512-Q02xem1D0sg4v437xHgmBLxI2iz/fc0D4K7fiVWHa/AnW8o7D751xyKNXgziA6HrTOme9ul1JfWN5ark8WH1xA==", - "dev": true, - "dependencies": { - "debug": "^4.1.1", - "fs-extra": "^9.0.1", - "promise-retry": "^2.0.1" - }, - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/@electron/notarize/node_modules/fs-extra": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", - "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", - "dev": true, - "dependencies": { - "at-least-node": "^1.0.0", - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@electron/notarize/node_modules/jsonfile": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", - "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", - "dev": true, - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/@electron/notarize/node_modules/universalify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", - "dev": true, - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/@electron/osx-sign": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@electron/osx-sign/-/osx-sign-1.0.5.tgz", - "integrity": "sha512-k9ZzUQtamSoweGQDV2jILiRIHUu7lYlJ3c6IEmjv1hC17rclE+eb9U+f6UFlOOETo0JzY1HNlXy4YOlCvl+Lww==", - "dev": true, - "dependencies": { - "compare-version": "^0.1.2", - "debug": "^4.3.4", - "fs-extra": "^10.0.0", - "isbinaryfile": "^4.0.8", - "minimist": "^1.2.6", - "plist": "^3.0.5" - }, - "bin": { - "electron-osx-flat": "bin/electron-osx-flat.js", - "electron-osx-sign": "bin/electron-osx-sign.js" - }, - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/@electron/osx-sign/node_modules/fs-extra": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", - "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", - "dev": true, - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@electron/osx-sign/node_modules/isbinaryfile": { - "version": "4.0.10", - "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-4.0.10.tgz", - "integrity": "sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw==", - "dev": true, - "engines": { - "node": ">= 8.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/gjtorikian/" - } - }, - "node_modules/@electron/osx-sign/node_modules/jsonfile": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", - "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", - "dev": true, - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/@electron/osx-sign/node_modules/universalify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", - "dev": true, - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/@electron/universal": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/@electron/universal/-/universal-1.4.1.tgz", - "integrity": "sha512-lE/U3UNw1YHuowNbTmKNs9UlS3En3cPgwM5MI+agIgr/B1hSze9NdOP0qn7boZaI9Lph8IDv3/24g9IxnJP7aQ==", - "dev": true, - "dependencies": { - "@electron/asar": "^3.2.1", - "@malept/cross-spawn-promise": "^1.1.0", - "debug": "^4.3.1", - "dir-compare": "^3.0.0", - "fs-extra": "^9.0.1", - "minimatch": "^3.0.4", - "plist": "^3.0.4" - }, - "engines": { - "node": ">=8.6" - } - }, - "node_modules/@electron/universal/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==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/@electron/universal/node_modules/fs-extra": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", - "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", - "dev": true, - "dependencies": { - "at-least-node": "^1.0.0", - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@electron/universal/node_modules/jsonfile": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", - "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", - "dev": true, - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/@electron/universal/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" - }, - "engines": { - "node": "*" - } - }, - "node_modules/@electron/universal/node_modules/universalify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", - "dev": true, - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.19.12.tgz", - "integrity": "sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.19.12.tgz", - "integrity": "sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.19.12.tgz", - "integrity": "sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.19.12.tgz", - "integrity": "sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.19.12.tgz", - "integrity": "sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.19.12.tgz", - "integrity": "sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.12.tgz", - "integrity": "sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.19.12.tgz", - "integrity": "sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.19.12.tgz", - "integrity": "sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.19.12.tgz", - "integrity": "sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.19.12.tgz", - "integrity": "sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.19.12.tgz", - "integrity": "sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA==", - "cpu": [ - "loong64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.19.12.tgz", - "integrity": "sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w==", - "cpu": [ - "mips64el" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.19.12.tgz", - "integrity": "sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg==", - "cpu": [ - "ppc64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.19.12.tgz", - "integrity": "sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg==", - "cpu": [ - "riscv64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.19.12.tgz", - "integrity": "sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg==", - "cpu": [ - "s390x" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.19.12.tgz", - "integrity": "sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.19.12.tgz", - "integrity": "sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.19.12.tgz", - "integrity": "sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.19.12.tgz", - "integrity": "sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.19.12.tgz", - "integrity": "sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.19.12.tgz", - "integrity": "sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.19.12.tgz", - "integrity": "sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "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==", - "dev": true, - "dependencies": { - "eslint-visitor-keys": "^3.3.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "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==", - "dev": true, - "engines": { - "node": "^12.0.0 || ^14.0.0 || >=16.0.0" - } - }, - "node_modules/@eslint/eslintrc": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", - "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", - "dev": true, - "dependencies": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^9.6.0", - "globals": "^13.19.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.1.2", - "strip-json-comments": "^3.1.1" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.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==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/@eslint/eslintrc/node_modules/globals": { - "version": "13.24.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", - "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", - "dev": true, - "dependencies": { - "type-fest": "^0.20.2" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "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==", - "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/@eslint/eslintrc/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==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@eslint/js": { - "version": "8.56.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.56.0.tgz", - "integrity": "sha512-gMsVel9D7f2HLkBma9VbtzZRehRogVRfbr++f06nL2vnCGCNlzOD+/MUov/F4p8myyAHspEhVobgjpX64q5m6A==", - "dev": true, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - } - }, - "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==", - "dev": true, - "dependencies": { - "@humanwhocodes/object-schema": "^2.0.2", - "debug": "^4.3.1", - "minimatch": "^3.0.5" - }, - "engines": { - "node": ">=10.10.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==", - "dev": true, - "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==", - "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "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==", - "dev": true, - "engines": { - "node": ">=12.22" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "node_modules/@humanwhocodes/object-schema": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.2.tgz", - "integrity": "sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw==", - "dev": true - }, - "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==", - "dev": true, - "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" - }, - "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==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/@isaacs/cliui/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==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/@isaacs/cliui/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==", - "dev": true - }, - "node_modules/@isaacs/cliui/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==", - "dev": true, - "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "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==", - "dev": true, - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/@isaacs/cliui/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==", - "dev": true, - "dependencies": { - "ansi-styles": "^6.1.0", - "string-width": "^5.0.1", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", - "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", - "dev": true, - "dependencies": { - "@jridgewell/set-array": "^1.0.1", - "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.9" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz", - "integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==", - "dev": true, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/set-array": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", - "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", - "dev": true, - "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==", - "dev": true - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.22", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.22.tgz", - "integrity": "sha512-Wf963MzWtA2sjrNt+g18IAln9lKnlRp+K2eH4jjIoF1wYeq3aMREpG09xhlhdzS0EjwU7qmUJYangWa+151vZw==", - "dev": true, - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, - "node_modules/@malept/cross-spawn-promise": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@malept/cross-spawn-promise/-/cross-spawn-promise-1.1.1.tgz", - "integrity": "sha512-RTBGWL5FWQcg9orDOCcp4LvItNzUPcyEU9bwaeJX0rJ1IQxzucC48Y0/sQLp/g6t99IQgAlGIaesJS+gTn7tVQ==", - "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/malept" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/subscription/pkg/npm-.malept-cross-spawn-promise?utm_medium=referral&utm_source=npm_fund" - } - ], - "dependencies": { - "cross-spawn": "^7.0.1" - }, - "engines": { - "node": ">= 10" - } - }, - "node_modules/@malept/flatpak-bundler": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/@malept/flatpak-bundler/-/flatpak-bundler-0.4.0.tgz", - "integrity": "sha512-9QOtNffcOF/c1seMCDnjckb3R9WHcG34tky+FHpNKKCW0wc/scYLwMtO+ptyGUfMW0/b/n4qRiALlaFHc9Oj7Q==", - "dev": true, - "dependencies": { - "debug": "^4.1.1", - "fs-extra": "^9.0.0", - "lodash": "^4.17.15", - "tmp-promise": "^3.0.2" - }, - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/@malept/flatpak-bundler/node_modules/fs-extra": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", - "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", - "dev": true, - "dependencies": { - "at-least-node": "^1.0.0", - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@malept/flatpak-bundler/node_modules/jsonfile": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", - "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", - "dev": true, - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/@malept/flatpak-bundler/node_modules/universalify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", - "dev": true, - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/@nodelib/fs.scandir": { - "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, - "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.stat": { - "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, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.walk": { - "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, - "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - }, - "engines": { - "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==", - "dev": true, - "optional": true, - "engines": { - "node": ">=14" - } - }, - "node_modules/@pkgr/core": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.1.1.tgz", - "integrity": "sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA==", - "dev": true, - "engines": { - "node": "^12.20.0 || ^14.18.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/unts" - } - }, - "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.9.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.9.6.tgz", - "integrity": "sha512-MVNXSSYN6QXOulbHpLMKYi60ppyO13W9my1qogeiAqtjb2yR4LSmfU2+POvDkLzhjYLXz9Rf9+9a3zFHW1Lecg==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-android-arm64": { - "version": "4.9.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.9.6.tgz", - "integrity": "sha512-T14aNLpqJ5wzKNf5jEDpv5zgyIqcpn1MlwCrUXLrwoADr2RkWA0vOWP4XxbO9aiO3dvMCQICZdKeDrFl7UMClw==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.9.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.9.6.tgz", - "integrity": "sha512-CqNNAyhRkTbo8VVZ5R85X73H3R5NX9ONnKbXuHisGWC0qRbTTxnF1U4V9NafzJbgGM0sHZpdO83pLPzq8uOZFw==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.9.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.9.6.tgz", - "integrity": "sha512-zRDtdJuRvA1dc9Mp6BWYqAsU5oeLixdfUvkTHuiYOHwqYuQ4YgSmi6+/lPvSsqc/I0Omw3DdICx4Tfacdzmhog==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.9.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.9.6.tgz", - "integrity": "sha512-oNk8YXDDnNyG4qlNb6is1ojTOGL/tRhbbKeE/YuccItzerEZT68Z9gHrY3ROh7axDc974+zYAPxK5SH0j/G+QQ==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.9.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.9.6.tgz", - "integrity": "sha512-Z3O60yxPtuCYobrtzjo0wlmvDdx2qZfeAWTyfOjEDqd08kthDKexLpV97KfAeUXPosENKd8uyJMRDfFMxcYkDQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.9.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.9.6.tgz", - "integrity": "sha512-gpiG0qQJNdYEVad+1iAsGAbgAnZ8j07FapmnIAQgODKcOTjLEWM9sRb+MbQyVsYCnA0Im6M6QIq6ax7liws6eQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.9.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.9.6.tgz", - "integrity": "sha512-+uCOcvVmFUYvVDr27aiyun9WgZk0tXe7ThuzoUTAukZJOwS5MrGbmSlNOhx1j80GdpqbOty05XqSl5w4dQvcOA==", - "cpu": [ - "riscv64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.9.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.9.6.tgz", - "integrity": "sha512-HUNqM32dGzfBKuaDUBqFB7tP6VMN74eLZ33Q9Y1TBqRDn+qDonkAUyKWwF9BR9unV7QUzffLnz9GrnKvMqC/fw==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.9.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.9.6.tgz", - "integrity": "sha512-ch7M+9Tr5R4FK40FHQk8VnML0Szi2KRujUgHXd/HjuH9ifH72GUmw6lStZBo3c3GB82vHa0ZoUfjfcM7JiiMrQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.9.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.9.6.tgz", - "integrity": "sha512-VD6qnR99dhmTQ1mJhIzXsRcTBvTjbfbGGwKAHcu+52cVl15AC/kplkhxzW/uT0Xl62Y/meBKDZvoJSJN+vTeGA==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.9.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.9.6.tgz", - "integrity": "sha512-J9AFDq/xiRI58eR2NIDfyVmTYGyIZmRcvcAoJ48oDld/NTR8wyiPUu2X/v1navJ+N/FGg68LEbX3Ejd6l8B7MQ==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.9.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.9.6.tgz", - "integrity": "sha512-jqzNLhNDvIZOrt69Ce4UjGRpXJBzhUBzawMwnaDAwyHriki3XollsewxWzOzz+4yOFDkuJHtTsZFwMxhYJWmLQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rushstack/eslint-patch": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.7.2.tgz", - "integrity": "sha512-RbhOOTCNoCrbfkRyoXODZp75MlpiHMgbE5MEBZAnnnLyQNgrigEj4p0lzsMDyc1zVsJDLrivB58tgg3emX0eEA==", - "dev": true - }, - "node_modules/@sindresorhus/is": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", - "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sindresorhus/is?sponsor=1" - } - }, - "node_modules/@szmarczak/http-timer": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz", - "integrity": "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==", - "dependencies": { - "defer-to-connect": "^2.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@tootallnate/once": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", - "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==", - "dev": true, - "engines": { - "node": ">= 10" - } - }, - "node_modules/@types/cacheable-request": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz", - "integrity": "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==", - "dependencies": { - "@types/http-cache-semantics": "*", - "@types/keyv": "^3.1.4", - "@types/node": "*", - "@types/responselike": "^1.0.0" - } - }, - "node_modules/@types/debug": { - "version": "4.1.12", - "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", - "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==", - "dev": true, - "dependencies": { - "@types/ms": "*" - } - }, - "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 - }, - "node_modules/@types/fs-extra": { - "version": "9.0.13", - "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-9.0.13.tgz", - "integrity": "sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA==", - "dev": true, - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/http-cache-semantics": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.4.tgz", - "integrity": "sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==" - }, - "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==", - "dev": true - }, - "node_modules/@types/keyv": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz", - "integrity": "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/ms": { - "version": "0.7.34", - "resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.34.tgz", - "integrity": "sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==", - "dev": true - }, - "node_modules/@types/node": { - "version": "18.19.10", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.10.tgz", - "integrity": "sha512-IZD8kAM02AW1HRDTPOlz3npFava678pr8Ie9Vp8uRhBROXAv8MXT2pCnGZZAKYdromsNQLHQcfWQ6EOatVLtqA==", - "dependencies": { - "undici-types": "~5.26.4" - } - }, - "node_modules/@types/plist": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@types/plist/-/plist-3.0.5.tgz", - "integrity": "sha512-E6OCaRmAe4WDmWNsL/9RMqdkkzDCY1etutkflWk4c+AcjDU07Pcz1fQwTX0TQz+Pxqn9i4L1TU3UFpjnrcDgxA==", - "dev": true, - "optional": true, - "dependencies": { - "@types/node": "*", - "xmlbuilder": ">=11.0.1" - } - }, - "node_modules/@types/responselike": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.3.tgz", - "integrity": "sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/semver": { - "version": "7.5.6", - "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.6.tgz", - "integrity": "sha512-dn1l8LaMea/IjDoHNd9J52uBbInB796CDffS6VdIxvqYCPSG0V0DzHp76GpaWnlhg88uYyPbXCDIowa86ybd5A==", - "dev": true - }, - "node_modules/@types/verror": { - "version": "1.10.9", - "resolved": "https://registry.npmjs.org/@types/verror/-/verror-1.10.9.tgz", - "integrity": "sha512-MLx9Z+9lGzwEuW16ubGeNkpBDE84RpB/NyGgg6z2BTpWzKkGU451cAY3UkUzZEp72RHF585oJ3V8JVNqIplcAQ==", - "dev": true, - "optional": true - }, - "node_modules/@types/yauzl": { - "version": "2.10.3", - "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", - "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", - "optional": true, - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@typescript-eslint/eslint-plugin": { - "version": "6.19.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.19.1.tgz", - "integrity": "sha512-roQScUGFruWod9CEyoV5KlCYrubC/fvG8/1zXuT0WTcxX87GnMMmnksMwSg99lo1xiKrBzw2icsJPMAw1OtKxg==", - "dev": true, - "dependencies": { - "@eslint-community/regexpp": "^4.5.1", - "@typescript-eslint/scope-manager": "6.19.1", - "@typescript-eslint/type-utils": "6.19.1", - "@typescript-eslint/utils": "6.19.1", - "@typescript-eslint/visitor-keys": "6.19.1", - "debug": "^4.3.4", - "graphemer": "^1.4.0", - "ignore": "^5.2.4", - "natural-compare": "^1.4.0", - "semver": "^7.5.4", - "ts-api-utils": "^1.0.1" - }, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "@typescript-eslint/parser": "^6.0.0 || ^6.0.0-alpha", - "eslint": "^7.0.0 || ^8.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/parser": { - "version": "6.19.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.19.1.tgz", - "integrity": "sha512-WEfX22ziAh6pRE9jnbkkLGp/4RhTpffr2ZK5bJ18M8mIfA8A+k97U9ZyaXCEJRlmMHh7R9MJZWXp/r73DzINVQ==", - "dev": true, - "dependencies": { - "@typescript-eslint/scope-manager": "6.19.1", - "@typescript-eslint/types": "6.19.1", - "@typescript-eslint/typescript-estree": "6.19.1", - "@typescript-eslint/visitor-keys": "6.19.1", - "debug": "^4.3.4" - }, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^7.0.0 || ^8.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/scope-manager": { - "version": "6.19.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.19.1.tgz", - "integrity": "sha512-4CdXYjKf6/6aKNMSly/BP4iCSOpvMmqtDzRtqFyyAae3z5kkqEjKndR5vDHL8rSuMIIWP8u4Mw4VxLyxZW6D5w==", - "dev": true, - "dependencies": { - "@typescript-eslint/types": "6.19.1", - "@typescript-eslint/visitor-keys": "6.19.1" - }, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/type-utils": { - "version": "6.19.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.19.1.tgz", - "integrity": "sha512-0vdyld3ecfxJuddDjACUvlAeYNrHP/pDeQk2pWBR2ESeEzQhg52DF53AbI9QCBkYE23lgkhLCZNkHn2hEXXYIg==", - "dev": true, - "dependencies": { - "@typescript-eslint/typescript-estree": "6.19.1", - "@typescript-eslint/utils": "6.19.1", - "debug": "^4.3.4", - "ts-api-utils": "^1.0.1" - }, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^7.0.0 || ^8.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/types": { - "version": "6.19.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.19.1.tgz", - "integrity": "sha512-6+bk6FEtBhvfYvpHsDgAL3uo4BfvnTnoge5LrrCj2eJN8g3IJdLTD4B/jK3Q6vo4Ql/Hoip9I8aB6fF+6RfDqg==", - "dev": true, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/typescript-estree": { - "version": "6.19.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.19.1.tgz", - "integrity": "sha512-aFdAxuhzBFRWhy+H20nYu19+Km+gFfwNO4TEqyszkMcgBDYQjmPJ61erHxuT2ESJXhlhrO7I5EFIlZ+qGR8oVA==", - "dev": true, - "dependencies": { - "@typescript-eslint/types": "6.19.1", - "@typescript-eslint/visitor-keys": "6.19.1", - "debug": "^4.3.4", - "globby": "^11.1.0", - "is-glob": "^4.0.3", - "minimatch": "9.0.3", - "semver": "^7.5.4", - "ts-api-utils": "^1.0.1" - }, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/utils": { - "version": "6.19.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.19.1.tgz", - "integrity": "sha512-JvjfEZuP5WoMqwh9SPAPDSHSg9FBHHGhjPugSRxu5jMfjvBpq5/sGTD+9M9aQ5sh6iJ8AY/Kk/oUYVEMAPwi7w==", - "dev": true, - "dependencies": { - "@eslint-community/eslint-utils": "^4.4.0", - "@types/json-schema": "^7.0.12", - "@types/semver": "^7.5.0", - "@typescript-eslint/scope-manager": "6.19.1", - "@typescript-eslint/types": "6.19.1", - "@typescript-eslint/typescript-estree": "6.19.1", - "semver": "^7.5.4" - }, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^7.0.0 || ^8.0.0" - } - }, - "node_modules/@typescript-eslint/visitor-keys": { - "version": "6.19.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.19.1.tgz", - "integrity": "sha512-gkdtIO+xSO/SmI0W68DBg4u1KElmIUo3vXzgHyGPs6cxgB0sa3TlptRAAE0hUY1hM6FcDKEv7aIwiTGm76cXfQ==", - "dev": true, - "dependencies": { - "@typescript-eslint/types": "6.19.1", - "eslint-visitor-keys": "^3.4.1" - }, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-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==", - "dev": true - }, - "node_modules/@vitejs/plugin-vue": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.0.3.tgz", - "integrity": "sha512-b8S5dVS40rgHdDrw+DQi/xOM9ed+kSRZzfm1T74bMmBDCd8XO87NKlFYInzCtwvtWwXZvo1QxE2OSspTATWrbA==", - "dev": true, - "engines": { - "node": "^18.0.0 || >=20.0.0" - }, - "peerDependencies": { - "vite": "^5.0.0", - "vue": "^3.2.25" - } - }, - "node_modules/@volar/language-core": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-1.11.1.tgz", - "integrity": "sha512-dOcNn3i9GgZAcJt43wuaEykSluAuOkQgzni1cuxLxTV0nJKanQztp7FxyswdRILaKH+P2XZMPRp2S4MV/pElCw==", - "dev": true, - "dependencies": { - "@volar/source-map": "1.11.1" - } - }, - "node_modules/@volar/source-map": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-1.11.1.tgz", - "integrity": "sha512-hJnOnwZ4+WT5iupLRnuzbULZ42L7BWWPMmruzwtLhJfpDVoZLjNBxHDi2sY2bgZXCKlpU5XcsMFoYrsQmPhfZg==", - "dev": true, - "dependencies": { - "muggle-string": "^0.3.1" - } - }, - "node_modules/@volar/typescript": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-1.11.1.tgz", - "integrity": "sha512-iU+t2mas/4lYierSnoFOeRFQUhAEMgsFuQxoxvwn5EdQopw43j+J27a4lt9LMInx1gLJBC6qL14WYGlgymaSMQ==", - "dev": true, - "dependencies": { - "@volar/language-core": "1.11.1", - "path-browserify": "^1.0.1" - } - }, - "node_modules/@vue/compiler-core": { - "version": "3.4.15", - "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.4.15.tgz", - "integrity": "sha512-XcJQVOaxTKCnth1vCxEChteGuwG6wqnUHxAm1DO3gCz0+uXKaJNx8/digSz4dLALCy8n2lKq24jSUs8segoqIw==", - "dev": true, - "dependencies": { - "@babel/parser": "^7.23.6", - "@vue/shared": "3.4.15", - "entities": "^4.5.0", - "estree-walker": "^2.0.2", - "source-map-js": "^1.0.2" - } - }, - "node_modules/@vue/compiler-dom": { - "version": "3.4.15", - "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.4.15.tgz", - "integrity": "sha512-wox0aasVV74zoXyblarOM3AZQz/Z+OunYcIHe1OsGclCHt8RsRm04DObjefaI82u6XDzv+qGWZ24tIsRAIi5MQ==", - "dev": true, - "dependencies": { - "@vue/compiler-core": "3.4.15", - "@vue/shared": "3.4.15" - } - }, - "node_modules/@vue/compiler-sfc": { - "version": "3.4.15", - "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.4.15.tgz", - "integrity": "sha512-LCn5M6QpkpFsh3GQvs2mJUOAlBQcCco8D60Bcqmf3O3w5a+KWS5GvYbrrJBkgvL1BDnTp+e8q0lXCLgHhKguBA==", - "dev": true, - "dependencies": { - "@babel/parser": "^7.23.6", - "@vue/compiler-core": "3.4.15", - "@vue/compiler-dom": "3.4.15", - "@vue/compiler-ssr": "3.4.15", - "@vue/shared": "3.4.15", - "estree-walker": "^2.0.2", - "magic-string": "^0.30.5", - "postcss": "^8.4.33", - "source-map-js": "^1.0.2" - } - }, - "node_modules/@vue/compiler-ssr": { - "version": "3.4.15", - "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.4.15.tgz", - "integrity": "sha512-1jdeQyiGznr8gjFDadVmOJqZiLNSsMa5ZgqavkPZ8O2wjHv0tVuAEsw5hTdUoUW4232vpBbL/wJhzVW/JwY1Uw==", - "dev": true, - "dependencies": { - "@vue/compiler-dom": "3.4.15", - "@vue/shared": "3.4.15" - } - }, - "node_modules/@vue/eslint-config-prettier": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/@vue/eslint-config-prettier/-/eslint-config-prettier-9.0.0.tgz", - "integrity": "sha512-z1ZIAAUS9pKzo/ANEfd2sO+v2IUalz7cM/cTLOZ7vRFOPk5/xuRKQteOu1DErFLAh/lYGXMVZ0IfYKlyInuDVg==", - "dev": true, - "dependencies": { - "eslint-config-prettier": "^9.0.0", - "eslint-plugin-prettier": "^5.0.0" - }, - "peerDependencies": { - "eslint": ">= 8.0.0", - "prettier": ">= 3.0.0" - } - }, - "node_modules/@vue/eslint-config-typescript": { - "version": "12.0.0", - "resolved": "https://registry.npmjs.org/@vue/eslint-config-typescript/-/eslint-config-typescript-12.0.0.tgz", - "integrity": "sha512-StxLFet2Qe97T8+7L8pGlhYBBr8Eg05LPuTDVopQV6il+SK6qqom59BA/rcFipUef2jD8P2X44Vd8tMFytfvlg==", - "dev": true, - "dependencies": { - "@typescript-eslint/eslint-plugin": "^6.7.0", - "@typescript-eslint/parser": "^6.7.0", - "vue-eslint-parser": "^9.3.1" - }, - "engines": { - "node": "^14.17.0 || >=16.0.0" - }, - "peerDependencies": { - "eslint": "^6.2.0 || ^7.0.0 || ^8.0.0", - "eslint-plugin-vue": "^9.0.0", - "typescript": "*" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@vue/language-core": { - "version": "1.8.27", - "resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-1.8.27.tgz", - "integrity": "sha512-L8Kc27VdQserNaCUNiSFdDl9LWT24ly8Hpwf1ECy3aFb9m6bDhBGQYOujDm21N7EW3moKIOKEanQwe1q5BK+mA==", - "dev": true, - "dependencies": { - "@volar/language-core": "~1.11.1", - "@volar/source-map": "~1.11.1", - "@vue/compiler-dom": "^3.3.0", - "@vue/shared": "^3.3.0", - "computeds": "^0.0.1", - "minimatch": "^9.0.3", - "muggle-string": "^0.3.1", - "path-browserify": "^1.0.1", - "vue-template-compiler": "^2.7.14" - }, - "peerDependencies": { - "typescript": "*" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@vue/reactivity": { - "version": "3.4.15", - "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.4.15.tgz", - "integrity": "sha512-55yJh2bsff20K5O84MxSvXKPHHt17I2EomHznvFiJCAZpJTNW8IuLj1xZWMLELRhBK3kkFV/1ErZGHJfah7i7w==", - "dev": true, - "dependencies": { - "@vue/shared": "3.4.15" - } - }, - "node_modules/@vue/runtime-core": { - "version": "3.4.15", - "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.4.15.tgz", - "integrity": "sha512-6E3by5m6v1AkW0McCeAyhHTw+3y17YCOKG0U0HDKDscV4Hs0kgNT5G+GCHak16jKgcCDHpI9xe5NKb8sdLCLdw==", - "dev": true, - "dependencies": { - "@vue/reactivity": "3.4.15", - "@vue/shared": "3.4.15" - } - }, - "node_modules/@vue/runtime-dom": { - "version": "3.4.15", - "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.4.15.tgz", - "integrity": "sha512-EVW8D6vfFVq3V/yDKNPBFkZKGMFSvZrUQmx196o/v2tHKdwWdiZjYUBS+0Ez3+ohRyF8Njwy/6FH5gYJ75liUw==", - "dev": true, - "dependencies": { - "@vue/runtime-core": "3.4.15", - "@vue/shared": "3.4.15", - "csstype": "^3.1.3" - } - }, - "node_modules/@vue/server-renderer": { - "version": "3.4.15", - "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.4.15.tgz", - "integrity": "sha512-3HYzaidu9cHjrT+qGUuDhFYvF/j643bHC6uUN9BgM11DVy+pM6ATsG6uPBLnkwOgs7BpJABReLmpL3ZPAsUaqw==", - "dev": true, - "dependencies": { - "@vue/compiler-ssr": "3.4.15", - "@vue/shared": "3.4.15" - }, - "peerDependencies": { - "vue": "3.4.15" - } - }, - "node_modules/@vue/shared": { - "version": "3.4.15", - "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.15.tgz", - "integrity": "sha512-KzfPTxVaWfB+eGcGdbSf4CWdaXcGDqckoeXUh7SB3fZdEtzPCK2Vq9B/lRRL3yutax/LWITz+SwvgyOxz5V75g==", - "dev": true - }, - "node_modules/@xmldom/xmldom": { - "version": "0.8.10", - "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.10.tgz", - "integrity": "sha512-2WALfTl4xo2SkGCYRt6rDTFfk9R1czmBvUQy12gK2KuRKIpWEhcbbzy8EZXtz/jkRqHX8bFEc6FC1HjX4TUWYw==", - "dev": true, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/7zip-bin": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/7zip-bin/-/7zip-bin-5.2.0.tgz", - "integrity": "sha512-ukTPVhqG4jNzMro2qA9HSCSSVJN3aN7tlb+hfqYCt3ER0yWroeA2VR38MNrOHLQ/cVj+DaIMad0kFCtWWowh/A==", - "dev": true - }, - "node_modules/acorn": { - "version": "8.11.3", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", - "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", - "dev": true, - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-jsx": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "dev": true, - "peerDependencies": { - "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" - } - }, - "node_modules/agent-base": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", - "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", - "dev": true, - "dependencies": { - "debug": "4" - }, - "engines": { - "node": ">= 6.0.0" - } - }, - "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/ajv-keywords": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", - "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", - "dev": true, - "peerDependencies": { - "ajv": "^6.9.1" - } - }, - "node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "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==", - "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/app-builder-bin": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/app-builder-bin/-/app-builder-bin-4.0.0.tgz", - "integrity": "sha512-xwdG0FJPQMe0M0UA4Tz0zEB8rBJTRA5a476ZawAqiBkMv16GRK5xpXThOjMaEOFnZ6zabejjG4J3da0SXG63KA==", - "dev": true - }, - "node_modules/app-builder-lib": { - "version": "24.9.1", - "resolved": "https://registry.npmjs.org/app-builder-lib/-/app-builder-lib-24.9.1.tgz", - "integrity": "sha512-Q1nYxZcio4r+W72cnIRVYofEAyjBd3mG47o+zms8HlD51zWtA/YxJb01Jei5F+jkWhge/PTQK+uldsPh6d0/4g==", - "dev": true, - "dependencies": { - "@develar/schema-utils": "~2.6.5", - "@electron/notarize": "2.1.0", - "@electron/osx-sign": "1.0.5", - "@electron/universal": "1.4.1", - "@malept/flatpak-bundler": "^0.4.0", - "@types/fs-extra": "9.0.13", - "7zip-bin": "~5.2.0", - "async-exit-hook": "^2.0.1", - "bluebird-lst": "^1.0.9", - "builder-util": "24.8.1", - "builder-util-runtime": "9.2.3", - "chromium-pickle-js": "^0.2.0", - "debug": "^4.3.4", - "ejs": "^3.1.8", - "electron-publish": "24.8.1", - "form-data": "^4.0.0", - "fs-extra": "^10.1.0", - "hosted-git-info": "^4.1.0", - "is-ci": "^3.0.0", - "isbinaryfile": "^5.0.0", - "js-yaml": "^4.1.0", - "lazy-val": "^1.0.5", - "minimatch": "^5.1.1", - "read-config-file": "6.3.2", - "sanitize-filename": "^1.6.3", - "semver": "^7.3.8", - "tar": "^6.1.12", - "temp-file": "^3.4.0" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/app-builder-lib/node_modules/fs-extra": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", - "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", - "dev": true, - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/app-builder-lib/node_modules/jsonfile": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", - "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", - "dev": true, - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/app-builder-lib/node_modules/minimatch": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", - "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", - "dev": true, - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/app-builder-lib/node_modules/universalify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", - "dev": true, - "engines": { - "node": ">= 10.0.0" - } - }, - "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==", - "dev": true - }, - "node_modules/array-union": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", - "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/assert-plus": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==", - "dev": true, - "optional": true, - "engines": { - "node": ">=0.8" - } - }, - "node_modules/astral-regex": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", - "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", - "dev": true, - "optional": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/async": { - "version": "3.2.5", - "resolved": "https://registry.npmjs.org/async/-/async-3.2.5.tgz", - "integrity": "sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==", - "dev": true - }, - "node_modules/async-exit-hook": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/async-exit-hook/-/async-exit-hook-2.0.1.tgz", - "integrity": "sha512-NW2cX8m1Q7KPA7a5M2ULQeZ2wR5qI5PAbw5L0UOMxdioVk9PMZ0h1TmyZEkPYrCvYjDlFICusOu1dlEKAAeXBw==", - "dev": true, - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "dev": true - }, - "node_modules/at-least-node": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", - "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", - "dev": true, - "engines": { - "node": ">= 4.0.0" - } - }, - "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==", - "dev": true - }, - "node_modules/base64-js": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/bluebird": { - "version": "3.7.2", - "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", - "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", - "dev": true - }, - "node_modules/bluebird-lst": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/bluebird-lst/-/bluebird-lst-1.0.9.tgz", - "integrity": "sha512-7B1Rtx82hjnSD4PGLAjVWeYH3tHAcVUmChh85a3lltKQm6FresXh9ErQo6oAv6CqxttczC3/kEg8SY5NluPuUw==", - "dev": true, - "dependencies": { - "bluebird": "^3.5.5" - } - }, - "node_modules/boolbase": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", - "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", - "dev": true - }, - "node_modules/boolean": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/boolean/-/boolean-3.2.0.tgz", - "integrity": "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==", - "optional": true - }, - "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==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", - "dev": true, - "dependencies": { - "fill-range": "^7.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/browserslist": { - "version": "4.22.3", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.22.3.tgz", - "integrity": "sha512-UAp55yfwNv0klWNapjs/ktHoguxuQNGnOzxYmfnXIS+8AsRDZkSDxg7R1AX3GKzn078SBI5dzwzj/Yx0Or0e3A==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "dependencies": { - "caniuse-lite": "^1.0.30001580", - "electron-to-chromium": "^1.4.648", - "node-releases": "^2.0.14", - "update-browserslist-db": "^1.0.13" - }, - "bin": { - "browserslist": "cli.js" - }, - "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" - } - }, - "node_modules/buffer": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", - "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "optional": true, - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.1.13" - } - }, - "node_modules/buffer-crc32": { - "version": "0.2.13", - "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", - "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", - "engines": { - "node": "*" - } - }, - "node_modules/buffer-equal": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/buffer-equal/-/buffer-equal-1.0.1.tgz", - "integrity": "sha512-QoV3ptgEaQpvVwbXdSO39iqPQTCxSF7A5U99AxbHYqUdCizL/lH2Z0A2y6nbZucxMEOtNyZfG2s6gsVugGpKkg==", - "dev": true, - "engines": { - "node": ">=0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/buffer-from": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", - "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", - "dev": true - }, - "node_modules/builder-util": { - "version": "24.8.1", - "resolved": "https://registry.npmjs.org/builder-util/-/builder-util-24.8.1.tgz", - "integrity": "sha512-ibmQ4BnnqCnJTNrdmdNlnhF48kfqhNzSeqFMXHLIl+o9/yhn6QfOaVrloZ9YUu3m0k3rexvlT5wcki6LWpjTZw==", - "dev": true, - "dependencies": { - "@types/debug": "^4.1.6", - "7zip-bin": "~5.2.0", - "app-builder-bin": "4.0.0", - "bluebird-lst": "^1.0.9", - "builder-util-runtime": "9.2.3", - "chalk": "^4.1.2", - "cross-spawn": "^7.0.3", - "debug": "^4.3.4", - "fs-extra": "^10.1.0", - "http-proxy-agent": "^5.0.0", - "https-proxy-agent": "^5.0.1", - "is-ci": "^3.0.0", - "js-yaml": "^4.1.0", - "source-map-support": "^0.5.19", - "stat-mode": "^1.0.0", - "temp-file": "^3.4.0" - } - }, - "node_modules/builder-util-runtime": { - "version": "9.2.3", - "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.2.3.tgz", - "integrity": "sha512-FGhkqXdFFZ5dNC4C+yuQB9ak311rpGAw+/ASz8ZdxwODCv1GGMWgLDeofRkdi0F3VCHQEWy/aXcJQozx2nOPiw==", - "dev": true, - "dependencies": { - "debug": "^4.3.4", - "sax": "^1.2.4" - }, - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/builder-util/node_modules/fs-extra": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", - "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", - "dev": true, - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/builder-util/node_modules/jsonfile": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", - "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", - "dev": true, - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/builder-util/node_modules/universalify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", - "dev": true, - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/cac": { - "version": "6.7.14", - "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", - "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/cacheable-lookup": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz", - "integrity": "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==", - "engines": { - "node": ">=10.6.0" - } - }, - "node_modules/cacheable-request": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.4.tgz", - "integrity": "sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==", - "dependencies": { - "clone-response": "^1.0.2", - "get-stream": "^5.1.0", - "http-cache-semantics": "^4.0.0", - "keyv": "^4.0.0", - "lowercase-keys": "^2.0.0", - "normalize-url": "^6.0.1", - "responselike": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/caniuse-lite": { - "version": "1.0.30001580", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001580.tgz", - "integrity": "sha512-mtj5ur2FFPZcCEpXFy8ADXbDACuNFXg6mxVDqp7tqooX6l3zwm+d8EPoeOSIFRDvHs8qu7/SLFOGniULkcH2iA==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/caniuse-lite" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ] - }, - "node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/chownr": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", - "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", - "dev": true, - "engines": { - "node": ">=10" - } - }, - "node_modules/chromium-pickle-js": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/chromium-pickle-js/-/chromium-pickle-js-0.2.0.tgz", - "integrity": "sha512-1R5Fho+jBq0DDydt+/vHWj5KJNJCKdARKOCwZUen84I5BreWoLqRLANH1U87eJy1tiASPtMnGqJJq0ZsLoRPOw==", - "dev": true - }, - "node_modules/ci-info": { - "version": "3.9.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", - "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/sibiraj-s" - } - ], - "engines": { - "node": ">=8" - } - }, - "node_modules/cli-truncate": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-2.1.0.tgz", - "integrity": "sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==", - "dev": true, - "optional": true, - "dependencies": { - "slice-ansi": "^3.0.0", - "string-width": "^4.2.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/cliui": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", - "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", - "dev": true, - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/clone-response": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.3.tgz", - "integrity": "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==", - "dependencies": { - "mimic-response": "^1.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "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==", - "dev": true - }, - "node_modules/combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dev": true, - "dependencies": { - "delayed-stream": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/commander": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz", - "integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==", - "dev": true, - "engines": { - "node": ">= 6" - } - }, - "node_modules/compare-version": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/compare-version/-/compare-version-0.1.2.tgz", - "integrity": "sha512-pJDh5/4wrEnXX/VWRZvruAGHkzKdr46z11OlTPN+VrATlWWhSKewNCJ1futCO5C7eJB3nPMFZA1LeYtcFboZ2A==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/computeds": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/computeds/-/computeds-0.0.1.tgz", - "integrity": "sha512-7CEBgcMjVmitjYo5q8JTJVra6X5mQ20uTThdK+0kR7UEaDrAWEQcRiBtWJzga4eRpP6afNwwLsX2SET2JhVB1Q==", - "dev": true - }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true - }, - "node_modules/config-file-ts": { - "version": "0.2.6", - "resolved": "https://registry.npmjs.org/config-file-ts/-/config-file-ts-0.2.6.tgz", - "integrity": "sha512-6boGVaglwblBgJqGyxm4+xCmEGcWgnWHSWHY5jad58awQhB6gftq0G8HbzU39YqCIYHMLAiL1yjwiZ36m/CL8w==", - "dev": true, - "dependencies": { - "glob": "^10.3.10", - "typescript": "^5.3.3" - } - }, - "node_modules/config-file-ts/node_modules/glob": { - "version": "10.3.10", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz", - "integrity": "sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==", - "dev": true, - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^2.3.5", - "minimatch": "^9.0.1", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0", - "path-scurry": "^1.10.1" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/convert-source-map": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", - "dev": true - }, - "node_modules/core-util-is": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==", - "dev": true, - "optional": true - }, - "node_modules/crc": { - "version": "3.8.0", - "resolved": "https://registry.npmjs.org/crc/-/crc-3.8.0.tgz", - "integrity": "sha512-iX3mfgcTMIq3ZKLIsVFAbv7+Mc10kxabAGQb8HvjA1o3T1PIYprbakQ65d3I+2HGHt6nSKkM9PYjgoJO2KcFBQ==", - "dev": true, - "optional": true, - "dependencies": { - "buffer": "^5.1.0" - } - }, - "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==", - "dev": true, - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/cssesc": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", - "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", - "dev": true, - "bin": { - "cssesc": "bin/cssesc" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/csstype": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", - "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "dev": true - }, - "node_modules/de-indent": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz", - "integrity": "sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==", - "dev": true - }, - "node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/decompress-response": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", - "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", - "dependencies": { - "mimic-response": "^3.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/decompress-response/node_modules/mimic-response": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", - "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "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==", - "dev": true - }, - "node_modules/defer-to-connect": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", - "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==", - "engines": { - "node": ">=10" - } - }, - "node_modules/define-data-property": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.1.tgz", - "integrity": "sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==", - "optional": true, - "dependencies": { - "get-intrinsic": "^1.2.1", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/define-properties": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", - "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", - "optional": true, - "dependencies": { - "define-data-property": "^1.0.1", - "has-property-descriptors": "^1.0.0", - "object-keys": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "dev": true, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/detect-node": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", - "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", - "optional": true - }, - "node_modules/dir-compare": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/dir-compare/-/dir-compare-3.3.0.tgz", - "integrity": "sha512-J7/et3WlGUCxjdnD3HAAzQ6nsnc0WL6DD7WcwJb7c39iH1+AWfg+9OqzJNaI6PkBwBvm1mhZNL9iY/nRiZXlPg==", - "dev": true, - "dependencies": { - "buffer-equal": "^1.0.0", - "minimatch": "^3.0.4" - } - }, - "node_modules/dir-compare/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==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/dir-compare/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" - }, - "engines": { - "node": "*" - } - }, - "node_modules/dir-glob": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", - "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", - "dev": true, - "dependencies": { - "path-type": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/dmg-builder": { - "version": "24.9.1", - "resolved": "https://registry.npmjs.org/dmg-builder/-/dmg-builder-24.9.1.tgz", - "integrity": "sha512-huC+O6hvHd24Ubj3cy2GMiGLe2xGFKN3klqVMLAdcbB6SWMd1yPSdZvV8W1O01ICzCCRlZDHiv4VrNUgnPUfbQ==", - "dev": true, - "dependencies": { - "app-builder-lib": "24.9.1", - "builder-util": "24.8.1", - "builder-util-runtime": "9.2.3", - "fs-extra": "^10.1.0", - "iconv-lite": "^0.6.2", - "js-yaml": "^4.1.0" - }, - "optionalDependencies": { - "dmg-license": "^1.0.11" - } - }, - "node_modules/dmg-builder/node_modules/fs-extra": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", - "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", - "dev": true, - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/dmg-builder/node_modules/jsonfile": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", - "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", - "dev": true, - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/dmg-builder/node_modules/universalify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", - "dev": true, - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/dmg-license": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/dmg-license/-/dmg-license-1.0.11.tgz", - "integrity": "sha512-ZdzmqwKmECOWJpqefloC5OJy1+WZBBse5+MR88z9g9Zn4VY+WYUkAyojmhzJckH5YbbZGcYIuGAkY5/Ys5OM2Q==", - "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "dependencies": { - "@types/plist": "^3.0.1", - "@types/verror": "^1.10.3", - "ajv": "^6.10.0", - "crc": "^3.8.0", - "iconv-corefoundation": "^1.1.7", - "plist": "^3.0.4", - "smart-buffer": "^4.0.2", - "verror": "^1.10.0" - }, - "bin": { - "dmg-license": "bin/dmg-license.js" - }, - "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==", - "dev": true, - "dependencies": { - "esutils": "^2.0.2" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/dotenv": { - "version": "9.0.2", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-9.0.2.tgz", - "integrity": "sha512-I9OvvrHp4pIARv4+x9iuewrWycX6CcZtoAu1XrzPxc5UygMJXJZYmBsynku8IkrJwgypE5DGNjDPmPRhDCptUg==", - "dev": true, - "engines": { - "node": ">=10" - } - }, - "node_modules/dotenv-expand": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-5.1.0.tgz", - "integrity": "sha512-YXQl1DSa4/PQyRfgrv6aoNjhasp/p4qs9FjJ4q4cQk+8m4r6k4ZSiEyytKG8f8W9gi8WsQtIObNmKd+tMzNTmA==", - "dev": true - }, - "node_modules/eastasianwidth": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", - "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", - "dev": true - }, - "node_modules/ejs": { - "version": "3.1.9", - "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.9.tgz", - "integrity": "sha512-rC+QVNMJWv+MtPgkt0y+0rVEIdbtxVADApW9JXrUVlzHetgcyczP/E7DJmWJ4fJCZF2cPcBk0laWO9ZHMG3DmQ==", - "dev": true, - "dependencies": { - "jake": "^10.8.5" - }, - "bin": { - "ejs": "bin/cli.js" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/electron": { - "version": "28.2.0", - "resolved": "https://registry.npmjs.org/electron/-/electron-28.2.0.tgz", - "integrity": "sha512-22SylXQQ9IHtwLw4D+Z4Si7OUpeDtpHfJVTjy3yv53iLg5zJKKPOCWT4ZwgYGHQZ0eldyBrYBHF/P9FPd2CcVQ==", - "hasInstallScript": true, - "dependencies": { - "@electron/get": "^2.0.0", - "@types/node": "^18.11.18", - "extract-zip": "^2.0.1" - }, - "bin": { - "electron": "cli.js" - }, - "engines": { - "node": ">= 12.20.55" - } - }, - "node_modules/electron-builder": { - "version": "24.9.1", - "resolved": "https://registry.npmjs.org/electron-builder/-/electron-builder-24.9.1.tgz", - "integrity": "sha512-v7BuakDuY6sKMUYM8mfQGrwyjBpZ/ObaqnenU0H+igEL10nc6ht049rsCw2HghRBdEwJxGIBuzs3jbEhNaMDmg==", - "dev": true, - "dependencies": { - "app-builder-lib": "24.9.1", - "builder-util": "24.8.1", - "builder-util-runtime": "9.2.3", - "chalk": "^4.1.2", - "dmg-builder": "24.9.1", - "fs-extra": "^10.1.0", - "is-ci": "^3.0.0", - "lazy-val": "^1.0.5", - "read-config-file": "6.3.2", - "simple-update-notifier": "2.0.0", - "yargs": "^17.6.2" - }, - "bin": { - "electron-builder": "cli.js", - "install-app-deps": "install-app-deps.js" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/electron-builder/node_modules/fs-extra": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", - "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", - "dev": true, - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/electron-builder/node_modules/jsonfile": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", - "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", - "dev": true, - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/electron-builder/node_modules/universalify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", - "dev": true, - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/electron-publish": { - "version": "24.8.1", - "resolved": "https://registry.npmjs.org/electron-publish/-/electron-publish-24.8.1.tgz", - "integrity": "sha512-IFNXkdxMVzUdweoLJNXSupXkqnvgbrn3J4vognuOY06LaS/m0xvfFYIf+o1CM8if6DuWYWoQFKPcWZt/FUjZPw==", - "dev": true, - "dependencies": { - "@types/fs-extra": "^9.0.11", - "builder-util": "24.8.1", - "builder-util-runtime": "9.2.3", - "chalk": "^4.1.2", - "fs-extra": "^10.1.0", - "lazy-val": "^1.0.5", - "mime": "^2.5.2" - } - }, - "node_modules/electron-publish/node_modules/fs-extra": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", - "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", - "dev": true, - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/electron-publish/node_modules/jsonfile": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", - "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", - "dev": true, - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/electron-publish/node_modules/universalify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", - "dev": true, - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/electron-to-chromium": { - "version": "1.4.648", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.648.tgz", - "integrity": "sha512-EmFMarXeqJp9cUKu/QEciEApn0S/xRcpZWuAm32U7NgoZCimjsilKXHRO9saeEW55eHZagIDg6XTUOv32w9pjg==", - "dev": true - }, - "node_modules/electron-vite": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/electron-vite/-/electron-vite-2.0.0.tgz", - "integrity": "sha512-EQiuPVSwJQRPGbZiVrCsCFMDVfNyNtSpDiyUV4fNwPKNadTjBfYMOudnLUpSeBTocKUZYf58SXSzSCAkNg0GTQ==", - "dev": true, - "dependencies": { - "@babel/core": "^7.23.5", - "@babel/plugin-transform-arrow-functions": "^7.23.3", - "cac": "^6.7.14", - "esbuild": "^0.19.8", - "magic-string": "^0.30.5", - "picocolors": "^1.0.0" - }, - "bin": { - "electron-vite": "bin/electron-vite.js" - }, - "engines": { - "node": "^18.0.0 || >=20.0.0" - }, - "peerDependencies": { - "@swc/core": "^1.0.0", - "vite": "^4.0.0 || ^5.0.0" - }, - "peerDependenciesMeta": { - "@swc/core": { - "optional": true - } - } - }, - "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==", - "dev": true - }, - "node_modules/end-of-stream": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", - "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", - "dependencies": { - "once": "^1.4.0" - } - }, - "node_modules/entities": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", - "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", - "dev": true, - "engines": { - "node": ">=0.12" - }, - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } - }, - "node_modules/env-paths": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", - "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", - "engines": { - "node": ">=6" - } - }, - "node_modules/err-code": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", - "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", - "dev": true - }, - "node_modules/es6-error": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", - "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==", - "optional": true - }, - "node_modules/esbuild": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.12.tgz", - "integrity": "sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg==", - "dev": true, - "hasInstallScript": true, - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=12" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.19.12", - "@esbuild/android-arm": "0.19.12", - "@esbuild/android-arm64": "0.19.12", - "@esbuild/android-x64": "0.19.12", - "@esbuild/darwin-arm64": "0.19.12", - "@esbuild/darwin-x64": "0.19.12", - "@esbuild/freebsd-arm64": "0.19.12", - "@esbuild/freebsd-x64": "0.19.12", - "@esbuild/linux-arm": "0.19.12", - "@esbuild/linux-arm64": "0.19.12", - "@esbuild/linux-ia32": "0.19.12", - "@esbuild/linux-loong64": "0.19.12", - "@esbuild/linux-mips64el": "0.19.12", - "@esbuild/linux-ppc64": "0.19.12", - "@esbuild/linux-riscv64": "0.19.12", - "@esbuild/linux-s390x": "0.19.12", - "@esbuild/linux-x64": "0.19.12", - "@esbuild/netbsd-x64": "0.19.12", - "@esbuild/openbsd-x64": "0.19.12", - "@esbuild/sunos-x64": "0.19.12", - "@esbuild/win32-arm64": "0.19.12", - "@esbuild/win32-ia32": "0.19.12", - "@esbuild/win32-x64": "0.19.12" - } - }, - "node_modules/escalade": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", - "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "devOptional": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint": { - "version": "8.56.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.56.0.tgz", - "integrity": "sha512-Go19xM6T9puCOWntie1/P997aXxFsOi37JIHRWI514Hc6ZnaHGKY9xFhrU65RT6CcBEzZoGG1e6Nq+DT04ZtZQ==", - "dev": true, - "dependencies": { - "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.6.1", - "@eslint/eslintrc": "^2.1.4", - "@eslint/js": "8.56.0", - "@humanwhocodes/config-array": "^0.11.13", - "@humanwhocodes/module-importer": "^1.0.1", - "@nodelib/fs.walk": "^1.2.8", - "@ungap/structured-clone": "^1.2.0", - "ajv": "^6.12.4", - "chalk": "^4.0.0", - "cross-spawn": "^7.0.2", - "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", - "esutils": "^2.0.2", - "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^6.0.1", - "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" - }, - "bin": { - "eslint": "bin/eslint.js" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint-config-prettier": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz", - "integrity": "sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==", - "dev": true, - "bin": { - "eslint-config-prettier": "bin/cli.js" - }, - "peerDependencies": { - "eslint": ">=7.0.0" - } - }, - "node_modules/eslint-plugin-prettier": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.1.3.tgz", - "integrity": "sha512-C9GCVAs4Eq7ZC/XFQHITLiHJxQngdtraXaM+LoUFoFp/lHNl2Zn8f3WQbe9HvTBBQ9YnKFB0/2Ajdqwo5D1EAw==", - "dev": true, - "dependencies": { - "prettier-linter-helpers": "^1.0.0", - "synckit": "^0.8.6" - }, - "engines": { - "node": "^14.18.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint-plugin-prettier" - }, - "peerDependencies": { - "@types/eslint": ">=8.0.0", - "eslint": ">=8.0.0", - "eslint-config-prettier": "*", - "prettier": ">=3.0.0" - }, - "peerDependenciesMeta": { - "@types/eslint": { - "optional": true - }, - "eslint-config-prettier": { - "optional": true - } - } - }, - "node_modules/eslint-plugin-vue": { - "version": "9.20.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-9.20.1.tgz", - "integrity": "sha512-GyCs8K3lkEvoyC1VV97GJhP1SvqsKCiWGHnbn0gVUYiUhaH2+nB+Dv1uekv1THFMPbBfYxukrzQdltw950k+LQ==", - "dev": true, - "dependencies": { - "@eslint-community/eslint-utils": "^4.4.0", - "natural-compare": "^1.4.0", - "nth-check": "^2.1.1", - "postcss-selector-parser": "^6.0.13", - "semver": "^7.5.4", - "vue-eslint-parser": "^9.4.0", - "xml-name-validator": "^4.0.0" - }, - "engines": { - "node": "^14.17.0 || >=16.0.0" - }, - "peerDependencies": { - "eslint": "^6.2.0 || ^7.0.0 || ^8.0.0" - } - }, - "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==", - "dev": true, - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint-visitor-keys": { - "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==", - "dev": true, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "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==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/eslint/node_modules/globals": { - "version": "13.24.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", - "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", - "dev": true, - "dependencies": { - "type-fest": "^0.20.2" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "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==", - "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/eslint/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==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "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==", - "dev": true, - "dependencies": { - "acorn": "^8.9.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^3.4.1" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.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==", - "dev": true, - "dependencies": { - "estraverse": "^5.1.0" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/esrecurse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", - "dev": true, - "dependencies": { - "estraverse": "^5.2.0" - }, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/estree-walker": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", - "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", - "dev": true - }, - "node_modules/esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/extract-zip": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", - "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", - "dependencies": { - "debug": "^4.1.1", - "get-stream": "^5.1.0", - "yauzl": "^2.10.0" - }, - "bin": { - "extract-zip": "cli.js" - }, - "engines": { - "node": ">= 10.17.0" - }, - "optionalDependencies": { - "@types/yauzl": "^2.9.1" - } - }, - "node_modules/extsprintf": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.4.1.tgz", - "integrity": "sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA==", - "dev": true, - "engines": [ - "node >=0.6.0" - ], - "optional": true - }, - "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==", - "dev": true - }, - "node_modules/fast-diff": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz", - "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", - "dev": true - }, - "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==", - "dev": true, - "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" - }, - "engines": { - "node": ">=8.6.0" - } - }, - "node_modules/fast-glob/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==", - "dev": true, - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "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==", - "dev": true - }, - "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==", - "dev": true - }, - "node_modules/fastq": { - "version": "1.17.0", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.0.tgz", - "integrity": "sha512-zGygtijUMT7jnk3h26kUms3BkSDp4IfIKjmnqI2tvx6nuBfiF1UqOxbnLfzdv+apBy+53oaImsKtMw/xYbW+1w==", - "dev": true, - "dependencies": { - "reusify": "^1.0.4" - } - }, - "node_modules/fd-slicer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", - "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", - "dependencies": { - "pend": "~1.2.0" - } - }, - "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==", - "dev": true, - "dependencies": { - "flat-cache": "^3.0.4" - }, - "engines": { - "node": "^10.12.0 || >=12.0.0" - } - }, - "node_modules/filelist": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", - "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==", - "dev": true, - "dependencies": { - "minimatch": "^5.0.1" - } - }, - "node_modules/filelist/node_modules/minimatch": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", - "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", - "dev": true, - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", - "dev": true, - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/find-up": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", - "dev": true, - "dependencies": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "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==", - "dev": true, - "dependencies": { - "flatted": "^3.2.9", - "keyv": "^4.5.3", - "rimraf": "^3.0.2" - }, - "engines": { - "node": "^10.12.0 || >=12.0.0" - } - }, - "node_modules/flatted": { - "version": "3.2.9", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.9.tgz", - "integrity": "sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==", - "dev": true - }, - "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==", - "dev": true, - "dependencies": { - "cross-spawn": "^7.0.0", - "signal-exit": "^4.0.1" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/form-data": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", - "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", - "dev": true, - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/fs-extra": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", - "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^4.0.0", - "universalify": "^0.1.0" - }, - "engines": { - "node": ">=6 <7 || >=8" - } - }, - "node_modules/fs-minipass": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", - "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", - "dev": true, - "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/fs-minipass/node_modules/minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "dev": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/fs-minipass/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - }, - "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==", - "dev": true - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "optional": true, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/gensync": { - "version": "1.0.0-beta.2", - "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", - "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "dev": true, - "engines": { - "node": "6.* || 8.* || >= 10.*" - } - }, - "node_modules/get-intrinsic": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.2.tgz", - "integrity": "sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA==", - "optional": true, - "dependencies": { - "function-bind": "^1.1.2", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3", - "hasown": "^2.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-stream": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", - "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", - "dependencies": { - "pump": "^3.0.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "dev": true, - "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==", - "dev": true, - "dependencies": { - "is-glob": "^4.0.3" - }, - "engines": { - "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==", - "dev": true, - "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==", - "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/global-agent": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/global-agent/-/global-agent-3.0.0.tgz", - "integrity": "sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==", - "optional": true, - "dependencies": { - "boolean": "^3.0.1", - "es6-error": "^4.1.1", - "matcher": "^3.0.0", - "roarr": "^2.15.3", - "semver": "^7.3.2", - "serialize-error": "^7.0.1" - }, - "engines": { - "node": ">=10.0" - } - }, - "node_modules/globals": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/globalthis": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.3.tgz", - "integrity": "sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA==", - "optional": true, - "dependencies": { - "define-properties": "^1.1.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/globby": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", - "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", - "dev": true, - "dependencies": { - "array-union": "^2.1.0", - "dir-glob": "^3.0.1", - "fast-glob": "^3.2.9", - "ignore": "^5.2.0", - "merge2": "^1.4.1", - "slash": "^3.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/gopd": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", - "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", - "optional": true, - "dependencies": { - "get-intrinsic": "^1.1.3" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/got": { - "version": "11.8.6", - "resolved": "https://registry.npmjs.org/got/-/got-11.8.6.tgz", - "integrity": "sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==", - "dependencies": { - "@sindresorhus/is": "^4.0.0", - "@szmarczak/http-timer": "^4.0.5", - "@types/cacheable-request": "^6.0.1", - "@types/responselike": "^1.0.0", - "cacheable-lookup": "^5.0.3", - "cacheable-request": "^7.0.2", - "decompress-response": "^6.0.0", - "http2-wrapper": "^1.0.0-beta.5.2", - "lowercase-keys": "^2.0.0", - "p-cancelable": "^2.0.0", - "responselike": "^2.0.0" - }, - "engines": { - "node": ">=10.19.0" - }, - "funding": { - "url": "https://github.com/sindresorhus/got?sponsor=1" - } - }, - "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==" - }, - "node_modules/graphemer": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", - "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", - "dev": true - }, - "node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/has-property-descriptors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.1.tgz", - "integrity": "sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==", - "optional": true, - "dependencies": { - "get-intrinsic": "^1.2.2" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", - "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==", - "optional": true, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-symbols": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", - "optional": true, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/hasown": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz", - "integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==", - "optional": true, - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/he": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", - "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", - "dev": true, - "bin": { - "he": "bin/he" - } - }, - "node_modules/hosted-git-info": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz", - "integrity": "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==", - "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/hosted-git-info/node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/hosted-git-info/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - }, - "node_modules/http-cache-semantics": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", - "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==" - }, - "node_modules/http-proxy-agent": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", - "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", - "dev": true, - "dependencies": { - "@tootallnate/once": "2", - "agent-base": "6", - "debug": "4" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/http2-wrapper": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz", - "integrity": "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==", - "dependencies": { - "quick-lru": "^5.1.1", - "resolve-alpn": "^1.0.0" - }, - "engines": { - "node": ">=10.19.0" - } - }, - "node_modules/https-proxy-agent": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", - "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", - "dev": true, - "dependencies": { - "agent-base": "6", - "debug": "4" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/iconv-corefoundation": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/iconv-corefoundation/-/iconv-corefoundation-1.1.7.tgz", - "integrity": "sha512-T10qvkw0zz4wnm560lOEg0PovVqUXuOFhhHAkixw8/sycy7TJt7v/RrkEKEQnAw2viPSJu6iAkErxnzR0g8PpQ==", - "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "dependencies": { - "cli-truncate": "^2.1.0", - "node-addon-api": "^1.6.3" - }, - "engines": { - "node": "^8.11.2 || >=10" - } - }, - "node_modules/iconv-lite": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "dev": true, - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/ieee754": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "optional": true - }, - "node_modules/ignore": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.0.tgz", - "integrity": "sha512-g7dmpshy+gD7mh88OC9NwSGTKoc3kyLAZQRU1mt53Aw/vnvfXnbC+F/7F7QoYVKbV+KNvJx8wArewKy1vXMtlg==", - "dev": true, - "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==", - "dev": true, - "dependencies": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", - "dev": true, - "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==", - "dev": true, - "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==", - "dev": true - }, - "node_modules/is-ci": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-3.0.1.tgz", - "integrity": "sha512-ZYvCgrefwqoQ6yTyYUbQu64HsITZ3NfKX1lzaEYdkTDcfKzzCI/wthRRYKkdjHKFVgNiXKAKm65Zo1pk2as/QQ==", - "dev": true, - "dependencies": { - "ci-info": "^3.2.0" - }, - "bin": { - "is-ci": "bin.js" - } - }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "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==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, - "engines": { - "node": ">=0.12.0" - } - }, - "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==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/isbinaryfile": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-5.0.0.tgz", - "integrity": "sha512-UDdnyGvMajJUWCkib7Cei/dvyJrrvo4FIrsvSFWdPpXSUorzXrDJ0S+X5Q4ZlasfPjca4yqCNNsjbCeiy8FFeg==", - "dev": true, - "engines": { - "node": ">= 14.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/gjtorikian/" - } - }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true - }, - "node_modules/jackspeak": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.6.tgz", - "integrity": "sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==", - "dev": true, - "dependencies": { - "@isaacs/cliui": "^8.0.2" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - }, - "optionalDependencies": { - "@pkgjs/parseargs": "^0.11.0" - } - }, - "node_modules/jake": { - "version": "10.8.7", - "resolved": "https://registry.npmjs.org/jake/-/jake-10.8.7.tgz", - "integrity": "sha512-ZDi3aP+fG/LchyBzUM804VjddnwfSfsdeYkwt8NcbKRvo4rFkjhs456iLFn3k2ZUWvNe4i48WACDbza8fhq2+w==", - "dev": true, - "dependencies": { - "async": "^3.2.3", - "chalk": "^4.0.2", - "filelist": "^1.0.4", - "minimatch": "^3.1.2" - }, - "bin": { - "jake": "bin/cli.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/jake/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==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/jake/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" - }, - "engines": { - "node": "*" - } - }, - "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==", - "dev": true - }, - "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==", - "dev": true, - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/jsesc": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", - "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", - "dev": true, - "bin": { - "jsesc": "bin/jsesc" - }, - "engines": { - "node": ">=4" - } - }, - "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==" - }, - "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==", - "dev": true - }, - "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==", - "dev": true - }, - "node_modules/json-stringify-safe": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", - "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", - "optional": true - }, - "node_modules/json5": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", - "dev": true, - "bin": { - "json5": "lib/cli.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/jsonfile": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", - "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/keyv": { - "version": "4.5.4", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", - "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", - "dependencies": { - "json-buffer": "3.0.1" - } - }, - "node_modules/lazy-val": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/lazy-val/-/lazy-val-1.0.5.tgz", - "integrity": "sha512-0/BnGCCfyUMkBpeDgWihanIAF9JmZhHBgUhEqzvf+adhNGLoP6TaiI5oF8oyb3I45P+PcnrqihSf01M0l0G5+Q==", - "dev": true - }, - "node_modules/levn": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", - "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", - "dev": true, - "dependencies": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "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==", - "dev": true, - "dependencies": { - "p-locate": "^5.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "dev": true - }, - "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==", - "dev": true - }, - "node_modules/lowercase-keys": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", - "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==", - "engines": { - "node": ">=8" - } - }, - "node_modules/lru-cache": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", - "dev": true, - "dependencies": { - "yallist": "^3.0.2" - } - }, - "node_modules/magic-string": { - "version": "0.30.5", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.5.tgz", - "integrity": "sha512-7xlpfBaQaP/T6Vh8MO/EqXSW5En6INHEvEXQiuff7Gku0PWjU3uf6w/j9o7O+SpB5fOAkrI5HeoNgwjEO0pFsA==", - "dev": true, - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.4.15" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/matcher": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz", - "integrity": "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==", - "optional": true, - "dependencies": { - "escape-string-regexp": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/merge2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "dev": true, - "engines": { - "node": ">= 8" - } - }, - "node_modules/micromatch": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", - "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", - "dev": true, - "dependencies": { - "braces": "^3.0.2", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, - "node_modules/mime": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", - "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", - "dev": true, - "bin": { - "mime": "cli.js" - }, - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "dev": true, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dev": true, - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mimic-response": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", - "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==", - "engines": { - "node": ">=4" - } - }, - "node_modules/minimatch": { - "version": "9.0.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", - "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", - "dev": true, - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/minimist": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "dev": true, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/minipass": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", - "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/minizlib": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", - "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", - "dev": true, - "dependencies": { - "minipass": "^3.0.0", - "yallist": "^4.0.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/minizlib/node_modules/minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "dev": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/minizlib/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - }, - "node_modules/mkdirp": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", - "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", - "dev": true, - "bin": { - "mkdirp": "bin/cmd.js" - }, - "engines": { - "node": ">=10" - } - }, - "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/muggle-string": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/muggle-string/-/muggle-string-0.3.1.tgz", - "integrity": "sha512-ckmWDJjphvd/FvZawgygcUeQCxzvohjFO5RxTjj4eq8kw359gFF3E1brjfI+viLMxss5JrHTDRHZvu2/tuy0Qg==", - "dev": true - }, - "node_modules/nanoid": { - "version": "3.3.7", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", - "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, - "node_modules/natural-compare": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", - "dev": true - }, - "node_modules/node-addon-api": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-1.7.2.tgz", - "integrity": "sha512-ibPK3iA+vaY1eEjESkQkM0BbCqFOaZMiXRTtdB0u7b4djtY6JnsjvPdUHVMg6xQt3B8fpTTWHI9A+ADjM9frzg==", - "dev": true, - "optional": true - }, - "node_modules/node-releases": { - "version": "2.0.14", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", - "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==", - "dev": true - }, - "node_modules/normalize-url": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", - "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/nth-check": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", - "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", - "dev": true, - "dependencies": { - "boolbase": "^1.0.0" - }, - "funding": { - "url": "https://github.com/fb55/nth-check?sponsor=1" - } - }, - "node_modules/object-keys": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", - "optional": true, - "engines": { - "node": ">= 0.4" - } - }, - "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==", - "dev": true, - "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" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/p-cancelable": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz", - "integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==", - "engines": { - "node": ">=8" - } - }, - "node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, - "dependencies": { - "yocto-queue": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-locate": { - "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==", - "dev": true, - "dependencies": { - "p-limit": "^3.0.2" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/parent-module": { - "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==", - "dev": true, - "dependencies": { - "callsites": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/path-browserify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", - "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", - "dev": true - }, - "node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true, - "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==", - "dev": true, - "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==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/path-scurry": { - "version": "1.10.1", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.10.1.tgz", - "integrity": "sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ==", - "dev": true, - "dependencies": { - "lru-cache": "^9.1.1 || ^10.0.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==", - "dev": true, - "engines": { - "node": "14 || >=16.14" - } - }, - "node_modules/path-type": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", - "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/pend": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", - "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==" - }, - "node_modules/picocolors": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", - "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", - "dev": true - }, - "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/plist": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/plist/-/plist-3.1.0.tgz", - "integrity": "sha512-uysumyrvkUX0rX/dEVqt8gC3sTBzd4zoWfLeS29nb53imdaXVvLINYXTI2GNqzaMuvacNx4uJQ8+b3zXR0pkgQ==", - "dev": true, - "dependencies": { - "@xmldom/xmldom": "^0.8.8", - "base64-js": "^1.5.1", - "xmlbuilder": "^15.1.1" - }, - "engines": { - "node": ">=10.4.0" - } - }, - "node_modules/postcss": { - "version": "8.4.33", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.33.tgz", - "integrity": "sha512-Kkpbhhdjw2qQs2O2DGX+8m5OVqEcbB9HRBvuYM9pgrjEFUg30A9LmXNlTAUj4S9kgtGyrMbTzVjH7E+s5Re2yg==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "dependencies": { - "nanoid": "^3.3.7", - "picocolors": "^1.0.0", - "source-map-js": "^1.0.2" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, - "node_modules/postcss-selector-parser": { - "version": "6.0.15", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.15.tgz", - "integrity": "sha512-rEYkQOMUCEMhsKbK66tbEU9QVIxbhN18YiniAwA7XQYTVBqrBy+P2p5JcdqsHgKM2zWylp8d7J6eszocfds5Sw==", - "dev": true, - "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - }, - "engines": { - "node": ">=4" - } - }, - "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==", - "dev": true, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/prettier": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.4.tgz", - "integrity": "sha512-FWu1oLHKCrtpO1ypU6J0SbK2d9Ckwysq6bHj/uaCP26DxrPpppCLQRGVuqAxSTvhF00AcvDRyYrLNW7ocBhFFQ==", - "dev": true, - "bin": { - "prettier": "bin/prettier.cjs" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/prettier/prettier?sponsor=1" - } - }, - "node_modules/prettier-linter-helpers": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz", - "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==", - "dev": true, - "dependencies": { - "fast-diff": "^1.1.2" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/progress": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", - "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/promise-retry": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", - "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", - "dev": true, - "dependencies": { - "err-code": "^2.0.2", - "retry": "^0.12.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/pump": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", - "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", - "dependencies": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, - "node_modules/punycode": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/queue-microtask": { - "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", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/quick-lru": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", - "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/read-config-file": { - "version": "6.3.2", - "resolved": "https://registry.npmjs.org/read-config-file/-/read-config-file-6.3.2.tgz", - "integrity": "sha512-M80lpCjnE6Wt6zb98DoW8WHR09nzMSpu8XHtPkiTHrJ5Az9CybfeQhTJ8D7saeBHpGhLPIVyA8lcL6ZmdKwY6Q==", - "dev": true, - "dependencies": { - "config-file-ts": "^0.2.4", - "dotenv": "^9.0.2", - "dotenv-expand": "^5.1.0", - "js-yaml": "^4.1.0", - "json5": "^2.2.0", - "lazy-val": "^1.0.4" - }, - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/resolve-alpn": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz", - "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==" - }, - "node_modules/resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/responselike": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.1.tgz", - "integrity": "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==", - "dependencies": { - "lowercase-keys": "^2.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/retry": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", - "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", - "dev": true, - "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==", - "dev": true, - "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==", - "dev": true, - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/roarr": { - "version": "2.15.4", - "resolved": "https://registry.npmjs.org/roarr/-/roarr-2.15.4.tgz", - "integrity": "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==", - "optional": true, - "dependencies": { - "boolean": "^3.0.1", - "detect-node": "^2.0.4", - "globalthis": "^1.0.1", - "json-stringify-safe": "^5.0.1", - "semver-compare": "^1.0.0", - "sprintf-js": "^1.1.2" - }, - "engines": { - "node": ">=8.0" - } - }, - "node_modules/rollup": { - "version": "4.9.6", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.9.6.tgz", - "integrity": "sha512-05lzkCS2uASX0CiLFybYfVkwNbKZG5NFQ6Go0VWyogFTXXbR039UVsegViTntkk4OglHBdF54ccApXRRuXRbsg==", - "dev": true, - "dependencies": { - "@types/estree": "1.0.5" - }, - "bin": { - "rollup": "dist/bin/rollup" - }, - "engines": { - "node": ">=18.0.0", - "npm": ">=8.0.0" - }, - "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.9.6", - "@rollup/rollup-android-arm64": "4.9.6", - "@rollup/rollup-darwin-arm64": "4.9.6", - "@rollup/rollup-darwin-x64": "4.9.6", - "@rollup/rollup-linux-arm-gnueabihf": "4.9.6", - "@rollup/rollup-linux-arm64-gnu": "4.9.6", - "@rollup/rollup-linux-arm64-musl": "4.9.6", - "@rollup/rollup-linux-riscv64-gnu": "4.9.6", - "@rollup/rollup-linux-x64-gnu": "4.9.6", - "@rollup/rollup-linux-x64-musl": "4.9.6", - "@rollup/rollup-win32-arm64-msvc": "4.9.6", - "@rollup/rollup-win32-ia32-msvc": "4.9.6", - "@rollup/rollup-win32-x64-msvc": "4.9.6", - "fsevents": "~2.3.2" - } - }, - "node_modules/run-parallel": { - "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", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "dependencies": { - "queue-microtask": "^1.2.2" - } - }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "dev": true - }, - "node_modules/sanitize-filename": { - "version": "1.6.3", - "resolved": "https://registry.npmjs.org/sanitize-filename/-/sanitize-filename-1.6.3.tgz", - "integrity": "sha512-y/52Mcy7aw3gRm7IrcGDFx/bCk4AhRh2eI9luHOQM86nZsqwiRkkq2GekHXBBD+SmPidc8i2PqtYZl+pWJ8Oeg==", - "dev": true, - "dependencies": { - "truncate-utf8-bytes": "^1.0.0" - } - }, - "node_modules/sax": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/sax/-/sax-1.3.0.tgz", - "integrity": "sha512-0s+oAmw9zLl1V1cS9BtZN7JAd0cW5e0QH4W3LWEK6a4LaLEA2OTpGYWDY+6XasBLtz6wkm3u1xRw95mRuJ59WA==", - "dev": true - }, - "node_modules/semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", - "devOptional": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/semver-compare": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz", - "integrity": "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==", - "optional": true - }, - "node_modules/semver/node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "devOptional": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/semver/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "devOptional": true - }, - "node_modules/serialize-error": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-7.0.1.tgz", - "integrity": "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==", - "optional": true, - "dependencies": { - "type-fest": "^0.13.1" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "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==", - "dev": true, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/simple-update-notifier": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", - "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", - "dev": true, - "dependencies": { - "semver": "^7.5.3" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/slice-ansi": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-3.0.0.tgz", - "integrity": "sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==", - "dev": true, - "optional": true, - "dependencies": { - "ansi-styles": "^4.0.0", - "astral-regex": "^2.0.0", - "is-fullwidth-code-point": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/smart-buffer": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", - "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", - "dev": true, - "optional": true, - "engines": { - "node": ">= 6.0.0", - "npm": ">= 3.0.0" - } - }, - "node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/source-map-js": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", - "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/source-map-support": { - "version": "0.5.21", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", - "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", - "dev": true, - "dependencies": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" - } - }, - "node_modules/sprintf-js": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", - "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", - "optional": true - }, - "node_modules/stat-mode": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/stat-mode/-/stat-mode-1.0.0.tgz", - "integrity": "sha512-jH9EhtKIjuXZ2cWxmXS8ZP80XyC3iasQxMDV8jzhNJpfDb7VbQLVW4Wvsxz9QZvzV+G4YoSfBUVKDOyxLzi/sg==", - "dev": true, - "engines": { - "node": ">= 6" - } - }, - "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==", - "dev": true, - "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": { - "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==", - "dev": true, - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "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==", - "dev": true, - "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==", - "dev": true, - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-json-comments": { - "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==", - "dev": true, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/sumchecker": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/sumchecker/-/sumchecker-3.0.1.tgz", - "integrity": "sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg==", - "dependencies": { - "debug": "^4.1.0" - }, - "engines": { - "node": ">= 8.0" - } - }, - "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==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/synckit": { - "version": "0.8.8", - "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.8.8.tgz", - "integrity": "sha512-HwOKAP7Wc5aRGYdKH+dw0PRRpbO841v2DENBtjnR5HFWoiNByAl7vrx3p0G/rCyYXQsrxqtX48TImFtPcIHSpQ==", - "dev": true, - "dependencies": { - "@pkgr/core": "^0.1.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": "^14.18.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/unts" - } - }, - "node_modules/tar": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.0.tgz", - "integrity": "sha512-/Wo7DcT0u5HUV486xg675HtjNd3BXZ6xDbzsCUZPt5iw8bTQ63bP0Raut3mvro9u+CUyq7YQd8Cx55fsZXxqLQ==", - "dev": true, - "dependencies": { - "chownr": "^2.0.0", - "fs-minipass": "^2.0.0", - "minipass": "^5.0.0", - "minizlib": "^2.1.1", - "mkdirp": "^1.0.3", - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/tar/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - }, - "node_modules/temp-file": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/temp-file/-/temp-file-3.4.0.tgz", - "integrity": "sha512-C5tjlC/HCtVUOi3KWVokd4vHVViOmGjtLwIh4MuzPo/nMYTV/p1urt3RnMz2IWXDdKEGJH3k5+KPxtqRsUYGtg==", - "dev": true, - "dependencies": { - "async-exit-hook": "^2.0.1", - "fs-extra": "^10.0.0" - } - }, - "node_modules/temp-file/node_modules/fs-extra": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", - "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", - "dev": true, - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/temp-file/node_modules/jsonfile": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", - "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", - "dev": true, - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/temp-file/node_modules/universalify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", - "dev": true, - "engines": { - "node": ">= 10.0.0" - } - }, - "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==", - "dev": true - }, - "node_modules/tmp": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz", - "integrity": "sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==", - "dev": true, - "dependencies": { - "rimraf": "^3.0.0" - }, - "engines": { - "node": ">=8.17.0" - } - }, - "node_modules/tmp-promise": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/tmp-promise/-/tmp-promise-3.0.3.tgz", - "integrity": "sha512-RwM7MoPojPxsOBYnyd2hy0bxtIlVrihNs9pj5SUvY8Zz1sQcQG2tG1hSr8PDxfgEB8RNKDhqbIlroIarSNDNsQ==", - "dev": true, - "dependencies": { - "tmp": "^0.2.0" - } - }, - "node_modules/to-fast-properties": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", - "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "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, - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, - "node_modules/truncate-utf8-bytes": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz", - "integrity": "sha512-95Pu1QXQvruGEhv62XCMO3Mm90GscOCClvrIUwCM0PYOXK3kaF3l3sIHxx71ThJfcbM2O5Au6SO3AWCSEfW4mQ==", - "dev": true, - "dependencies": { - "utf8-byte-length": "^1.0.1" - } - }, - "node_modules/ts-api-utils": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.0.3.tgz", - "integrity": "sha512-wNMeqtMz5NtwpT/UZGY5alT+VoKdSsOOP/kqHFcUW1P/VRhH2wJ48+DN2WwUliNbQ976ETwDL0Ifd2VVvgonvg==", - "dev": true, - "engines": { - "node": ">=16.13.0" - }, - "peerDependencies": { - "typescript": ">=4.2.0" - } - }, - "node_modules/tslib": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", - "dev": true - }, - "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==", - "dev": true, - "dependencies": { - "prelude-ls": "^1.2.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/type-fest": { - "version": "0.13.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz", - "integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==", - "optional": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/typescript": { - "version": "5.3.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz", - "integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==", - "dev": true, - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/undici-types": { - "version": "5.26.5", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" - }, - "node_modules/universalify": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", - "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", - "engines": { - "node": ">= 4.0.0" - } - }, - "node_modules/update-browserslist-db": { - "version": "1.0.13", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz", - "integrity": "sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "dependencies": { - "escalade": "^3.1.1", - "picocolors": "^1.0.0" - }, - "bin": { - "update-browserslist-db": "cli.js" - }, - "peerDependencies": { - "browserslist": ">= 4.21.0" - } - }, - "node_modules/uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, - "dependencies": { - "punycode": "^2.1.0" - } - }, - "node_modules/utf8-byte-length": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/utf8-byte-length/-/utf8-byte-length-1.0.4.tgz", - "integrity": "sha512-4+wkEYLBbWxqTahEsWrhxepcoVOJ+1z5PGIjPZxRkytcdSUaNjIjBM7Xn8E+pdSuV7SzvWovBFA54FO0JSoqhA==", - "dev": true - }, - "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==", - "dev": true - }, - "node_modules/verror": { - "version": "1.10.1", - "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.1.tgz", - "integrity": "sha512-veufcmxri4e3XSrT0xwfUR7kguIkaxBeosDg00yDWhk49wdwkSUrvvsm7nc75e1PUyvIeZj6nS8VQRYz2/S4Xg==", - "dev": true, - "optional": true, - "dependencies": { - "assert-plus": "^1.0.0", - "core-util-is": "1.0.2", - "extsprintf": "^1.2.0" - }, - "engines": { - "node": ">=0.6.0" - } - }, - "node_modules/vite": { - "version": "5.0.12", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.0.12.tgz", - "integrity": "sha512-4hsnEkG3q0N4Tzf1+t6NdN9dg/L3BM+q8SWgbSPnJvrgH2kgdyzfVJwbR1ic69/4uMJJ/3dqDZZE5/WwqW8U1w==", - "dev": true, - "dependencies": { - "esbuild": "^0.19.3", - "postcss": "^8.4.32", - "rollup": "^4.2.0" - }, - "bin": { - "vite": "bin/vite.js" - }, - "engines": { - "node": "^18.0.0 || >=20.0.0" - }, - "funding": { - "url": "https://github.com/vitejs/vite?sponsor=1" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" - }, - "peerDependencies": { - "@types/node": "^18.0.0 || >=20.0.0", - "less": "*", - "lightningcss": "^1.21.0", - "sass": "*", - "stylus": "*", - "sugarss": "*", - "terser": "^5.4.0" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "less": { - "optional": true - }, - "lightningcss": { - "optional": true - }, - "sass": { - "optional": true - }, - "stylus": { - "optional": true - }, - "sugarss": { - "optional": true - }, - "terser": { - "optional": true - } - } - }, - "node_modules/vue": { - "version": "3.4.15", - "resolved": "https://registry.npmjs.org/vue/-/vue-3.4.15.tgz", - "integrity": "sha512-jC0GH4KkWLWJOEQjOpkqU1bQsBwf4R1rsFtw5GQJbjHVKWDzO6P0nWWBTmjp1xSemAioDFj1jdaK1qa3DnMQoQ==", - "dev": true, - "dependencies": { - "@vue/compiler-dom": "3.4.15", - "@vue/compiler-sfc": "3.4.15", - "@vue/runtime-dom": "3.4.15", - "@vue/server-renderer": "3.4.15", - "@vue/shared": "3.4.15" - }, - "peerDependencies": { - "typescript": "*" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/vue-eslint-parser": { - "version": "9.4.2", - "resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-9.4.2.tgz", - "integrity": "sha512-Ry9oiGmCAK91HrKMtCrKFWmSFWvYkpGglCeFAIqDdr9zdXmMMpJOmUJS7WWsW7fX81h6mwHmUZCQQ1E0PkSwYQ==", - "dev": true, - "dependencies": { - "debug": "^4.3.4", - "eslint-scope": "^7.1.1", - "eslint-visitor-keys": "^3.3.0", - "espree": "^9.3.1", - "esquery": "^1.4.0", - "lodash": "^4.17.21", - "semver": "^7.3.6" - }, - "engines": { - "node": "^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/mysticatea" - }, - "peerDependencies": { - "eslint": ">=6.0.0" - } - }, - "node_modules/vue-template-compiler": { - "version": "2.7.16", - "resolved": "https://registry.npmjs.org/vue-template-compiler/-/vue-template-compiler-2.7.16.tgz", - "integrity": "sha512-AYbUWAJHLGGQM7+cNTELw+KsOG9nl2CnSv467WobS5Cv9uk3wFcnr1Etsz2sEIHEZvw1U+o9mRlEO6QbZvUPGQ==", - "dev": true, - "dependencies": { - "de-indent": "^1.0.2", - "he": "^1.2.0" - } - }, - "node_modules/vue-tsc": { - "version": "1.8.27", - "resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-1.8.27.tgz", - "integrity": "sha512-WesKCAZCRAbmmhuGl3+VrdWItEvfoFIPXOvUJkjULi+x+6G/Dy69yO3TBRJDr9eUlmsNAwVmxsNZxvHKzbkKdg==", - "dev": true, - "dependencies": { - "@volar/typescript": "~1.11.1", - "@vue/language-core": "1.8.27", - "semver": "^7.5.4" - }, - "bin": { - "vue-tsc": "bin/vue-tsc.js" - }, - "peerDependencies": { - "typescript": "*" - } - }, - "node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/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==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "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==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-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/xml-name-validator": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz", - "integrity": "sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==", - "dev": true, - "engines": { - "node": ">=12" - } - }, - "node_modules/xmlbuilder": { - "version": "15.1.1", - "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-15.1.1.tgz", - "integrity": "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==", - "dev": true, - "engines": { - "node": ">=8.0" - } - }, - "node_modules/y18n": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", - "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "dev": true, - "engines": { - "node": ">=10" - } - }, - "node_modules/yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", - "dev": true - }, - "node_modules/yargs": { - "version": "17.7.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", - "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", - "dev": true, - "dependencies": { - "cliui": "^8.0.1", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.3", - "y18n": "^5.0.5", - "yargs-parser": "^21.1.1" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/yargs-parser": { - "version": "21.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", - "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", - "dev": true, - "engines": { - "node": ">=12" - } - }, - "node_modules/yauzl": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", - "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", - "dependencies": { - "buffer-crc32": "~0.2.3", - "fd-slicer": "~1.1.0" - } - }, - "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==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - } - } -} diff --git a/installer/package.json b/installer/package.json deleted file mode 100644 index fdba6ea..0000000 --- a/installer/package.json +++ /dev/null @@ -1,45 +0,0 @@ -{ - "name": "electron-app", - "version": "1.0.0", - "description": "An Electron application with Vue and TypeScript", - "main": "./out/main/index.js", - "author": "example.com", - "homepage": "https://electron-vite.org", - "scripts": { - "typecheck:node": "npx tsc --noEmit -p tsconfig.node.json --composite false", - "typecheck:web": "npx vue-tsc --noEmit -p tsconfig.web.json --composite false", - "typecheck": "npm run typecheck:node && npm run typecheck:web", - "start": "npx electron-vite preview", - "dev": "npx electron-vite dev", - "build": "npm run typecheck && electron-vite build", - "postinstall": "npx electron-builder install-app-deps", - "build:unpack": "npm run build && electron-builder --dir", - "build:win": "npm run build && electron-builder --win", - "build:mac": "npm run build && electron-builder --mac", - "build:linux": "npm run build && electron-builder --linux" - }, - "dependencies": { - "@electron-toolkit/preload": "^3.0.0", - "@electron-toolkit/utils": "^3.0.0" - }, - "devDependencies": { - "@electron-toolkit/eslint-config": "^1.0.2", - "@electron-toolkit/eslint-config-ts": "^1.0.1", - "@electron-toolkit/tsconfig": "^1.0.1", - "@rushstack/eslint-patch": "^1.7.1", - "@types/node": "^18.19.9", - "@vitejs/plugin-vue": "^5.0.3", - "@vue/eslint-config-prettier": "^9.0.0", - "@vue/eslint-config-typescript": "^12.0.0", - "electron": "^28.2.0", - "electron-builder": "^24.9.1", - "electron-vite": "^2.0.0", - "eslint": "^8.56.0", - "eslint-plugin-vue": "^9.20.1", - "prettier": "^3.2.4", - "typescript": "^5.3.3", - "vite": "^5.0.12", - "vue": "^3.4.15", - "vue-tsc": "^1.8.27" - } -} diff --git a/installer/resources/icon.png b/installer/resources/icon.png deleted file mode 100644 index cf9e8b2..0000000 Binary files a/installer/resources/icon.png and /dev/null differ diff --git a/installer/src/main/index.ts b/installer/src/main/index.ts deleted file mode 100644 index 2299c5b..0000000 --- a/installer/src/main/index.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { app, shell, BrowserWindow, ipcMain } from 'electron' -import { join } from 'path' -import { electronApp, optimizer, is } from '@electron-toolkit/utils' -import icon from '../../resources/icon.png?asset' - -function createWindow(): void { - // Create the browser window. - const mainWindow = new BrowserWindow({ - width: 900, - height: 670, - show: false, - autoHideMenuBar: true, - ...(process.platform === 'linux' ? { icon } : {}), - webPreferences: { - preload: join(__dirname, '../preload/index.js'), - sandbox: false - } - }) - - mainWindow.on('ready-to-show', () => { - mainWindow.show() - }) - - mainWindow.webContents.setWindowOpenHandler((details) => { - shell.openExternal(details.url) - return { action: 'deny' } - }) - - // HMR for renderer base on electron-vite cli. - // Load the remote URL for development or the local html file for production. - if (is.dev && process.env['ELECTRON_RENDERER_URL']) { - mainWindow.loadURL(process.env['ELECTRON_RENDERER_URL']) - } else { - mainWindow.loadFile(join(__dirname, '../renderer/index.html')) - } -} - -// This method will be called when Electron has finished -// initialization and is ready to create browser windows. -// Some APIs can only be used after this event occurs. -app.whenReady().then(() => { - // Set app user model id for windows - electronApp.setAppUserModelId('com.electron') - - // Default open or close DevTools by F12 in development - // and ignore CommandOrControl + R in production. - // see https://github.com/alex8088/electron-toolkit/tree/master/packages/utils - app.on('browser-window-created', (_, window) => { - optimizer.watchWindowShortcuts(window) - }) - - // IPC test - // ipcMain.on('ping', () => console.log('pong')) - - createWindow() - - app.on('activate', function () { - // On macOS it's common to re-create a window in the app when the - // dock icon is clicked and there are no other windows open. - if (BrowserWindow.getAllWindows().length === 0) createWindow() - }) -}) - -// Quit when all windows are closed, except on macOS. There, it's common -// for applications and their menu bar to stay active until the user quits -// explicitly with Cmd + Q. -app.on('window-all-closed', () => { - if (process.platform !== 'darwin') { - app.quit() - } -}) - -// In this file you can include the rest of your app"s specific main process -// code. You can also put them in separate files and require them here. diff --git a/installer/src/preload/index.d.ts b/installer/src/preload/index.d.ts deleted file mode 100644 index a153669..0000000 --- a/installer/src/preload/index.d.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { ElectronAPI } from '@electron-toolkit/preload' - -declare global { - interface Window { - electron: ElectronAPI - api: unknown - } -} diff --git a/installer/src/preload/index.ts b/installer/src/preload/index.ts deleted file mode 100644 index 2d18524..0000000 --- a/installer/src/preload/index.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { contextBridge } from 'electron' -import { electronAPI } from '@electron-toolkit/preload' - -// Custom APIs for renderer -const api = {} - -// Use `contextBridge` APIs to expose Electron APIs to -// renderer only if context isolation is enabled, otherwise -// just add to the DOM global. -if (process.contextIsolated) { - try { - contextBridge.exposeInMainWorld('electron', electronAPI) - contextBridge.exposeInMainWorld('api', api) - } catch (error) { - console.error(error) - } -} else { - // @ts-ignore (define in dts) - window.electron = electronAPI - // @ts-ignore (define in dts) - window.api = api -} diff --git a/installer/src/renderer/index.html b/installer/src/renderer/index.html deleted file mode 100644 index aafc73e..0000000 --- a/installer/src/renderer/index.html +++ /dev/null @@ -1,17 +0,0 @@ - - - - - NetIgnite Agent Installer for ESP32 - - - - - -
- - - diff --git a/installer/src/renderer/src/App.vue b/installer/src/renderer/src/App.vue deleted file mode 100644 index 6fb8fe8..0000000 --- a/installer/src/renderer/src/App.vue +++ /dev/null @@ -1,26 +0,0 @@ - - - diff --git a/installer/src/renderer/src/assets/base.css b/installer/src/renderer/src/assets/base.css deleted file mode 100644 index 5ed6406..0000000 --- a/installer/src/renderer/src/assets/base.css +++ /dev/null @@ -1,67 +0,0 @@ -:root { - --ev-c-white: #ffffff; - --ev-c-white-soft: #f8f8f8; - --ev-c-white-mute: #f2f2f2; - - --ev-c-black: #1b1b1f; - --ev-c-black-soft: #222222; - --ev-c-black-mute: #282828; - - --ev-c-gray-1: #515c67; - --ev-c-gray-2: #414853; - --ev-c-gray-3: #32363f; - - --ev-c-text-1: rgba(255, 255, 245, 0.86); - --ev-c-text-2: rgba(235, 235, 245, 0.6); - --ev-c-text-3: rgba(235, 235, 245, 0.38); - - --ev-button-alt-border: transparent; - --ev-button-alt-text: var(--ev-c-text-1); - --ev-button-alt-bg: var(--ev-c-gray-3); - --ev-button-alt-hover-border: transparent; - --ev-button-alt-hover-text: var(--ev-c-text-1); - --ev-button-alt-hover-bg: var(--ev-c-gray-2); -} - -:root { - --color-background: var(--ev-c-black); - --color-background-soft: var(--ev-c-black-soft); - --color-background-mute: var(--ev-c-black-mute); - - --color-text: var(--ev-c-text-1); -} - -*, -*::before, -*::after { - box-sizing: border-box; - margin: 0; - font-weight: normal; -} - -ul { - list-style: none; -} - -body { - min-height: 100vh; - color: var(--color-text); - background: var(--color-background); - line-height: 1.6; - font-family: - Inter, - -apple-system, - BlinkMacSystemFont, - 'Segoe UI', - Roboto, - Oxygen, - Ubuntu, - Cantarell, - 'Fira Sans', - 'Droid Sans', - 'Helvetica Neue', - sans-serif; - text-rendering: optimizeLegibility; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; -} diff --git a/installer/src/renderer/src/assets/main.css b/installer/src/renderer/src/assets/main.css deleted file mode 100644 index 495aa99..0000000 --- a/installer/src/renderer/src/assets/main.css +++ /dev/null @@ -1,171 +0,0 @@ -@import './base.css'; - -body { - display: flex; - align-items: center; - justify-content: center; - overflow: hidden; - background-image: url('./wavy-lines.svg'); - background-size: cover; - user-select: none; -} - -code { - font-weight: 600; - padding: 3px 5px; - border-radius: 2px; - background-color: var(--color-background-mute); - font-family: - ui-monospace, - SFMono-Regular, - SF Mono, - Menlo, - Consolas, - Liberation Mono, - monospace; - font-size: 85%; -} - -#app { - display: flex; - align-items: center; - justify-content: center; - flex-direction: column; - margin-bottom: 80px; -} - -.logo { - margin-bottom: 20px; - -webkit-user-drag: none; - height: 128px; - width: 128px; - will-change: filter; - transition: filter 300ms; -} - -.logo:hover { - filter: drop-shadow(0 0 1.2em #6988e6aa); -} - -.creator { - font-size: 14px; - line-height: 16px; - color: var(--ev-c-text-2); - font-weight: 600; - margin-bottom: 10px; -} - -.text { - font-size: 28px; - color: var(--ev-c-text-1); - font-weight: 700; - line-height: 32px; - text-align: center; - margin: 0 10px; - padding: 16px 0; -} - -.tip { - font-size: 16px; - line-height: 24px; - color: var(--ev-c-text-2); - font-weight: 600; -} - -.vue { - background: -webkit-linear-gradient(315deg, #42d392 25%, #647eff); - background-clip: text; - -webkit-background-clip: text; - -webkit-text-fill-color: transparent; - font-weight: 700; -} - -.ts { - background: -webkit-linear-gradient(315deg, #3178c6 45%, #f0dc4e); - background-clip: text; - -webkit-background-clip: text; - -webkit-text-fill-color: transparent; - font-weight: 700; -} - -.actions { - display: flex; - padding-top: 32px; - margin: -6px; - flex-wrap: wrap; - justify-content: flex-start; -} - -.action { - flex-shrink: 0; - padding: 6px; -} - -.action a { - cursor: pointer; - text-decoration: none; - display: inline-block; - border: 1px solid transparent; - text-align: center; - font-weight: 600; - white-space: nowrap; - border-radius: 20px; - padding: 0 20px; - line-height: 38px; - font-size: 14px; - border-color: var(--ev-button-alt-border); - color: var(--ev-button-alt-text); - background-color: var(--ev-button-alt-bg); -} - -.action a:hover { - border-color: var(--ev-button-alt-hover-border); - color: var(--ev-button-alt-hover-text); - background-color: var(--ev-button-alt-hover-bg); -} - -.versions { - position: absolute; - bottom: 30px; - margin: 0 auto; - padding: 15px 0; - font-family: 'Menlo', 'Lucida Console', monospace; - display: inline-flex; - overflow: hidden; - align-items: center; - border-radius: 22px; - background-color: #202127; - backdrop-filter: blur(24px); -} - -.versions li { - display: block; - float: left; - border-right: 1px solid var(--ev-c-gray-1); - padding: 0 20px; - font-size: 14px; - line-height: 14px; - opacity: 0.8; - &:last-child { - border: none; - } -} - -@media (max-width: 720px) { - .text { - font-size: 20px; - } -} - -@media (max-width: 620px) { - .versions { - display: none; - } -} - -@media (max-width: 350px) { - .tip, - .actions { - display: none; - } -} diff --git a/installer/src/renderer/src/env.d.ts b/installer/src/renderer/src/env.d.ts deleted file mode 100644 index aafef95..0000000 --- a/installer/src/renderer/src/env.d.ts +++ /dev/null @@ -1,8 +0,0 @@ -/// - -declare module '*.vue' { - import type { DefineComponent } from 'vue' - // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/ban-types - const component: DefineComponent<{}, {}, any> - export default component -} diff --git a/installer/src/renderer/src/main.ts b/installer/src/renderer/src/main.ts deleted file mode 100644 index 0ac3a5f..0000000 --- a/installer/src/renderer/src/main.ts +++ /dev/null @@ -1,6 +0,0 @@ -import './assets/main.css' - -import { createApp } from 'vue' -import App from './App.vue' - -createApp(App).mount('#app') diff --git a/installer/tsconfig.json b/installer/tsconfig.json deleted file mode 100644 index 31bac6e..0000000 --- a/installer/tsconfig.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "files": [], - "references": [{ "path": "./tsconfig.node.json" }, { "path": "./tsconfig.web.json" }] -} diff --git a/installer/tsconfig.node.json b/installer/tsconfig.node.json deleted file mode 100644 index db23a68..0000000 --- a/installer/tsconfig.node.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "extends": "@electron-toolkit/tsconfig/tsconfig.node.json", - "include": ["electron.vite.config.*", "src/main/**/*", "src/preload/**/*"], - "compilerOptions": { - "composite": true, - "types": ["electron-vite/node"] - } -} diff --git a/installer/tsconfig.web.json b/installer/tsconfig.web.json deleted file mode 100644 index e9d73a9..0000000 --- a/installer/tsconfig.web.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "extends": "@electron-toolkit/tsconfig/tsconfig.web.json", - "include": [ - "src/renderer/src/env.d.ts", - "src/renderer/src/**/*", - "src/renderer/src/**/*.vue", - "src/preload/*.d.ts" - ], - "compilerOptions": { - "composite": true, - "baseUrl": ".", - "paths": { - "@renderer/*": [ - "src/renderer/src/*" - ] - } - } -} diff --git a/lib/README b/lib/README deleted file mode 100644 index 9379397..0000000 --- a/lib/README +++ /dev/null @@ -1,46 +0,0 @@ - -This directory is intended for project specific (private) libraries. -PlatformIO will compile them to static libraries and link into the executable file. - -The source code of each library should be placed in a separate directory -("lib/your_library_name/[Code]"). - -For example, see the structure of the following example libraries `Foo` and `Bar`: - -|--lib -| | -| |--Bar -| | |--docs -| | |--examples -| | |--src -| | |- Bar.c -| | |- Bar.h -| | |- library.json (optional. for custom build options, etc) https://docs.platformio.org/page/librarymanager/config.html -| | -| |--Foo -| | |- Foo.c -| | |- Foo.h -| | -| |- README --> THIS FILE -| -|- platformio.ini -|--src - |- main.c - -Example contents of `src/main.c` using Foo and Bar: -``` -#include -#include - -int main (void) -{ - ... -} - -``` - -The PlatformIO Library Dependency Finder will find automatically dependent -libraries by scanning project source files. - -More information about PlatformIO Library Dependency Finder -- https://docs.platformio.org/page/librarymanager/ldf.html diff --git a/lib/netignite_core/library.json b/lib/netignite_core/library.json new file mode 100644 index 0000000..71bcb7f --- /dev/null +++ b/lib/netignite_core/library.json @@ -0,0 +1,10 @@ +{ + "name": "netignite_core", + "version": "2.0.0", + "description": "Hardware-independent parts of the NetIgnite agent firmware: parsers and encoders for MAC addresses, server URLs, the agent protocol, Improv Serial and Wake-on-LAN. Unit-tested on the host (pio test -e native).", + "frameworks": "*", + "platforms": "*", + "build": { + "flags": ["-DARDUINOJSON_ENABLE_STD_STRING=1"] + } +} diff --git a/lib/netignite_core/src/netignite/CommandLine.cpp b/lib/netignite_core/src/netignite/CommandLine.cpp new file mode 100644 index 0000000..04ce636 --- /dev/null +++ b/lib/netignite_core/src/netignite/CommandLine.cpp @@ -0,0 +1,41 @@ +#include "netignite/CommandLine.h" + +namespace netignite { + +bool splitCommandLine(std::string_view line, std::vector& out) { + out.clear(); + std::string current; + bool inArgument = false; + bool quoted = false; + + for (size_t i = 0; i < line.size(); i++) { + const char c = line[i]; + + if (c == '\\' && i + 1 < line.size()) { + current += line[++i]; + inArgument = true; + continue; + } + if (c == '"') { + quoted = !quoted; + inArgument = true; + continue; + } + if (!quoted && (c == ' ' || c == '\t')) { + if (inArgument) { + out.push_back(current); + current.clear(); + inArgument = false; + } + continue; + } + current += c; + inArgument = true; + } + + if (quoted) return false; + if (inArgument) out.push_back(current); + return true; +} + +} // namespace netignite diff --git a/lib/netignite_core/src/netignite/CommandLine.h b/lib/netignite_core/src/netignite/CommandLine.h new file mode 100644 index 0000000..d044459 --- /dev/null +++ b/lib/netignite_core/src/netignite/CommandLine.h @@ -0,0 +1,16 @@ +#pragma once + +#include +#include +#include + +namespace netignite { + +/** + * Splits a console line into arguments. Arguments are separated by spaces; double quotes group + * words (`wifi "My Network" secret`) and a backslash escapes the next character. Returns false + * for an unterminated quote. + */ +bool splitCommandLine(std::string_view line, std::vector& out); + +} // namespace netignite diff --git a/lib/netignite_core/src/netignite/Hex.cpp b/lib/netignite_core/src/netignite/Hex.cpp new file mode 100644 index 0000000..70913b7 --- /dev/null +++ b/lib/netignite_core/src/netignite/Hex.cpp @@ -0,0 +1,67 @@ +#include "netignite/Hex.h" + +namespace netignite { + +namespace { + +int nibble(char c) { + if (c >= '0' && c <= '9') return c - '0'; + if (c >= 'a' && c <= 'f') return c - 'a' + 10; + if (c >= 'A' && c <= 'F') return c - 'A' + 10; + return -1; +} + +} // namespace + +std::string toHex(const uint8_t* data, size_t length) { + static constexpr char DIGITS[] = "0123456789abcdef"; + std::string out; + out.reserve(length * 2); + for (size_t i = 0; i < length; i++) { + out += DIGITS[data[i] >> 4]; + out += DIGITS[data[i] & 0x0f]; + } + return out; +} + +std::string toBase64(const uint8_t* data, size_t length, bool pad) { + static constexpr char ALPHABET[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + std::string out; + out.reserve((length + 2) / 3 * 4); + for (size_t i = 0; i < length; i += 3) { + const uint32_t chunk = (static_cast(data[i]) << 16) | + (i + 1 < length ? static_cast(data[i + 1]) << 8 : 0) | + (i + 2 < length ? static_cast(data[i + 2]) : 0); + out += ALPHABET[(chunk >> 18) & 0x3f]; + out += ALPHABET[(chunk >> 12) & 0x3f]; + if (i + 1 < length) { + out += ALPHABET[(chunk >> 6) & 0x3f]; + } else if (pad) { + out += '='; + } + if (i + 2 < length) { + out += ALPHABET[chunk & 0x3f]; + } else if (pad) { + out += '='; + } + } + return out; +} + +bool fromHex(std::string_view text, std::vector& out) { + out.clear(); + if (text.size() % 2 != 0) return false; + out.reserve(text.size() / 2); + for (size_t i = 0; i < text.size(); i += 2) { + const int high = nibble(text[i]); + const int low = nibble(text[i + 1]); + if (high < 0 || low < 0) { + out.clear(); + return false; + } + out.push_back(static_cast((high << 4) | low)); + } + return true; +} + +} // namespace netignite diff --git a/lib/netignite_core/src/netignite/Hex.h b/lib/netignite_core/src/netignite/Hex.h new file mode 100644 index 0000000..e183823 --- /dev/null +++ b/lib/netignite_core/src/netignite/Hex.h @@ -0,0 +1,20 @@ +#pragma once + +#include +#include +#include +#include +#include + +namespace netignite { + +/** Lower-case hex encoding. */ +std::string toHex(const uint8_t* data, size_t length); + +/** Decodes hex (either case). False for odd lengths or other characters. */ +bool fromHex(std::string_view text, std::vector& out); + +/** Standard base64; without `=` padding if `pad` is false (OpenSSH fingerprints). */ +std::string toBase64(const uint8_t* data, size_t length, bool pad = true); + +} // namespace netignite diff --git a/lib/netignite_core/src/netignite/Improv.cpp b/lib/netignite_core/src/netignite/Improv.cpp new file mode 100644 index 0000000..d1c72f3 --- /dev/null +++ b/lib/netignite_core/src/netignite/Improv.cpp @@ -0,0 +1,63 @@ +#include "netignite/Improv.h" + +namespace netignite::improv { + +std::vector buildPacket(PacketType type, const std::vector& data) { + std::vector packet = {'I', 'M', 'P', 'R', 'O', 'V', VERSION, static_cast(type), + static_cast(data.size())}; + packet.insert(packet.end(), data.begin(), data.end()); + + uint8_t checksum = 0; + for (uint8_t byte : packet) checksum = static_cast(checksum + byte); + packet.push_back(checksum); + packet.push_back('\n'); + return packet; +} + +std::vector buildStatePacket(State state) { + return buildPacket(PacketType::CurrentState, {static_cast(state)}); +} + +std::vector buildErrorPacket(Error error) { + return buildPacket(PacketType::ErrorState, {static_cast(error)}); +} + +std::vector buildRpcResult(Command command, const std::vector& strings) { + std::vector data = {static_cast(command), 0}; + for (const std::string& text : strings) { + const size_t length = text.size() > 255 ? 255 : text.size(); + // The whole result must fit the packet's one-byte length. + if (data.size() + 1 + length > 255) break; + data.push_back(static_cast(length)); + data.insert(data.end(), text.begin(), text.begin() + static_cast(length)); + } + data[1] = static_cast(data.size() - 2); + return buildPacket(PacketType::RpcResult, data); +} + +bool parseRpc(const std::vector& data, RpcCommand& out) { + if (data.size() < 2) return false; + + const uint8_t length = data[1]; + if (static_cast(length) + 2 != data.size()) return false; + + out = RpcCommand{static_cast(data[0]), {}, {}}; + if (out.command != Command::WifiSettings) return true; + + // payload: ssid length | ssid | password length | password + size_t pos = 2; + if (pos >= data.size()) return false; + const size_t ssidLength = data[pos++]; + // The SSID and the password's length byte must fit. + if (pos + ssidLength + 1 > data.size()) return false; + out.ssid.assign(data.begin() + static_cast(pos), data.begin() + static_cast(pos + ssidLength)); + pos += ssidLength; + + const size_t passwordLength = data[pos++]; + if (pos + passwordLength != data.size()) return false; + out.password.assign(data.begin() + static_cast(pos), data.end()); + + return !out.ssid.empty(); +} + +} // namespace netignite::improv diff --git a/lib/netignite_core/src/netignite/Improv.h b/lib/netignite_core/src/netignite/Improv.h new file mode 100644 index 0000000..09521d5 --- /dev/null +++ b/lib/netignite_core/src/netignite/Improv.h @@ -0,0 +1,74 @@ +#pragma once + +#include +#include +#include + +/** + * Improv Wi-Fi Serial (https://www.improv-wifi.com/serial/), as used by ESP Web Tools to send + * Wi-Fi credentials right after flashing. + * + * Packet: "IMPROV" | version (1) | type | length | data[length] | checksum | '\n' + * The checksum is the low byte of the sum of all bytes before it, starting at 'I'. + */ +namespace netignite::improv { + +constexpr uint8_t VERSION = 1; + +enum class PacketType : uint8_t { + CurrentState = 0x01, + ErrorState = 0x02, + Rpc = 0x03, + RpcResult = 0x04, +}; + +enum class State : uint8_t { + Stopped = 0x00, + AwaitingAuthorization = 0x01, + Authorized = 0x02, + Provisioning = 0x03, + Provisioned = 0x04, +}; + +enum class Error : uint8_t { + None = 0x00, + InvalidRpc = 0x01, + UnknownRpc = 0x02, + UnableToConnect = 0x03, + NotAuthorized = 0x04, + BadHostname = 0x05, + Unknown = 0xff, +}; + +enum class Command : uint8_t { + WifiSettings = 0x01, + GetCurrentState = 0x02, + GetDeviceInfo = 0x03, + GetWifiNetworks = 0x04, + Hostname = 0x05, + DeviceName = 0x06, + GetNetworkState = 0x07, +}; + +struct RpcCommand { + Command command; + /** Only for WifiSettings. */ + std::string ssid; + std::string password; +}; + +/** A complete serial packet including header, checksum and trailing newline. */ +std::vector buildPacket(PacketType type, const std::vector& data); + +std::vector buildStatePacket(State state); +std::vector buildErrorPacket(Error error); +/** RPC result: the strings, each prefixed with its length (truncated to 255 bytes). */ +std::vector buildRpcResult(Command command, const std::vector& strings); + +/** + * Parses the data of an RPC packet: command | length | payload[length]. Returns false for a + * malformed packet (lengths that don't add up). + */ +bool parseRpc(const std::vector& data, RpcCommand& out); + +} // namespace netignite::improv diff --git a/lib/netignite_core/src/netignite/LogRing.cpp b/lib/netignite_core/src/netignite/LogRing.cpp new file mode 100644 index 0000000..c99f671 --- /dev/null +++ b/lib/netignite_core/src/netignite/LogRing.cpp @@ -0,0 +1,39 @@ +#include "netignite/LogRing.h" + +namespace netignite { + +void LogRing::push(std::string_view line) { + if (line.size() > MAX_LINE) line = line.substr(0, MAX_LINE); + lines_.emplace_back(line); + bytes_ += line.size(); + + while (bytes_ > capacity_ && !lines_.empty()) { + bytes_ -= lines_.front().size(); + lines_.pop_front(); + firstSeq_++; + } +} + +LogRing::ReadResult LogRing::read(uint32_t from, size_t maxBytes, std::vector& out) const { + ReadResult result; + // Sequence numbers wrap after 2^32 lines; compare by distance. + uint32_t start = from; + if (static_cast(from - firstSeq_) < 0) { + result.dropped = firstSeq_ - from; + start = firstSeq_; + } + if (static_cast(start - nextSeq()) > 0) start = nextSeq(); + + size_t used = 0; + uint32_t seq = start; + for (size_t i = start - firstSeq_; i < lines_.size(); i++, seq++) { + const std::string& line = lines_[i]; + if (used > 0 && used + line.size() > maxBytes) break; + out.push_back(line); + used += line.size(); + } + result.next = seq; + return result; +} + +} // namespace netignite diff --git a/lib/netignite_core/src/netignite/LogRing.h b/lib/netignite_core/src/netignite/LogRing.h new file mode 100644 index 0000000..747fd0c --- /dev/null +++ b/lib/netignite_core/src/netignite/LogRing.h @@ -0,0 +1,48 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +namespace netignite { + +/** + * The most recent log lines, up to a byte budget, for the dashboard's console. Every line gets a + * sequence number, so a reader can continue where it stopped and learn how many lines it missed. + * Not thread-safe; the caller locks. + */ +class LogRing { + public: + /** Longer lines are cut. */ + static constexpr size_t MAX_LINE = 240; + + explicit LogRing(size_t capacityBytes) : capacity_(capacityBytes) {} + + void push(std::string_view line); + + /** Sequence number the next pushed line gets. */ + uint32_t nextSeq() const { return firstSeq_ + static_cast(lines_.size()); } + + struct ReadResult { + /** Where to continue reading. */ + uint32_t next = 0; + /** Lines between `from` and the oldest stored line that were already dropped. */ + uint32_t dropped = 0; + }; + + /** Appends lines from sequence `from` on to `out`, at most `maxBytes` of text. */ + ReadResult read(uint32_t from, size_t maxBytes, std::vector& out) const; + + size_t bytes() const { return bytes_; } + + private: + size_t capacity_; + size_t bytes_ = 0; + uint32_t firstSeq_ = 0; + std::deque lines_; +}; + +} // namespace netignite diff --git a/lib/netignite_core/src/netignite/Mac.cpp b/lib/netignite_core/src/netignite/Mac.cpp new file mode 100644 index 0000000..eee6e3c --- /dev/null +++ b/lib/netignite_core/src/netignite/Mac.cpp @@ -0,0 +1,103 @@ +#include "netignite/Mac.h" + +#include + +namespace netignite { + +namespace { + +int hexValue(char c) { + if (c >= '0' && c <= '9') return c - '0'; + if (c >= 'a' && c <= 'f') return c - 'a' + 10; + if (c >= 'A' && c <= 'F') return c - 'A' + 10; + return -1; +} + +bool parsePair(std::string_view text, size_t index, uint8_t& out) { + const int high = hexValue(text[index]); + const int low = hexValue(text[index + 1]); + if (high < 0 || low < 0) return false; + out = static_cast((high << 4) | low); + return true; +} + +} // namespace + +bool parseMac(std::string_view text, MacAddress& out) { + MacAddress mac{}; + + if (text.size() == 12) { + for (size_t i = 0; i < 6; i++) { + if (!parsePair(text, i * 2, mac[i])) return false; + } + out = mac; + return true; + } + + if (text.size() != 17) return false; + + const char separator = text[2]; + if (separator != ':' && separator != '-') return false; + + for (size_t i = 0; i < 6; i++) { + if (!parsePair(text, i * 3, mac[i])) return false; + if (i < 5 && text[i * 3 + 2] != separator) return false; + } + + out = mac; + return true; +} + +std::string formatMac(const MacAddress& mac) { + char buffer[18]; + std::snprintf(buffer, sizeof(buffer), "%02X:%02X:%02X:%02X:%02X:%02X", mac[0], mac[1], mac[2], mac[3], + mac[4], mac[5]); + return buffer; +} + +IPv6Bytes linkLocalFromMac(const MacAddress& mac) { + return IPv6Bytes{ + 0xfe, 0x80, 0, 0, 0, 0, 0, 0, + // Modified EUI-64: flip the universal/local bit, insert FF:FE in the middle. + static_cast(mac[0] ^ 0x02), mac[1], mac[2], 0xff, 0xfe, mac[3], mac[4], mac[5], + }; +} + +std::string formatIPv6(const IPv6Bytes& address) { + uint16_t groups[8]; + for (int i = 0; i < 8; i++) { + groups[i] = static_cast((address[i * 2] << 8) | address[i * 2 + 1]); + } + + // Longest run (length >= 2) of zero groups is written as "::". + int bestStart = -1, bestLength = 0; + for (int i = 0; i < 8;) { + if (groups[i] != 0) { + i++; + continue; + } + int j = i; + while (j < 8 && groups[j] == 0) j++; + if (j - i > bestLength && j - i >= 2) { + bestStart = i; + bestLength = j - i; + } + i = j; + } + + std::string text; + char part[6]; + for (int i = 0; i < 8; i++) { + if (i == bestStart) { + text += "::"; + i += bestLength - 1; + continue; + } + if (!text.empty() && text.back() != ':') text += ':'; + std::snprintf(part, sizeof(part), "%x", groups[i]); + text += part; + } + return text; +} + +} // namespace netignite diff --git a/lib/netignite_core/src/netignite/Mac.h b/lib/netignite_core/src/netignite/Mac.h new file mode 100644 index 0000000..5ab86f3 --- /dev/null +++ b/lib/netignite_core/src/netignite/Mac.h @@ -0,0 +1,28 @@ +#pragma once + +#include +#include +#include +#include + +namespace netignite { + +using MacAddress = std::array; +using IPv6Bytes = std::array; + +/** + * Parses a MAC address: six hex byte pairs separated by `:` or `-` (`AA:BB:CC:DD:EE:FF`, + * `aa-bb-cc-dd-ee-ff`), or twelve hex digits without separators (`AABBCCDDEEFF`). + */ +bool parseMac(std::string_view text, MacAddress& out); + +/** `AA:BB:CC:DD:EE:FF` */ +std::string formatMac(const MacAddress& mac); + +/** The IPv6 link-local address with the modified EUI-64 interface id derived from `mac`. */ +IPv6Bytes linkLocalFromMac(const MacAddress& mac); + +/** Compact IPv6 text form (`fe80::1a2b:3cff:fe4d:5e6f`), for logs. */ +std::string formatIPv6(const IPv6Bytes& address); + +} // namespace netignite diff --git a/lib/netignite_core/src/netignite/MagicPacket.cpp b/lib/netignite_core/src/netignite/MagicPacket.cpp new file mode 100644 index 0000000..50e031c --- /dev/null +++ b/lib/netignite_core/src/netignite/MagicPacket.cpp @@ -0,0 +1,18 @@ +#include "netignite/MagicPacket.h" + +namespace netignite { + +std::array buildMagicPacket(const MacAddress& mac) { + std::array packet{}; + for (size_t i = 0; i < 6; i++) packet[i] = 0xff; + for (size_t repeat = 0; repeat < 16; repeat++) { + for (size_t i = 0; i < 6; i++) packet[6 + repeat * 6 + i] = mac[i]; + } + return packet; +} + +uint32_t directedBroadcast(uint32_t address, uint32_t netmask) { + return (address & netmask) | ~netmask; +} + +} // namespace netignite diff --git a/lib/netignite_core/src/netignite/MagicPacket.h b/lib/netignite_core/src/netignite/MagicPacket.h new file mode 100644 index 0000000..16c5318 --- /dev/null +++ b/lib/netignite_core/src/netignite/MagicPacket.h @@ -0,0 +1,18 @@ +#pragma once + +#include +#include + +#include "netignite/Mac.h" + +namespace netignite { + +constexpr size_t MAGIC_PACKET_SIZE = 102; + +/** Wake-on-LAN magic packet: 6 × 0xFF followed by the MAC address 16 times. */ +std::array buildMagicPacket(const MacAddress& mac); + +/** The IPv4 subnet broadcast address (host byte order) for an address and netmask. */ +uint32_t directedBroadcast(uint32_t address, uint32_t netmask); + +} // namespace netignite diff --git a/lib/netignite_core/src/netignite/Protocol.cpp b/lib/netignite_core/src/netignite/Protocol.cpp new file mode 100644 index 0000000..d26deea --- /dev/null +++ b/lib/netignite_core/src/netignite/Protocol.cpp @@ -0,0 +1,447 @@ +#include "netignite/Protocol.h" + +#include + +#include "netignite/Hex.h" +#include "netignite/ServerUrl.h" +#include "netignite/Validation.h" + +namespace netignite::protocol { + +namespace { + +bool readPort(JsonVariantConst value, uint16_t& out, bool allowZero) { + if (!value.is()) return false; + const long long port = value.as(); + if (port < (allowZero ? 0 : 1) || port > 65535) return false; + out = static_cast(port); + return true; +} + +/** An absolute server path without `//`, `..`, query or fragment. */ +bool safePath(std::string_view path) { + if (path.empty() || path.size() > 200 || path[0] != '/' || path.find("//") != std::string_view::npos || + path.find("..") != std::string_view::npos) { + return false; + } + for (char c : path) { + const bool ok = (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '/' || + c == '.' || c == '_' || c == '-' || c == '~' || c == '+'; + if (!ok) return false; + } + return true; +} + +bool safeVersion(std::string_view version) { + if (version.empty() || version.size() > 64) return false; + for (char c : version) { + const bool ok = (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '.' || + c == '-' || c == '+'; + if (!ok) return false; + } + return true; +} + +std::string serialize(const JsonDocument& doc) { + std::string out; + serializeJson(doc, out); + return out; +} + +} // namespace + +bool parseFrame(std::string_view text, JsonDocument& doc, Frame& out, std::string& error) { + out = Frame{}; + + const DeserializationError result = deserializeJson(doc, text.data(), text.size()); + if (result) { + error = std::string("invalid JSON: ") + result.c_str(); + return false; + } + + JsonObjectConst frame = doc.as(); + if (frame.isNull()) { + error = "frame is not an object"; + return false; + } + + const char* type = frame["t"]; + if (!type) { + error = "missing frame type"; + return false; + } + + const std::string_view t(type); + if (t == "req") { + out.type = FrameType::Request; + } else if (t == "res") { + out.type = FrameType::Response; + } else if (t == "evt") { + out.type = FrameType::Event; + } else { + error = "unknown frame type"; + return false; + } + + if (out.type != FrameType::Event) { + JsonVariantConst id = frame["id"]; + if (!id.is() || id.as() < 0 || id.as() > 0xffffffffLL) { + error = "missing or invalid id"; + return false; + } + out.id = static_cast(id.as()); + } + + if (out.type != FrameType::Response) { + const char* cmd = frame["cmd"]; + if (!cmd || !*cmd) { + error = "missing cmd"; + return false; + } + out.cmd = cmd; + } + + out.payload = frame["p"].as(); + return true; +} + +std::string encodeResponse(uint32_t id, JsonObjectConst payload) { + JsonDocument doc; + doc["t"] = "res"; + doc["id"] = id; + doc["ok"] = true; + JsonObject p = doc["p"].to(); + if (!payload.isNull()) p.set(payload); + return serialize(doc); +} + +std::string encodeEmptyResponse(uint32_t id) { + return encodeResponse(id, JsonObjectConst()); +} + +std::string encodeError(uint32_t id, std::string_view error) { + JsonDocument doc; + doc["t"] = "res"; + doc["id"] = id; + doc["ok"] = false; + doc["err"] = std::string(error.substr(0, 1024)); + return serialize(doc); +} + +std::string encodeEvent(std::string_view cmd, JsonObjectConst payload) { + JsonDocument doc; + doc["t"] = "evt"; + doc["cmd"] = std::string(cmd); + JsonObject p = doc["p"].to(); + if (!payload.isNull()) p.set(payload); + return serialize(doc); +} + +bool parseWakeRequest(JsonObjectConst payload, WakeRequest& out, std::string& error) { + const char* mac = payload["mac"]; + if (!mac || !parseMac(mac, out.mac)) { + error = "Invalid MAC address"; + return false; + } + if (!readPort(payload["port"], out.port, false)) { + error = "Invalid port"; + return false; + } + return true; +} + +bool parseStatusRequest(JsonObjectConst payload, StatusRequest& out, std::string& error) { + out = StatusRequest{}; + + JsonObjectConst target = payload["target"]; + const char* type = target["type"]; + if (!type) { + error = "Missing status target"; + return false; + } + + const std::string_view t(type); + if (t == "link_local") { + out.target = StatusRequest::Target::LinkLocal; + const char* mac = target["mac"]; + if (!mac || !parseMac(mac, out.mac)) { + error = "Invalid MAC address"; + return false; + } + } else if (t == "ip" || t == "host") { + out.target = t == "ip" ? StatusRequest::Target::Ip : StatusRequest::Target::Host; + const char* address = target[t == "ip" ? "address" : "host"]; + if (!address || !*address || std::string_view(address).size() > 253) { + error = "Invalid address"; + return false; + } + out.address = address; + // The agent uses its own interface for link-local addresses. + const size_t zone = out.address.find('%'); + if (zone != std::string::npos) out.address.erase(zone); + } else { + error = "Unknown status target"; + return false; + } + + JsonVariantConst icmp = payload["icmp"]; + out.icmp = icmp.is() ? icmp.as() : true; + + JsonVariantConst tcpPort = payload["tcp_port"]; + if (!tcpPort.isNull() && !readPort(tcpPort, out.tcpPort, false)) { + error = "Invalid TCP port"; + return false; + } + + if (!out.icmp && out.tcpPort == 0) { + error = "No probe requested"; + return false; + } + return true; +} + +bool parseOtaRequest(JsonObjectConst payload, OtaRequest& out, std::string& error) { + out = OtaRequest{}; + + const char* version = payload["version"]; + if (!version || !safeVersion(version)) { + error = "Invalid version"; + return false; + } + out.version = version; + + const char* path = payload["path"]; + if (!path || !safePath(path)) { + error = "Invalid download path"; + return false; + } + out.path = path; + + JsonVariantConst size = payload["size"]; + if (!size.is() || size.as() <= 0 || size.as() > MAX_FIRMWARE_SIZE) { + error = "Invalid size (the image must fit into an OTA slot)"; + return false; + } + out.size = static_cast(size.as()); + + const char* sha256 = payload["sha256"]; + std::vector digest; + if (!sha256 || !fromHex(sha256, digest) || digest.size() != out.sha256.size()) { + error = "Invalid SHA-256"; + return false; + } + std::copy(digest.begin(), digest.end(), out.sha256.begin()); + + JsonVariantConst signature = payload["signature"]; + if (!signature.isNull()) { + const char* text = signature.as(); + // DER-encoded ECDSA P-256 signatures are 70–72 bytes. + if (!text || !fromHex(text, out.signature) || out.signature.size() < 8 || out.signature.size() > 80) { + error = "Invalid signature"; + return false; + } + } + return true; +} + +bool parseConfigRequest(JsonObjectConst payload, ConfigRequest& out, std::string& error) { + out = ConfigRequest{}; + + JsonVariantConst server = payload["server_url"]; + if (!server.isNull()) { + const char* text = server.as(); + const auto parsed = text ? parseServerUrl(text) : std::nullopt; + if (!parsed) { + error = "Invalid server URL"; + return false; + } + out.serverUrl = parsed->url(""); + } + + JsonVariantConst name = payload["name"]; + if (!name.isNull()) { + const char* text = name.as(); + if (!text || !isValidHostname(text)) { + error = "Invalid name (letters, digits and -, max. 63)"; + return false; + } + out.name = text; + } + + JsonVariantConst level = payload["log_level"]; + if (!level.isNull()) { + const char* text = level.as(); + const std::string_view l = text ? text : ""; + if (l != "error" && l != "warn" && l != "info" && l != "debug") { + error = "Invalid log level"; + return false; + } + out.logLevel = std::string(l); + } + + if (!payload["ip"].isNull()) { + JsonObjectConst ip = payload["ip"]; + const char* mode = ip["mode"]; + ConfigRequest::Ip config; + if (mode && std::string_view(mode) == "dhcp") { + out.ip = config; + } else if (mode && std::string_view(mode) == "static") { + config.isStatic = true; + const char* const fields[] = {"address", "gateway", "netmask", "dns1", "dns2"}; + std::string* const targets[] = {&config.address, &config.gateway, &config.netmask, &config.dns1, + &config.dns2}; + for (size_t i = 0; i < 5; i++) { + const char* value = ip[fields[i]]; + if (!value || !*value) { + // address, gateway and netmask are required. + if (i < 3) { + error = std::string("Missing ") + fields[i]; + return false; + } + continue; + } + if (!isValidIpv4(value)) { + error = std::string("Invalid ") + fields[i]; + return false; + } + *targets[i] = value; + } + out.ip = config; + } else { + error = "Invalid IP mode (dhcp or static)"; + return false; + } + } + + if (!out.serverUrl && !out.name && !out.logLevel && !out.ip) { + error = "Nothing to change"; + return false; + } + return true; +} + +namespace { + +bool usernameChar(char c) { + return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '.' || c == '_' || + c == '@' || c == '\\' || c == '-'; +} + +bool base64Char(char c) { + return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '+' || c == '/'; +} + +} // namespace + +bool parseSshRequest(JsonObjectConst payload, bool exec, SshRequest& out, std::string& error) { + out = SshRequest{}; + + const char* host = payload["host"]; + const std::string_view h = host ? host : ""; + bool hostOk = !h.empty() && h.size() <= 253; + for (char c : h) hostOk = hostOk && static_cast(c) > ' ' && c != '/' && c != '@'; + if (!hostOk) { + error = "Invalid host"; + return false; + } + out.host = std::string(h); + + if (!readPort(payload["port"], out.port, false)) { + error = "Invalid port"; + return false; + } + + const char* username = payload["username"]; + const std::string_view u = username ? username : ""; + bool userOk = !u.empty() && u.size() <= 64; + for (char c : u) userOk = userOk && usernameChar(c); + if (!userOk) { + error = "Invalid username"; + return false; + } + out.username = std::string(u); + + JsonObjectConst auth = payload["auth"]; + const char* type = auth["type"]; + const char* secret = nullptr; + if (type && std::string_view(type) == "password") { + out.auth = SshRequest::Auth::Password; + secret = auth["password"]; + } else if (type && std::string_view(type) == "key") { + out.auth = SshRequest::Auth::Key; + secret = auth["private_key"]; + } else { + error = "Unknown auth type"; + return false; + } + if (!secret || !*secret || std::string_view(secret).size() > 8192) { + error = "Missing password or key"; + return false; + } + out.secret = secret; + + JsonVariantConst hostKey = payload["host_key"]; + if (!hostKey.isNull()) { + const char* text = hostKey.as(); + const std::string_view k = text ? text : ""; + bool keyOk = k.size() == 50 && k.substr(0, 7) == "SHA256:"; + for (size_t i = 7; keyOk && i < k.size(); i++) keyOk = base64Char(k[i]); + if (!keyOk) { + error = "Invalid host key fingerprint"; + return false; + } + out.hostKey = std::string(k); + } + + if (!exec) return true; + + if (out.hostKey.empty()) { + error = "A trusted host key is required"; + return false; + } + const char* action = payload["action"]; + if (action && std::string_view(action) == "shutdown") { + out.action = SshRequest::Action::Shutdown; + } else if (action && std::string_view(action) == "reboot") { + out.action = SshRequest::Action::Reboot; + } else { + error = "Unknown action"; + return false; + } + const char* os = payload["os"]; + if (os && std::string_view(os) == "windows") { + out.windows = true; + } else if (!os || std::string_view(os) != "posix") { + error = "Unknown operating system"; + return false; + } + JsonVariantConst sudo = payload["sudo"]; + if (!sudo.is()) { + error = "sudo must be true or false"; + return false; + } + out.sudo = sudo.as(); + return true; +} + +std::string sshCommand(const SshRequest& request) { + const bool reboot = request.action == SshRequest::Action::Reboot; + if (request.windows) return reboot ? "shutdown /r /t 0" : "shutdown /s /t 0"; + return std::string(request.sudo ? "sudo -n " : "") + (reboot ? "shutdown -r now" : "shutdown -h now"); +} + +std::string sshFingerprint(const uint8_t sha256[32]) { + return "SHA256:" + toBase64(sha256, 32, false); +} + +bool parseLogSubscribe(JsonObjectConst payload, bool& enabled, std::string& error) { + JsonVariantConst value = payload["enabled"]; + if (!value.is()) { + error = "enabled must be true or false"; + return false; + } + enabled = value.as(); + return true; +} + +} // namespace netignite::protocol diff --git a/lib/netignite_core/src/netignite/Protocol.h b/lib/netignite_core/src/netignite/Protocol.h new file mode 100644 index 0000000..45e6deb --- /dev/null +++ b/lib/netignite_core/src/netignite/Protocol.h @@ -0,0 +1,146 @@ +#pragma once + +#include + +#include +#include +#include +#include +#include +#include + +#include "netignite/Mac.h" + +/** + * NetIgnite agent protocol v2 (the server side is `server/lib/agents/protocol.ts`). + * + * One WebSocket to `/ws/agent`, authenticated with + * `Authorization: Bearer netignite_agent_:`. JSON text frames: + * + * {"t":"req","id":7,"cmd":"WAKE","p":{...}} server → agent + * {"t":"res","id":7,"ok":true,"p":{...}} agent → server, same id + * {"t":"res","id":7,"ok":false,"err":"..."} + * {"t":"evt","cmd":"HELLO","p":{...}} agent → server, first frame + * + * Requests: PING, WAKE, STATUS, OTA_BEGIN, LOG_SUBSCRIBE, GET_LOGS, GET_INFO, CONFIG_SET, REBOOT, + * FACTORY_RESET, SSH_CHECK, SSH_EXEC. Events: HELLO, LOG, OTA_PROGRESS. + */ +namespace netignite::protocol { + +constexpr int VERSION = 2; +constexpr const char* AGENT_KIND = "esp32"; + +/** Capabilities this firmware announces in HELLO. */ +constexpr const char* CAPABILITIES[] = { + "wake", "status.link_local", "status.ip", "status.host", "status.tcp", + "ota", "logs", "config", "device.reboot", "device.reset", + "ssh", +}; + +/** Largest firmware image: one OTA slot in partitions.csv. */ +constexpr uint32_t MAX_FIRMWARE_SIZE = 0x1E0000; + +enum class FrameType { Invalid, Request, Response, Event }; + +struct Frame { + FrameType type = FrameType::Invalid; + uint32_t id = 0; + std::string cmd; + /** Points into the JsonDocument passed to parseFrame. */ + JsonObjectConst payload; +}; + +/** Parses a frame from the server. Returns false (and an error text) for invalid frames. */ +bool parseFrame(std::string_view text, JsonDocument& doc, Frame& out, std::string& error); + +std::string encodeResponse(uint32_t id, JsonObjectConst payload); +std::string encodeEmptyResponse(uint32_t id); +std::string encodeError(uint32_t id, std::string_view error); +std::string encodeEvent(std::string_view cmd, JsonObjectConst payload); + +struct WakeRequest { + MacAddress mac{}; + uint16_t port = 9; +}; + +bool parseWakeRequest(JsonObjectConst payload, WakeRequest& out, std::string& error); + +struct StatusRequest { + enum class Target { LinkLocal, Ip, Host }; + Target target = Target::Ip; + MacAddress mac{}; + /** IP address (zone suffix like `%eth0` removed) or host name. */ + std::string address; + bool icmp = true; + /** 0 = no TCP probe. */ + uint16_t tcpPort = 0; +}; + +bool parseStatusRequest(JsonObjectConst payload, StatusRequest& out, std::string& error); + +/** + * OTA_BEGIN: download `path` from the agent's own server (with its agent credential) and install + * it if size and SHA-256 match, and the signature if the firmware has a public key compiled in. + */ +struct OtaRequest { + std::string version; + /** Absolute path on the server, e.g. `/api/v1/firmware/2.1.0/files/firmware.bin`. */ + std::string path; + uint32_t size = 0; + std::array sha256{}; + /** DER-encoded ECDSA P-256 signature of the image; empty for unsigned releases. */ + std::vector signature; +}; + +bool parseOtaRequest(JsonObjectConst payload, OtaRequest& out, std::string& error); + +/** CONFIG_SET: only the fields present change. */ +struct ConfigRequest { + struct Ip { + bool isStatic = false; + std::string address, gateway, netmask, dns1, dns2; + }; + /** Normalized (`ServerUrl::url("")`). */ + std::optional serverUrl; + std::optional name; + /** `error`, `warn`, `info` or `debug`. */ + std::optional logLevel; + std::optional ip; +}; + +bool parseConfigRequest(JsonObjectConst payload, ConfigRequest& out, std::string& error); + +/** LOG_SUBSCRIBE `{ "enabled": bool }`. */ +bool parseLogSubscribe(JsonObjectConst payload, bool& enabled, std::string& error); + +/** + * SSH_CHECK (log in, no session) and SSH_EXEC (log in with a trusted host key and run the fixed + * shutdown or reboot command). The secret is a password or an OpenSSH private key. + */ +struct SshRequest { + enum class Auth { Password, Key }; + enum class Action { Shutdown, Reboot }; + + std::string host; + uint16_t port = 22; + std::string username; + Auth auth = Auth::Password; + std::string secret; + /** Trusted host key fingerprint (`SHA256:...`), or empty. Required for SSH_EXEC. */ + std::string hostKey; + + // SSH_EXEC only + Action action = Action::Shutdown; + bool windows = false; + bool sudo = false; +}; + +bool parseSshRequest(JsonObjectConst payload, bool exec, SshRequest& out, std::string& error); + +/** The command SSH_EXEC runs: `[sudo -n ]shutdown -h|-r now`, or `shutdown /s|/r /t 0`. */ +std::string sshCommand(const SshRequest& request); + +/** OpenSSH's fingerprint of a host key from its SHA-256: `SHA256:` + unpadded base64. */ +std::string sshFingerprint(const uint8_t sha256[32]); + +} // namespace netignite::protocol diff --git a/lib/netignite_core/src/netignite/SerialDemux.cpp b/lib/netignite_core/src/netignite/SerialDemux.cpp new file mode 100644 index 0000000..1d4eb6c --- /dev/null +++ b/lib/netignite_core/src/netignite/SerialDemux.cpp @@ -0,0 +1,99 @@ +#include "netignite/SerialDemux.h" + +namespace netignite { + +namespace { +constexpr char HEADER[] = "IMPROV"; +constexpr size_t HEADER_LENGTH = 6; +// header + version + type + length +constexpr size_t PREFIX_LENGTH = HEADER_LENGTH + 3; +} // namespace + +SerialDemux::Event SerialDemux::finishLine() { + Event event; + std::string line(buffer_.begin(), buffer_.end()); + buffer_.clear(); + mode_ = Mode::LineStart; + + while (!line.empty() && (line.back() == ' ' || line.back() == '\t')) line.pop_back(); + size_t start = 0; + while (start < line.size() && (line[start] == ' ' || line[start] == '\t')) start++; + line.erase(0, start); + + if (line.empty()) return event; + event.type = Event::Type::Line; + event.line = std::move(line); + return event; +} + +SerialDemux::Event SerialDemux::feed(uint8_t byte) { + switch (mode_) { + case Mode::LineStart: + if (byte == '\n' || byte == '\r') { + buffer_.clear(); + return {}; + } + buffer_.push_back(byte); + if (byte != static_cast(HEADER[buffer_.size() - 1])) { + mode_ = Mode::Text; + } else if (buffer_.size() == HEADER_LENGTH) { + mode_ = Mode::Improv; + } + return {}; + + case Mode::Text: + if (byte == '\n' || byte == '\r') return finishLine(); + if (buffer_.size() >= MAX_LINE) { + buffer_.clear(); + mode_ = Mode::Discard; + return {}; + } + // Backspace / delete, for people typing in a terminal. + if (byte == 0x08 || byte == 0x7f) { + if (!buffer_.empty()) buffer_.pop_back(); + return {}; + } + buffer_.push_back(byte); + return {}; + + case Mode::Discard: + if (byte == '\n' || byte == '\r') mode_ = Mode::LineStart; + return {}; + + case Mode::Improv: { + buffer_.push_back(byte); + // "IMPROV" followed by anything but version 1 and a known type is just text. + if (buffer_.size() == HEADER_LENGTH + 1 && byte != 1) { + mode_ = Mode::Text; + return {}; + } + if (buffer_.size() == HEADER_LENGTH + 2 && (byte < 0x01 || byte > 0x04)) { + mode_ = Mode::Text; + return {}; + } + if (buffer_.size() < PREFIX_LENGTH) return {}; + + const size_t dataLength = buffer_[PREFIX_LENGTH - 1]; + const size_t total = PREFIX_LENGTH + dataLength + 1; + if (buffer_.size() < total) return {}; + + Event event; + uint8_t checksum = 0; + for (size_t i = 0; i + 1 < total; i++) checksum = static_cast(checksum + buffer_[i]); + + if (checksum != buffer_[total - 1]) { + event.type = Event::Type::ImprovBadChecksum; + } else { + event.type = Event::Type::ImprovPacket; + event.improvType = buffer_[HEADER_LENGTH + 1]; + event.improvData.assign(buffer_.begin() + PREFIX_LENGTH, buffer_.begin() + static_cast(total - 1)); + } + buffer_.clear(); + mode_ = Mode::LineStart; + return event; + } + } + return {}; +} + +} // namespace netignite diff --git a/lib/netignite_core/src/netignite/SerialDemux.h b/lib/netignite_core/src/netignite/SerialDemux.h new file mode 100644 index 0000000..fd9e289 --- /dev/null +++ b/lib/netignite_core/src/netignite/SerialDemux.h @@ -0,0 +1,39 @@ +#pragma once + +#include +#include +#include + +namespace netignite { + +/** + * Splits the serial input into Improv packets and text lines (the local console), which share + * the UART. A packet is recognized when a line starts with "IMPROV"; it is read by its length, + * because its data may contain newline bytes. + */ +class SerialDemux { + public: + struct Event { + enum class Type { None, Line, ImprovPacket, ImprovBadChecksum }; + Type type = Type::None; + /** Line, without the line ending. */ + std::string line; + /** Improv packet type and data (without header and checksum). */ + uint8_t improvType = 0; + std::vector improvData; + }; + + static constexpr size_t MAX_LINE = 512; + + Event feed(uint8_t byte); + + private: + enum class Mode { LineStart, Text, Improv, Discard }; + + Mode mode_ = Mode::LineStart; + std::vector buffer_; + + Event finishLine(); +}; + +} // namespace netignite diff --git a/lib/netignite_core/src/netignite/ServerUrl.cpp b/lib/netignite_core/src/netignite/ServerUrl.cpp new file mode 100644 index 0000000..41742af --- /dev/null +++ b/lib/netignite_core/src/netignite/ServerUrl.cpp @@ -0,0 +1,110 @@ +#include "netignite/ServerUrl.h" + +#include + +namespace netignite { + +namespace { + +std::string lower(std::string_view text) { + std::string out(text); + for (char& c : out) c = static_cast(std::tolower(static_cast(c))); + return out; +} + +bool validHostChar(char c) { + return std::isalnum(static_cast(c)) || c == '-' || c == '.' || c == '_'; +} + +} // namespace + +std::optional parseServerUrl(std::string_view text) { + while (!text.empty() && std::isspace(static_cast(text.front()))) text.remove_prefix(1); + while (!text.empty() && std::isspace(static_cast(text.back()))) text.remove_suffix(1); + if (text.empty() || text.size() > 200) return std::nullopt; + + ServerUrl url; + + const size_t schemeEnd = text.find("://"); + if (schemeEnd != std::string_view::npos) { + const std::string scheme = lower(text.substr(0, schemeEnd)); + if (scheme == "https") { + url.secure = true; + } else if (scheme == "http") { + url.secure = false; + } else { + return std::nullopt; + } + text.remove_prefix(schemeEnd + 3); + } + url.port = url.secure ? 443 : 80; + + if (text.find_first_of("?#@ ") != std::string_view::npos) return std::nullopt; + + const size_t pathStart = text.find('/'); + std::string_view authority = text.substr(0, pathStart); + std::string_view path = pathStart == std::string_view::npos ? std::string_view{} : text.substr(pathStart); + + std::string_view portText; + bool hasPort = false; + if (!authority.empty() && authority.front() == '[') { + const size_t close = authority.find(']'); + if (close == std::string_view::npos || close == 1) return std::nullopt; + url.host = lower(authority.substr(1, close - 1)); + for (char c : url.host) { + if (!std::isxdigit(static_cast(c)) && c != ':' && c != '.') return std::nullopt; + } + std::string_view rest = authority.substr(close + 1); + if (!rest.empty()) { + if (rest.front() != ':') return std::nullopt; + hasPort = true; + portText = rest.substr(1); + } + } else { + const size_t colon = authority.find(':'); + url.host = lower(authority.substr(0, colon)); + if (colon != std::string_view::npos) { + hasPort = true; + portText = authority.substr(colon + 1); + } + if (url.host.empty()) return std::nullopt; + for (char c : url.host) { + if (!validHostChar(c)) return std::nullopt; + } + } + if (url.host.empty()) return std::nullopt; + + if (hasPort) { + if (portText.empty() || portText.size() > 5) return std::nullopt; + uint32_t port = 0; + for (char c : portText) { + if (!std::isdigit(static_cast(c))) return std::nullopt; + port = port * 10 + static_cast(c - '0'); + } + if (port == 0 || port > 65535) return std::nullopt; + url.port = static_cast(port); + } + + while (!path.empty() && path.back() == '/') path.remove_suffix(1); + url.basePath = std::string(path); + + return url; +} + +std::string ServerUrl::origin() const { + std::string out = secure ? "https://" : "http://"; + const bool ipv6 = host.find(':') != std::string::npos; + out += ipv6 ? "[" + host + "]" : host; + if (port != (secure ? 443 : 80)) out += ":" + std::to_string(port); + return out; +} + +std::string ServerUrl::url(std::string_view p) const { + return origin() + path(p); +} + +std::string ServerUrl::path(std::string_view p) const { + return basePath + std::string(p); +} + +} // namespace netignite diff --git a/lib/netignite_core/src/netignite/ServerUrl.h b/lib/netignite_core/src/netignite/ServerUrl.h new file mode 100644 index 0000000..17ebe9c --- /dev/null +++ b/lib/netignite_core/src/netignite/ServerUrl.h @@ -0,0 +1,33 @@ +#pragma once + +#include +#include +#include +#include + +namespace netignite { + +/** A NetIgnite server address, e.g. `https://app.netignite.dev` or `http://192.168.1.10:18232`. */ +struct ServerUrl { + bool secure = true; + /** Host name or IP address; IPv6 without brackets. */ + std::string host; + uint16_t port = 443; + /** Path prefix if the server runs under a sub-path (`/netignite`), without a trailing slash. */ + std::string basePath; + + /** `https://host[:port]` (port only if not the scheme's default). */ + std::string origin() const; + /** `origin + basePath + path`, e.g. `url("/api/v1/agents/enroll")`. */ + std::string url(std::string_view path) const; + /** `basePath + path`, for clients that take host, port and path separately. */ + std::string path(std::string_view path) const; +}; + +/** + * Parses `http://` and `https://` URLs with an optional port and path. Query strings, fragments + * and credentials are rejected; a missing scheme means `https://`. + */ +std::optional parseServerUrl(std::string_view text); + +} // namespace netignite diff --git a/lib/netignite_core/src/netignite/Validation.cpp b/lib/netignite_core/src/netignite/Validation.cpp new file mode 100644 index 0000000..ccc387c --- /dev/null +++ b/lib/netignite_core/src/netignite/Validation.cpp @@ -0,0 +1,38 @@ +#include "netignite/Validation.h" + +#include + +namespace netignite { + +bool isValidHostname(std::string_view name) { + if (name.empty() || name.size() > 63 || name.front() == '-' || name.back() == '-') return false; + for (char c : name) { + if (!std::isalnum(static_cast(c)) && c != '-') return false; + } + return true; +} + +bool isValidIpv4(std::string_view text) { + int parts = 0; + size_t start = 0; + while (true) { + const size_t end = text.find('.', start); + const std::string_view part = + end == std::string_view::npos ? text.substr(start) : text.substr(start, end - start); + if (part.empty() || part.size() > 3 || (part.size() > 1 && part[0] == '0')) return false; + + int value = 0; + for (char c : part) { + if (c < '0' || c > '9') return false; + value = value * 10 + (c - '0'); + } + if (value > 255) return false; + + parts++; + if (end == std::string_view::npos) break; + start = end + 1; + } + return parts == 4; +} + +} // namespace netignite diff --git a/lib/netignite_core/src/netignite/Validation.h b/lib/netignite_core/src/netignite/Validation.h new file mode 100644 index 0000000..295103a --- /dev/null +++ b/lib/netignite_core/src/netignite/Validation.h @@ -0,0 +1,13 @@ +#pragma once + +#include + +namespace netignite { + +/** A network host name label: letters, digits and `-`, 1–63 characters, no `-` at either end. */ +bool isValidHostname(std::string_view name); + +/** A dotted-quad IPv4 address (`192.168.1.5`); no leading zeros, no shorthand forms. */ +bool isValidIpv4(std::string_view text); + +} // namespace netignite diff --git a/partitions.csv b/partitions.csv new file mode 100644 index 0000000..27e31f6 --- /dev/null +++ b/partitions.csv @@ -0,0 +1,10 @@ +# NetIgnite agent partition table (Arduino's min_spiffs layout). +# Two 1.875 MB app slots for OTA updates. Don't move nvs: the web installer writes the agent's +# provisioning data (server URL, enrollment token) into an NVS image at 0x9000. +# Name, Type, SubType, Offset, Size, Flags +nvs, data, nvs, 0x9000, 0x5000, +otadata, data, ota, 0xe000, 0x2000, +app0, app, ota_0, 0x10000, 0x1E0000, +app1, app, ota_1, 0x1F0000, 0x1E0000, +spiffs, data, spiffs, 0x3D0000, 0x20000, +coredump, data, coredump,0x3F0000, 0x10000, diff --git a/platformio.ini b/platformio.ini index 948c625..0bcc287 100644 --- a/platformio.ini +++ b/platformio.ini @@ -1,28 +1,61 @@ -; PlatformIO Project Configuration File +; NetIgnite ESP32 agent, firmware v2. ; -; Build options: build flags, source filter -; Upload options: custom upload port, speed and extra flags -; Library options: dependencies, extra library storages -; Advanced options: extra scripting -; -; Please visit documentation for the other options and examples -; https://docs.platformio.org/page/projectconf.html +; Build: pio run -e esp32dev +; Flash: pio run -e esp32dev -t upload (or use the web installer in the NetIgnite dashboard) +; Tests: pio test -e native + +[platformio] +default_envs = esp32dev [env:esp32dev] -platform = espressif32 +; pioarduino = Arduino-ESP32 3.3.12 on ESP-IDF 5.5.5. Pinned: firmware builds must be reproducible. +platform = https://github.com/pioarduino/platform-espressif32/releases/download/55.03.312-1/platform-espressif32.zip board = esp32dev framework = arduino -lib_deps = - a7md0/WakeOnLan@^1.1.7 - links2004/WebSockets@^2.6.1 - bblanchon/ArduinoJson@^7.4.1 +; Two 1.875 MB OTA slots (min_spiffs layout). NVS stays at 0x9000, which the web installer writes. +board_build.partitions = partitions.csv +lib_deps = + links2004/WebSockets@2.7.3 + bblanchon/ArduinoJson@7.4.3 + ; SSH logins to devices (SSH_CHECK, SSH_EXEC); libssh 0.11 with the mbedTLS backend, LGPL. + ewpa/LibSSH-ESP32@5.10.0 monitor_speed = 115200 +monitor_filters = esp32_exception_decoder build_unflags = -std=gnu++11 -build_flags = -std=gnu++17 - -; Generates data/cert/x509_crt_bundle.bin (embedded below) before every build. +build_flags = + -std=gnu++17 + -Wall + -DCORE_DEBUG_LEVEL=0 +; Generate the trusted root certificate bundle, the firmware version and the OTA signing key +; header before building. extra_scripts = - pre:data/cert/run_gen_script.py + pre:data/cert/run_gen_script.py + pre:scripts/version.py + pre:scripts/signing_key.py +; With this file (scripts/generate_signing_key.sh) the firmware only installs signed OTA updates. +custom_signing_key = keys/firmware-signing.pub.pem +board_build.embed_files = + data/cert/x509_crt_bundle.bin + +[env:esp32dev-qemu] +; For Espressif's QEMU (no Wi-Fi radio): the same firmware with Wi-Fi disabled, to test the serial +; console, Improv and settings without hardware. Not for real boards. +extends = env:esp32dev +build_flags = + ${env:esp32dev.build_flags} + -DNETIGNITE_EMULATOR +; A test key (its private half is in the repository), so the emulator test can sign updates. +custom_signing_key = test/emulator/keys/emulator-test.pub.pem -board_build.embed_txtfiles = - data/cert/x509_crt_bundle.bin \ No newline at end of file +[env:native] +; Host-side unit tests for the parts that don't need hardware (parsers, encoders). +platform = native +lib_deps = + bblanchon/ArduinoJson@7.4.3 +build_flags = + -std=gnu++17 + -DNETIGNITE_NATIVE_TEST +build_src_filter = -<*> +test_build_src = no +lib_extra_dirs = +lib_compat_mode = off diff --git a/scripts/check_size.py b/scripts/check_size.py new file mode 100644 index 0000000..40bbb17 --- /dev/null +++ b/scripts/check_size.py @@ -0,0 +1,36 @@ +""" +Fails if the firmware takes more than 85 % of an OTA app slot, so OTA updates keep room to grow. + + python scripts/check_size.py [.pio/build/esp32dev/firmware.bin] +""" + +import csv +import os +import sys + +LIMIT = 0.85 +ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + + +def app_slot_size(): + with open(os.path.join(ROOT, "partitions.csv"), newline="") as handle: + rows = (row for row in csv.reader(handle) if row and not row[0].strip().startswith("#")) + for row in rows: + name, kind, subtype, _offset, size = (cell.strip() for cell in row[:5]) + if kind == "app" and subtype == "ota_0": + return int(size, 0) + raise SystemExit("partitions.csv has no ota_0 app partition") + + +def main(): + firmware = sys.argv[1] if len(sys.argv) > 1 else os.path.join(ROOT, ".pio", "build", "esp32dev", "firmware.bin") + size = os.path.getsize(firmware) + slot = app_slot_size() + used = size / slot + print(f"Firmware: {size:,} bytes = {used:.1%} of the {slot:,}-byte OTA slot (limit {LIMIT:.0%})") + if used > LIMIT: + raise SystemExit("The firmware is too large for OTA updates to keep working.") + + +if __name__ == "__main__": + main() diff --git a/scripts/collect_release.py b/scripts/collect_release.py new file mode 100644 index 0000000..afd8a47 --- /dev/null +++ b/scripts/collect_release.py @@ -0,0 +1,92 @@ +""" +Collects the flash images of a build into a release directory and writes release.json, which the +NetIgnite server reads to offer the firmware in the web installer and for OTA updates. + + python scripts/collect_release.py [--signature firmware.sig] + +The files are the standard Arduino-ESP32 layout: bootloader at 0x1000, partition table at 0x8000, +OTA data (boot_app0) at 0xe000 and the app at 0x10000. The web installer adds an NVS image at +0x9000 with the server URL, a single-use enrollment token and the agent's name: the keys listed +in release.json's nvs.keys (the server writes no others). +""" + +import argparse +import glob +import hashlib +import json +import os +import shutil + +ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +BUILD = os.path.join(ROOT, ".pio", "build", "esp32dev") + +PARTS = [ + ("bootloader.bin", 0x1000), + ("partitions.bin", 0x8000), + ("boot_app0.bin", 0xE000), + ("firmware.bin", 0x10000), +] + + +def find_boot_app0(): + core = os.environ.get("PLATFORMIO_CORE_DIR", os.path.join(os.path.expanduser("~"), ".platformio")) + matches = glob.glob(os.path.join(core, "packages", "framework-arduinoespressif32*", "tools", "partitions", "boot_app0.bin")) + if not matches: + raise SystemExit("boot_app0.bin not found in the PlatformIO packages") + return matches[0] + + +def sha256(path): + digest = hashlib.sha256() + with open(path, "rb") as handle: + for chunk in iter(lambda: handle.read(65536), b""): + digest.update(chunk) + return digest.hexdigest() + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument("version") + parser.add_argument("output") + parser.add_argument("--signature", help="ECDSA P-256 signature (DER) of firmware.bin") + args = parser.parse_args() + + os.makedirs(args.output, exist_ok=True) + sources = { + "bootloader.bin": os.path.join(BUILD, "bootloader.bin"), + "partitions.bin": os.path.join(BUILD, "partitions.bin"), + "boot_app0.bin": find_boot_app0(), + "firmware.bin": os.path.join(BUILD, "firmware.bin"), + } + + parts = [] + for name, offset in PARTS: + target = os.path.join(args.output, name) + shutil.copyfile(sources[name], target) + parts.append({"file": name, "offset": offset, "size": os.path.getsize(target), "sha256": sha256(target)}) + + signature = None + if args.signature: + shutil.copyfile(args.signature, os.path.join(args.output, "firmware.sig")) + signature = "firmware.sig" + + release = { + "format": 1, + "name": "NetIgnite Agent", + "version": args.version.lstrip("v"), + "chip_family": "ESP32", + "board": "esp32dev", + "protocol": 2, + "parts": parts, + "nvs": {"offset": 0x9000, "size": 0x5000, "namespace": "netignite", "keys": ["server", "enroll", "name"]}, + "app": {"file": "firmware.bin", "signature": signature}, + } + with open(os.path.join(args.output, "release.json"), "w") as handle: + json.dump(release, handle, indent=2) + handle.write("\n") + + print(json.dumps(release, indent=2)) + + +if __name__ == "__main__": + main() diff --git a/scripts/generate_signing_key.sh b/scripts/generate_signing_key.sh new file mode 100644 index 0000000..44e5000 --- /dev/null +++ b/scripts/generate_signing_key.sh @@ -0,0 +1,15 @@ +#!/usr/bin/env sh +# Creates the ECDSA P-256 key pair for signing firmware releases. +# +# sh scripts/generate_signing_key.sh +# +# - firmware-signing-key.pem (private): store it as the repository secret FIRMWARE_SIGNING_KEY +# (Settings -> Secrets and variables -> Actions), then delete the file. Never commit it. +# - keys/firmware-signing.pub.pem (public): commit it. OTA updates only accept firmware signed with +# the matching private key. +set -eu +mkdir -p keys +openssl ecparam -name prime256v1 -genkey -noout -out firmware-signing-key.pem +openssl ec -in firmware-signing-key.pem -pubout -out keys/firmware-signing.pub.pem +echo "Private key: firmware-signing-key.pem (add as secret FIRMWARE_SIGNING_KEY, then delete)" +echo "Public key: keys/firmware-signing.pub.pem (commit)" diff --git a/scripts/signing_key.py b/scripts/signing_key.py new file mode 100644 index 0000000..e88630e --- /dev/null +++ b/scripts/signing_key.py @@ -0,0 +1,54 @@ +""" +PlatformIO pre-build script: compiles the OTA signing public key into the firmware. + +Generates `netignite_signing_key.h` in the build directory. If the environment's +`custom_signing_key` (a PEM ECDSA P-256 public key, see scripts/generate_signing_key.sh) exists, +the header defines NETIGNITE_OTA_PUBLIC_KEY and the firmware then only installs OTA updates with a +valid signature. Without the file, updates are checked by SHA-256 only. + +The emulator environment uses a test key whose private half is in the repository +(test/emulator/keys); it is refused for any other environment. +""" + +import os + +Import("env") # noqa: F821 - provided by PlatformIO (SCons) + +project_dir = env.subst("$PROJECT_DIR") # noqa: F821 +key_option = env.GetProjectOption("custom_signing_key", "").strip() # noqa: F821 +key_path = os.path.join(project_dir, key_option) if key_option else "" +build_flags = env.GetProjectOption("build_flags", []) # noqa: F821 +if isinstance(build_flags, str): + build_flags = [build_flags] +is_emulator = any("NETIGNITE_EMULATOR" in flag for flag in build_flags) + +out_dir = os.path.join(env.subst("$BUILD_DIR"), "generated") # noqa: F821 +os.makedirs(out_dir, exist_ok=True) +header = os.path.join(out_dir, "netignite_signing_key.h") + +lines = [ + "#pragma once", + "// Generated by scripts/signing_key.py. Do not edit.", +] + +if key_path and os.path.isfile(key_path): + if "test" in os.path.relpath(key_path, project_dir).replace("\\", "/").split("/") and not is_emulator: + raise SystemExit(f"Refusing to build {env.subst('$PIOENV')} with the test signing key {key_option}") # noqa: F821 + with open(key_path, "r", encoding="ascii") as handle: + pem = handle.read().strip() + "\n" + if "BEGIN PUBLIC KEY" not in pem: + raise SystemExit(f"{key_option} isn't a PEM public key") + escaped = pem.replace("\\", "\\\\").replace('"', '\\"').replace("\n", "\\n") + lines.append(f'#define NETIGNITE_OTA_PUBLIC_KEY "{escaped}"') + print(f"OTA updates must be signed with the key for {key_option}") +else: + if key_option: + print(f"No {key_option}: OTA updates are checked by SHA-256 only (no signature)") + +new_content = "\n".join(lines) + "\n" +old_content = open(header, encoding="ascii").read() if os.path.exists(header) else None +if new_content != old_content: + with open(header, "w", encoding="ascii", newline="\n") as handle: + handle.write(new_content) + +env.Append(CPPPATH=[out_dir]) # noqa: F821 diff --git a/scripts/version.py b/scripts/version.py new file mode 100644 index 0000000..fa61f3d --- /dev/null +++ b/scripts/version.py @@ -0,0 +1,42 @@ +""" +PlatformIO pre-build script: defines NETIGNITE_FW_VERSION. + +Release builds set the NETIGNITE_FW_VERSION environment variable (CI passes the tag without the +leading "v"). Local builds use `git describe` (e.g. "2.0.0-3-gabc1234-dirty"), or +"2.0.0-dev+" while the repository has no tags. +""" + +import os +import subprocess + +Import("env") # noqa: F821 - provided by PlatformIO (SCons) + +DEFAULT_BASE = "2.0.0-dev" + + +def git(*args): + try: + return subprocess.check_output( + ["git", *args], cwd=env.subst("$PROJECT_DIR"), stderr=subprocess.DEVNULL # noqa: F821 + ).decode().strip() + except Exception: + return "" + + +def firmware_version(): + version = os.environ.get("NETIGNITE_FW_VERSION", "").strip() + if version: + return version.lstrip("v") + + described = git("describe", "--tags", "--dirty", "--match", "v*") + if described: + return described.lstrip("v") + + commit = git("rev-parse", "--short", "HEAD") + dirty = ".dirty" if git("status", "--porcelain") else "" + return f"{DEFAULT_BASE}+{commit}{dirty}" if commit else DEFAULT_BASE + + +version = firmware_version() +print(f"NetIgnite firmware version: {version}") +env.Append(CPPDEFINES=[("NETIGNITE_FW_VERSION", env.StringifyMacro(version))]) # noqa: F821 diff --git a/src/Config.h b/src/Config.h new file mode 100644 index 0000000..043298f --- /dev/null +++ b/src/Config.h @@ -0,0 +1,89 @@ +#pragma once + +#include + +#ifndef NETIGNITE_FW_VERSION +#define NETIGNITE_FW_VERSION "2.0.0-dev" +#endif + +namespace config { + +constexpr const char* FIRMWARE_NAME = "NetIgnite Agent"; +constexpr const char* FIRMWARE_VERSION = NETIGNITE_FW_VERSION; + +/** + * NVS namespace and keys (max. 15 characters each). The web installer writes `server`, `enroll` + * and `name` into an NVS partition image (the keys scripts/collect_release.py lists in + * release.json), so these names are part of the provisioning format: change them only together + * with the server (server/lib/firmware/provisioning.ts). + */ +namespace keys { +constexpr const char* NAMESPACE = "netignite"; +constexpr const char* SERVER_URL = "server"; +constexpr const char* ENROLL_TOKEN = "enroll"; +constexpr const char* AGENT_ID = "agent_id"; +constexpr const char* TOKEN_ID = "token_id"; +constexpr const char* SECRET = "secret"; +constexpr const char* WIFI_SSID = "wifi_ssid"; +constexpr const char* WIFI_PASSWORD = "wifi_pass"; +constexpr const char* DEVICE_NAME = "name"; +constexpr const char* IP_MODE = "ip_mode"; +constexpr const char* STATIC_IP = "ip"; +constexpr const char* GATEWAY = "gateway"; +constexpr const char* NETMASK = "netmask"; +constexpr const char* DNS1 = "dns1"; +constexpr const char* DNS2 = "dns2"; +constexpr const char* LOG_LEVEL = "log_level"; +/** Version and partition of an installed update until it confirms itself (ota/Rollback.h). */ +constexpr const char* OTA_VERSION = "ota_ver"; +constexpr const char* OTA_PARTITION = "ota_part"; +} // namespace keys + +constexpr uint32_t SERIAL_BAUD = 115200; + +/** Wi-Fi: reconnect when the link has been down this long (the driver also retries on its own). */ +constexpr uint32_t WIFI_RECONNECT_AFTER_MS = 30000; +/** Wi-Fi: how long a new network (Improv, console) may take to connect before reverting. */ +constexpr uint32_t WIFI_TRY_CONNECT_MS = 20000; + +/** Server: reconnect backoff, doubled after each failed attempt. */ +constexpr uint32_t RECONNECT_MIN_MS = 5000; +constexpr uint32_t RECONNECT_MAX_MS = 60000; +/** The server sends a PING every 30 s; without any frame for this long the link is dead. */ +constexpr uint32_t SERVER_SILENCE_TIMEOUT_MS = 100000; + +/** Enrollment: retry interval after network errors. */ +constexpr uint32_t ENROLL_RETRY_MS = 30000; + +/** Status checks must finish before the server's 6 s request timeout. */ +constexpr uint32_t STATUS_BUDGET_MS = 4500; +constexpr uint32_t PING_TIMEOUT_MS = 1000; +constexpr uint32_t TCP_TIMEOUT_MS = 1500; + +/** + * OTA: a new firmware goes back to the previous one unless it reaches the server within this + * time. Short in emulator builds, which can't reach a server, so the test sees the rollback. + */ +#ifdef NETIGNITE_EMULATOR +constexpr uint32_t OTA_CONFIRM_TIMEOUT_MS = 20000; +#else +constexpr uint32_t OTA_CONFIRM_TIMEOUT_MS = 5 * 60 * 1000; +#endif +/** OTA: the download needs a second TLS session; below this the server connection is closed first. */ +constexpr uint32_t OTA_MIN_FREE_BLOCK = 50000; + +/** + * CONFIG_SET: a new server URL or IP setting is only saved once the agent reaches the server with + * it; otherwise the agent restarts with the previous settings after this time. + */ +constexpr uint32_t CONFIG_TRIAL_MS = 120000; + +/** Console: log lines are sent to the server in batches of at most this size, this often. */ +constexpr size_t LOG_EVENT_MAX_BYTES = 6000; +constexpr uint32_t LOG_EVENT_INTERVAL_MS = 300; + +/** Wake-on-LAN: rounds of packets per request, and the pause between them. */ +constexpr int WOL_ROUNDS = 3; +constexpr uint32_t WOL_ROUND_DELAY_MS = 100; + +} // namespace config diff --git a/src/agent/AgentClient.cpp b/src/agent/AgentClient.cpp new file mode 100644 index 0000000..b0fb249 --- /dev/null +++ b/src/agent/AgentClient.cpp @@ -0,0 +1,373 @@ +#include "agent/AgentClient.h" + +#include +#include + +#include "Config.h" +#include "core/CaBundle.h" +#include "core/DeviceInfo.h" +#include "core/Log.h" +#include "net/WakeSender.h" +#include "ota/OtaWriter.h" +#include "ota/Rollback.h" + +namespace protocol = netignite::protocol; + +void AgentClient::begin(Hooks hooks) { + hooks_ = std::move(hooks); + worker_.begin(); + ssh_.begin(); + ws_.onEvent([this](WStype_t type, uint8_t* payload, size_t length) { onEvent(type, payload, length); }); +} + +void AgentClient::start(const netignite::ServerUrl& server, const String& tokenId, const String& secret) { + stop(); + + server_ = server; + const std::string path = server.path("/ws/agent"); + authorization_ = std::string("Bearer netignite_agent_") + tokenId.c_str() + ":" + secret.c_str(); + authHeader_ = "Authorization: " + authorization_; + + // An empty subprotocol: the server doesn't negotiate one. + if (server.secure) { + ws_.beginSslWithBundle(server.host.c_str(), server.port, path.c_str(), caBundle::data(), caBundle::size(), ""); + } else { + ws_.begin(server.host.c_str(), server.port, path.c_str(), ""); + } + ws_.setExtraHeaders(authHeader_.c_str()); + + reconnectInterval_ = config::RECONNECT_MIN_MS; + ws_.setReconnectInterval(reconnectInterval_); + + running_ = true; + Log::info("Connecting to %s%s", server.origin().c_str(), path.c_str()); +} + +void AgentClient::stop() { + if (!running_) return; + running_ = false; + ws_.disconnect(); + connected_ = false; + helloSent_ = false; + logSubscribed_ = false; + generation_++; +} + +AgentClient::State AgentClient::state() const { + if (!running_) return State::Stopped; + return connected_ && helloSent_ ? State::Online : State::Connecting; +} + +void AgentClient::loop() { + if (!running_) return; + + // While an update downloads with the connection paused, don't reconnect. + if (otaPausedConnection_ && ota_.busy()) { + if (connected_) ws_.disconnect(); + } else { + otaPausedConnection_ = false; + ws_.loop(); + } + + StatusWorker::Done done; + while (worker_.poll(done)) { + if (done.generation != generation_ || !connected_) continue; + if (done.error[0] != '\0') { + send(protocol::encodeError(done.requestId, done.error)); + } else { + JsonDocument payload; + payload["online"] = done.online; + send(protocol::encodeResponse(done.requestId, payload.as())); + } + } + + SshWorker::Done ssh; + while (ssh_.poll(ssh)) { + if (ssh.generation != generation_ || !connected_) continue; + if (!ssh.ok) { + send(protocol::encodeError(ssh.requestId, ssh.error)); + } else if (ssh.exec) { + send(protocol::encodeEmptyResponse(ssh.requestId)); + } else { + JsonDocument payload; + payload["online"] = ssh.online; + if (ssh.online) { + if (ssh.hostKey[0] != '\0') payload["host_key"] = ssh.hostKey; + payload["authenticated"] = ssh.authenticated; + if (ssh.checkError[0] != '\0') payload["error"] = ssh.checkError; + } + send(protocol::encodeResponse(ssh.requestId, payload.as())); + } + } + + reportOta(); + if (logSubscribed_ && connected_ && millis() - lastLogSentAt_ >= config::LOG_EVENT_INTERVAL_MS) sendLogs(); + + // The server pings every 30 s; silence means the connection is dead even if TCP doesn't know. + if (connected_ && millis() - lastFrameAt_ > config::SERVER_SILENCE_TIMEOUT_MS) { + Log::warn("No message from the server for %lu s, reconnecting", + static_cast(config::SERVER_SILENCE_TIMEOUT_MS / 1000)); + ws_.disconnect(); + } +} + +void AgentClient::onEvent(WStype_t type, uint8_t* payload, size_t length) { + switch (type) { + case WStype_CONNECTED: + connected_ = true; + confirmed_ = false; + generation_++; + lastFrameAt_ = millis(); + reconnectInterval_ = config::RECONNECT_MIN_MS; + ws_.setReconnectInterval(reconnectInterval_); + sendHello(); + break; + + case WStype_DISCONNECTED: + if (connected_) { + if (!otaPausedConnection_) Log::warn("Disconnected from the server"); + } else { + Log::warn("Couldn't connect to the server, retrying in %lu s", + static_cast(reconnectInterval_ / 1000)); + // Exponential backoff while the server is unreachable or rejects the credential. + reconnectInterval_ = min(reconnectInterval_ * 2, config::RECONNECT_MAX_MS); + ws_.setReconnectInterval(reconnectInterval_); + } + connected_ = false; + helloSent_ = false; + logSubscribed_ = false; + generation_++; + break; + + case WStype_TEXT: + lastFrameAt_ = millis(); + handleText(std::string_view(reinterpret_cast(payload), length)); + break; + + case WStype_PING: + case WStype_PONG: + lastFrameAt_ = millis(); + break; + + case WStype_ERROR: + Log::warn("WebSocket error: %.*s", static_cast(length), payload ? reinterpret_cast(payload) : ""); + break; + + default: + break; + } +} + +void AgentClient::fillInfo(JsonObject info) { + deviceInfo::fill(info, settings_, wifi_); + info["signed_updates"] = OtaWriter::requiresSignature(); + if (rollback::pending()) info["ota_probation"] = true; + if (rollback::failedUpdate().length() > 0) info["ota_failed_version"] = rollback::failedUpdate(); +} + +void AgentClient::sendHello() { + JsonDocument hello; + hello["protocol"] = protocol::VERSION; + hello["kind"] = protocol::AGENT_KIND; + hello["version"] = config::FIRMWARE_VERSION; + + JsonArray capabilities = hello["capabilities"].to(); + for (const char* capability : protocol::CAPABILITIES) capabilities.add(capability); + + fillInfo(hello["info"].to()); + + send(protocol::encodeEvent("HELLO", hello.as())); + helloSent_ = true; + Log::info("Connected to the server as agent %lu", static_cast(settings_.agentId())); +} + +void AgentClient::send(const std::string& frame) { + if (!connected_) return; + ws_.sendTXT(frame.c_str(), frame.size()); +} + +void AgentClient::sendLogs() { + lastLogSentAt_ = millis(); + std::vector lines; + const auto result = Log::read(logCursor_, config::LOG_EVENT_MAX_BYTES, lines); + logCursor_ = result.next; + if (lines.empty() && result.dropped == 0) return; + + JsonDocument payload; + JsonArray array = payload["lines"].to(); + for (const std::string& line : lines) array.add(line); + if (result.dropped > 0) payload["dropped"] = result.dropped; + send(protocol::encodeEvent("LOG", payload.as())); +} + +void AgentClient::reportOta() { + const OtaUpdater::Progress progress = ota_.progress(); + if (progress.state == OtaUpdater::State::Idle) return; + + const bool finished = progress.state != OtaUpdater::State::Downloading; + const bool changed = progress.revision != otaReportedRevision_; + if (changed && connected_ && helloSent_ && (finished || millis() - otaReportedAt_ >= 1000)) { + otaReportedRevision_ = progress.revision; + otaReportedAt_ = millis(); + + JsonDocument payload; + payload["version"] = progress.version; + payload["state"] = OtaUpdater::stateName(progress.state); + payload["percent"] = progress.percent; + if (!progress.error.empty()) payload["error"] = progress.error; + send(protocol::encodeEvent("OTA_PROGRESS", payload.as())); + } + + if (progress.state == OtaUpdater::State::Installed && !otaInstalledHandled_) { + otaInstalledHandled_ = true; + if (hooks_.updateInstalled) hooks_.updateInstalled(progress.version); + } +} + +void AgentClient::handleText(std::string_view text) { + JsonDocument doc; + protocol::Frame frame; + std::string error; + + if (!protocol::parseFrame(text, doc, frame, error)) { + Log::warn("Ignored an invalid frame from the server: %s", error.c_str()); + return; + } + if (frame.type != protocol::FrameType::Request) return; + + // The server only sends requests after accepting the credential and HELLO. + if (!confirmed_) { + confirmed_ = true; + if (hooks_.serverConfirmed) hooks_.serverConfirmed(); + } + + Log::debug("Request %lu: %s", static_cast(frame.id), frame.cmd.c_str()); + handleRequest(frame); +} + +void AgentClient::handleRequest(const protocol::Frame& frame) { + std::string error; + const std::string& cmd = frame.cmd; + + if (cmd == "PING") { + send(protocol::encodeEmptyResponse(frame.id)); + return; + } + + if (cmd == "WAKE") { + protocol::WakeRequest wake; + if (!protocol::parseWakeRequest(frame.payload, wake, error)) { + send(protocol::encodeError(frame.id, error)); + return; + } + String sendError; + if (wake::send(wake.mac, wake.port, sendError)) { + send(protocol::encodeEmptyResponse(frame.id)); + } else { + send(protocol::encodeError(frame.id, sendError.c_str())); + } + return; + } + + if (cmd == "STATUS") { + protocol::StatusRequest status; + if (!protocol::parseStatusRequest(frame.payload, status, error)) { + send(protocol::encodeError(frame.id, error)); + return; + } + if (!worker_.submit(frame.id, generation_, status)) { + send(protocol::encodeError(frame.id, "Too many status checks at once")); + } + return; + } + + if (cmd == "SSH_CHECK" || cmd == "SSH_EXEC") { + const bool exec = cmd == "SSH_EXEC"; + protocol::SshRequest request; + if (!protocol::parseSshRequest(frame.payload, exec, request, error)) { + send(protocol::encodeError(frame.id, error)); + return; + } + if (!ssh_.submit(frame.id, generation_, exec, request)) { + send(protocol::encodeError(frame.id, "Another SSH login is running")); + } + return; + } + + if (cmd == "GET_INFO") { + JsonDocument payload; + payload["version"] = config::FIRMWARE_VERSION; + fillInfo(payload["info"].to()); + send(protocol::encodeResponse(frame.id, payload.as())); + return; + } + + if (cmd == "GET_LOGS" || cmd == "LOG_SUBSCRIBE") { + bool enabled = true; + if (cmd == "LOG_SUBSCRIBE" && !protocol::parseLogSubscribe(frame.payload, enabled, error)) { + send(protocol::encodeError(frame.id, error)); + return; + } + + JsonDocument payload; + if (enabled) { + // Everything stored so far; a subscription continues from there with LOG events. + std::vector lines; + const auto result = Log::read(0, Log::RING_BYTES, lines); + JsonArray array = payload["lines"].to(); + for (const std::string& line : lines) array.add(line); + payload["level"] = Log::levelName(Log::level()); + if (cmd == "LOG_SUBSCRIBE") logCursor_ = result.next; + } + if (cmd == "LOG_SUBSCRIBE") logSubscribed_ = enabled; + send(protocol::encodeResponse(frame.id, payload.as())); + return; + } + + if (cmd == "CONFIG_SET") { + protocol::ConfigRequest request; + if (!protocol::parseConfigRequest(frame.payload, request, error) || + !(hooks_.applyConfig && hooks_.applyConfig(request, error))) { + send(protocol::encodeError(frame.id, error.empty() ? "Can't change the configuration" : error)); + return; + } + send(protocol::encodeEmptyResponse(frame.id)); + return; + } + + if (cmd == "REBOOT" || cmd == "FACTORY_RESET") { + send(protocol::encodeEmptyResponse(frame.id)); + Log::info("%s requested by the server", cmd == "REBOOT" ? "Restart" : "Factory reset"); + if (hooks_.restart) hooks_.restart(cmd == "FACTORY_RESET"); + return; + } + + if (cmd == "OTA_BEGIN") { + protocol::OtaRequest request; + if (!protocol::parseOtaRequest(frame.payload, request, error)) { + send(protocol::encodeError(frame.id, error)); + return; + } + if (rollback::pending()) { + send(protocol::encodeError(frame.id, "The current firmware isn't confirmed yet; try again shortly")); + return; + } + + // Two TLS sessions at once (WebSocket + download) need memory; without it, pause the connection. + const bool pause = server_.secure && ESP.getMaxAllocHeap() < config::OTA_MIN_FREE_BLOCK; + if (!ota_.start(server_, authorization_, request, error)) { + send(protocol::encodeError(frame.id, error)); + return; + } + otaInstalledHandled_ = false; + send(protocol::encodeEmptyResponse(frame.id)); + if (pause) { + Log::info("Pausing the server connection during the download (largest free block: %lu bytes)", + static_cast(ESP.getMaxAllocHeap())); + otaPausedConnection_ = true; + } + return; + } + + send(protocol::encodeError(frame.id, "Unknown command " + cmd)); +} diff --git a/src/agent/AgentClient.h b/src/agent/AgentClient.h new file mode 100644 index 0000000..16ad8af --- /dev/null +++ b/src/agent/AgentClient.h @@ -0,0 +1,88 @@ +#pragma once + +#include +#include + +#include +#include +#include + +#include "agent/SshWorker.h" +#include "agent/StatusWorker.h" +#include "net/WifiLink.h" +#include "netignite/Protocol.h" +#include "netignite/ServerUrl.h" +#include "ota/OtaUpdater.h" + +/** + * The connection to the NetIgnite server: one WebSocket to `/ws/agent` speaking protocol v2. + * Sends HELLO on connect and answers the server's requests (wake, status checks, OTA updates, + * logs, configuration, reboot, SSH logins to devices). + */ +class AgentClient { + public: + enum class State { Stopped, Connecting, Online }; + + /** Device-wide actions, carried out by the main loop. */ + struct Hooks { + /** Validates and schedules a CONFIG_SET. False (with an error) if it can't be applied. */ + std::function applyConfig; + /** Restart soon (REBOOT); with `factoryReset`, erase all settings first. */ + std::function restart; + /** The server answered on this connection: the credential, server URL and network work. */ + std::function serverConfirmed; + /** An OTA update was installed; restart into it. */ + std::function updateInstalled; + }; + + AgentClient(Settings& settings, WifiLink& wifi) : settings_(settings), wifi_(wifi) {} + + void begin(Hooks hooks); + void start(const netignite::ServerUrl& server, const String& tokenId, const String& secret); + void stop(); + void loop(); + + State state() const; + bool running() const { return running_; } + const OtaUpdater& ota() const { return ota_; } + + private: + Settings& settings_; + WifiLink& wifi_; + Hooks hooks_; + WebSocketsClient ws_; + StatusWorker worker_; + SshWorker ssh_; + OtaUpdater ota_; + netignite::ServerUrl server_; + + bool running_ = false; + bool connected_ = false; + bool helloSent_ = false; + bool confirmed_ = false; + uint32_t generation_ = 0; + uint32_t reconnectInterval_ = 0; + uint32_t lastFrameAt_ = 0; + std::string authHeader_; + std::string authorization_; + + // Console log stream (LOG_SUBSCRIBE) + bool logSubscribed_ = false; + uint32_t logCursor_ = 0; + uint32_t lastLogSentAt_ = 0; + + // OTA: progress reported to the server; the connection is paused if memory is short. + uint32_t otaReportedRevision_ = 0; + uint32_t otaReportedAt_ = 0; + bool otaPausedConnection_ = false; + bool otaInstalledHandled_ = false; + + void onEvent(WStype_t type, uint8_t* payload, size_t length); + void handleText(std::string_view text); + void handleRequest(const netignite::protocol::Frame& frame); + void sendHello(); + void send(const std::string& frame); + void sendLogs(); + void reportOta(); + void fillInfo(JsonObject info); +}; diff --git a/src/agent/Enrollment.cpp b/src/agent/Enrollment.cpp new file mode 100644 index 0000000..ad78bab --- /dev/null +++ b/src/agent/Enrollment.cpp @@ -0,0 +1,104 @@ +#include "agent/Enrollment.h" + +#include +#include +#include +#include +#include + +#include + +#include "core/CaBundle.h" +#include "core/Log.h" +#include "netignite/Hex.h" +#include "netignite/Protocol.h" + +namespace enrollment { + +namespace { + +String generateSecret() { + uint8_t bytes[32]; + esp_fill_random(bytes, sizeof(bytes)); + return netignite::toHex(bytes, sizeof(bytes)).c_str(); +} + +} // namespace + +Result enroll(const netignite::ServerUrl& server, const String& enrollmentToken) { + Result result; + const String secret = generateSecret(); + const std::string url = server.url("/api/v1/agents/enroll"); + + std::unique_ptr client; + if (server.secure) { + auto* secure = new NetworkClientSecure(); + secure->setCACertBundle(caBundle::data(), caBundle::size()); + client.reset(secure); + } else { + client.reset(new NetworkClient()); + } + + HTTPClient http; + http.setTimeout(15000); + http.setReuse(false); + if (!http.begin(*client, url.c_str())) { + result.message = "Invalid server URL"; + return result; + } + http.addHeader("Content-Type", "application/json"); + + JsonDocument body; + body["enrollment_token"] = enrollmentToken; + body["secret"] = secret; + // The server refuses a token of a Linux agent (409) without using it up. + body["kind"] = netignite::protocol::AGENT_KIND; + String payload; + serializeJson(body, payload); + + Log::info("Enrolling with %s", server.origin().c_str()); + const int status = http.POST(payload); + const String response = status > 0 ? http.getString() : String(); + http.end(); + + if (status <= 0) { + result.message = String("Request failed: ") + HTTPClient::errorToString(status); + return result; + } + + JsonDocument answer; + const bool parsed = !deserializeJson(answer, response); + const char* message = parsed ? answer["message"].as() : nullptr; + + if (status == 401 || status == 400 || status == 409) { + result.status = Result::Status::Rejected; + result.message = message ? message : "Enrollment token rejected"; + return result; + } + if (status != 200 || !parsed || !answer["success"].as()) { + result.message = String("Server answered ") + status + (message ? String(": ") + message : String()); + return result; + } + + const char* kind = answer["data"]["kind"]; + const char* tokenId = answer["data"]["token_id"]; + const uint32_t agentId = answer["data"]["agent_id"] | 0u; + if (!tokenId || !*tokenId || agentId == 0) { + result.message = "Unexpected answer from the server"; + return result; + } + if (!kind || String(kind) != netignite::protocol::AGENT_KIND) { + // The token belongs to a Linux agent; the credential is useless here. + result.status = Result::Status::Rejected; + result.message = "This enrollment token is for a different kind of agent"; + return result; + } + + result.status = Result::Status::Ok; + result.agentId = agentId; + result.tokenId = tokenId; + result.secret = secret; + return result; +} + +} // namespace enrollment diff --git a/src/agent/Enrollment.h b/src/agent/Enrollment.h new file mode 100644 index 0000000..f433439 --- /dev/null +++ b/src/agent/Enrollment.h @@ -0,0 +1,31 @@ +#pragma once + +#include + +#include "netignite/ServerUrl.h" + +/** + * Enrollment: the agent generates its own secret (hardware RNG) and exchanges a single-use + * enrollment token for a credential at `POST /api/v1/agents/enroll`. The secret is only sent + * once, over this request; the server keeps a hash of it. + */ +namespace enrollment { + +struct Result { + enum class Status { + Ok, + /** The server rejected the token (unknown, used or expired): a new one is needed. */ + Rejected, + /** Network or server error: try again later. */ + Failed, + }; + Status status = Status::Failed; + String message; + uint32_t agentId = 0; + String tokenId; + String secret; +}; + +Result enroll(const netignite::ServerUrl& server, const String& enrollmentToken); + +} // namespace enrollment diff --git a/src/agent/SshWorker.cpp b/src/agent/SshWorker.cpp new file mode 100644 index 0000000..55906e2 --- /dev/null +++ b/src/agent/SshWorker.cpp @@ -0,0 +1,200 @@ +#include "agent/SshWorker.h" + +#include +#include + +#include + +#include "core/Log.h" + +namespace protocol = netignite::protocol; + +namespace { + +constexpr uint32_t TASK_STACK = 40 * 1024; +constexpr long CONNECT_TIMEOUT_S = 5; +/** SSH_EXEC: how long to wait for the command's exit status (shutdown usually drops the link). */ +constexpr uint32_t EXEC_WAIT_MS = 5000; + +/** Overwrites a secret before its memory is freed. */ +void wipe(std::string& secret) { + volatile char* data = secret.data(); + for (size_t i = 0; i < secret.size(); i++) data[i] = 0; + secret.clear(); +} + +/** Closes the session on every path out of a login. */ +struct Session { + ssh_session session = ssh_new(); + ~Session() { + if (!session) return; + if (ssh_is_connected(session)) ssh_disconnect(session); + ssh_free(session); + } +}; + +} // namespace + +void SshWorker::begin() { + results_ = xQueueCreate(2, sizeof(Done)); + libssh_begin(); +} + +bool SshWorker::submit(uint32_t requestId, uint32_t generation, bool exec, const protocol::SshRequest& request) { + bool expected = false; + if (!busy_.compare_exchange_strong(expected, true)) return false; + + auto* job = new Job{this, requestId, generation, exec, request}; + if (xTaskCreate(run, "ssh", TASK_STACK, job, 1, nullptr) != pdPASS) { + wipe(job->request.secret); + delete job; + busy_ = false; + return false; + } + return true; +} + +bool SshWorker::poll(Done& out) { + return results_ && xQueueReceive(results_, &out, 0) == pdTRUE; +} + +void SshWorker::run(void* arg) { + auto* job = static_cast(arg); + + Done done = {}; + done.requestId = job->requestId; + done.generation = job->generation; + done.exec = job->exec; + login(*job, done); + + SshWorker* worker = job->worker; + wipe(job->request.secret); + delete job; + + xQueueSend(worker->results_, &done, portMAX_DELAY); + worker->busy_ = false; + vTaskDelete(nullptr); +} + +void SshWorker::login(const Job& job, Done& done) { + const protocol::SshRequest& request = job.request; + auto fail = [&done](const char* message) { + done.ok = false; + strlcpy(done.error, message, sizeof(done.error)); + }; + + Log::info("SSH %s: %s@%s:%u", job.exec ? "command" : "check", request.username.c_str(), request.host.c_str(), + request.port); + + Session ssh; + if (!ssh.session) return fail("Not enough memory for SSH"); + + int port = request.port; + long timeout = CONNECT_TIMEOUT_S; + bool processConfig = false; + ssh_options_set(ssh.session, SSH_OPTIONS_HOST, request.host.c_str()); + ssh_options_set(ssh.session, SSH_OPTIONS_PORT, &port); + ssh_options_set(ssh.session, SSH_OPTIONS_USER, request.username.c_str()); + ssh_options_set(ssh.session, SSH_OPTIONS_TIMEOUT, &timeout); + ssh_options_set(ssh.session, SSH_OPTIONS_PROCESS_CONFIG, &processConfig); + + if (ssh_connect(ssh.session) != SSH_OK) { + if (!job.exec) { + // The SSH port didn't accept a connection: the device is offline (for this check). + done.ok = true; + done.online = false; + return; + } + return fail((std::string("Can't connect: ") + ssh_get_error(ssh.session)).c_str()); + } + done.online = true; + + // The host key, before any credential is sent. + ssh_key serverKey = nullptr; + unsigned char* hash = nullptr; + size_t hashLength = 0; + if (ssh_get_server_publickey(ssh.session, &serverKey) != SSH_OK || + ssh_get_publickey_hash(serverKey, SSH_PUBLICKEY_HASH_SHA256, &hash, &hashLength) != SSH_OK || hashLength != 32) { + ssh_key_free(serverKey); + ssh_clean_pubkey_hash(&hash); + return fail("Can't read the device's host key"); + } + const std::string hostKey = protocol::sshFingerprint(hash); + ssh_clean_pubkey_hash(&hash); + ssh_key_free(serverKey); + strlcpy(done.hostKey, hostKey.c_str(), sizeof(done.hostKey)); + + if (!request.hostKey.empty() && hostKey != request.hostKey) { + Log::warn("SSH: %s presented another host key (%s)", request.host.c_str(), hostKey.c_str()); + if (job.exec) return fail("host key changed"); + done.ok = true; + strlcpy(done.checkError, "host_key_mismatch", sizeof(done.checkError)); + return; + } + + int auth = SSH_AUTH_ERROR; + if (request.auth == protocol::SshRequest::Auth::Password) { + auth = ssh_userauth_password(ssh.session, nullptr, request.secret.c_str()); + } else { + ssh_key key = nullptr; + if (ssh_pki_import_privkey_base64(request.secret.c_str(), nullptr, nullptr, nullptr, &key) != SSH_OK) { + return fail("The private key can't be read"); + } + auth = ssh_userauth_publickey(ssh.session, nullptr, key); + ssh_key_free(key); + } + if (auth != SSH_AUTH_SUCCESS) { + if (job.exec) return fail("authentication failed"); + done.ok = true; + strlcpy(done.checkError, "auth_failed", sizeof(done.checkError)); + return; + } + done.authenticated = true; + + if (!job.exec) { + // Log in only: never open a session, so forced commands in authorized_keys don't run. + done.ok = true; + return; + } + + ssh_channel channel = ssh_channel_new(ssh.session); + if (!channel || ssh_channel_open_session(channel) != SSH_OK) { + if (channel) ssh_channel_free(channel); + return fail("The device refused a session"); + } + const std::string command = protocol::sshCommand(request); + if (ssh_channel_request_exec(channel, command.c_str()) != SSH_OK) { + ssh_channel_free(channel); + return fail("The device refused the command"); + } + + // Wait for the exit status. Shutdown often drops the connection first: that counts as success. + std::string stderrText; + char buffer[128]; + bool eof = false; + const uint32_t start = millis(); + while (millis() - start < EXEC_WAIT_MS) { + const int err = ssh_channel_read_timeout(channel, buffer, sizeof(buffer), 1, 200); + if (err > 0 && stderrText.size() < 200) stderrText.append(buffer, std::min(err, 200 - stderrText.size())); + const int out = ssh_channel_read_timeout(channel, buffer, sizeof(buffer), 0, 50); + if (err == SSH_ERROR || out == SSH_ERROR || !ssh_is_connected(ssh.session)) break; + if (ssh_channel_is_eof(channel)) { + eof = true; + break; + } + } + + uint32_t exitCode = 0; + const bool exited = eof && ssh_channel_get_exit_state(channel, &exitCode, nullptr, nullptr) == SSH_OK; + ssh_channel_free(channel); + + if (exited && exitCode != 0) { + while (!stderrText.empty() && (stderrText.back() == '\n' || stderrText.back() == '\r')) stderrText.pop_back(); + const std::string message = + "Exit code " + std::to_string(exitCode) + (stderrText.empty() ? std::string() : ": " + stderrText); + Log::warn("SSH: %s failed: %s", command.c_str(), message.c_str()); + return fail(message.c_str()); + } + Log::info("SSH: ran \"%s\" on %s", command.c_str(), request.host.c_str()); + done.ok = true; +} diff --git a/src/agent/SshWorker.h b/src/agent/SshWorker.h new file mode 100644 index 0000000..b2118c0 --- /dev/null +++ b/src/agent/SshWorker.h @@ -0,0 +1,51 @@ +#pragma once + +#include + +#include + +#include "netignite/Protocol.h" + +/** + * Runs SSH_CHECK and SSH_EXEC with libssh, one at a time, each in a task of its own (libssh needs + * a large stack, which is only allocated while a login runs). The main loop submits jobs and polls + * for results. Passwords and keys are wiped from memory after each job. + */ +class SshWorker { + public: + struct Done { + uint32_t requestId; + uint32_t generation; + bool exec; + /** SSH_EXEC succeeded / SSH_CHECK ran. Otherwise `error` says why. */ + bool ok; + char error[200]; + + // SSH_CHECK + bool online; + bool authenticated; + char hostKey[64]; + /** `host_key_mismatch`, `auth_failed` or a short message; empty on success. */ + char checkError[96]; + }; + + void begin(); + /** False if a login is already running or the task can't start. */ + bool submit(uint32_t requestId, uint32_t generation, bool exec, const netignite::protocol::SshRequest& request); + bool poll(Done& out); + + private: + struct Job { + SshWorker* worker; + uint32_t requestId; + uint32_t generation; + bool exec; + netignite::protocol::SshRequest request; + }; + + QueueHandle_t results_ = nullptr; + std::atomic busy_{false}; + + static void run(void* job); + static void login(const Job& job, Done& done); +}; diff --git a/src/agent/StatusWorker.cpp b/src/agent/StatusWorker.cpp new file mode 100644 index 0000000..a7b6c30 --- /dev/null +++ b/src/agent/StatusWorker.cpp @@ -0,0 +1,51 @@ +#include "agent/StatusWorker.h" + +#include "net/Probes.h" + +namespace { +constexpr UBaseType_t QUEUE_LENGTH = 8; +constexpr uint32_t TASK_STACK = 6144; +} // namespace + +void StatusWorker::begin() { + jobs_ = xQueueCreate(QUEUE_LENGTH, sizeof(Job*)); + results_ = xQueueCreate(QUEUE_LENGTH, sizeof(Done)); + xTaskCreate(run, "status", TASK_STACK, this, 2, nullptr); +} + +bool StatusWorker::submit(uint32_t requestId, uint32_t generation, const netignite::protocol::StatusRequest& request) { + auto* job = new Job{requestId, generation, request}; + if (xQueueSend(jobs_, &job, 0) != pdTRUE) { + delete job; + return false; + } + return true; +} + +bool StatusWorker::poll(Done& out) { + return xQueueReceive(results_, &out, 0) == pdTRUE; +} + +void StatusWorker::run(void* arg) { + auto* self = static_cast(arg); + for (;;) { + Job* job = nullptr; + if (xQueueReceive(self->jobs_, &job, portMAX_DELAY) != pdTRUE || !job) continue; + + const probes::Result result = probes::check(job->request); + + Done done = {}; + done.requestId = job->requestId; + done.generation = job->generation; + done.online = result.online; + strlcpy(done.error, result.error.c_str(), sizeof(done.error)); + delete job; + + // The main loop drains results continuously; drop one if it ever falls behind. + if (xQueueSend(self->results_, &done, pdMS_TO_TICKS(1000)) != pdTRUE) { + Done stale; + xQueueReceive(self->results_, &stale, 0); + xQueueSend(self->results_, &done, 0); + } + } +} diff --git a/src/agent/StatusWorker.h b/src/agent/StatusWorker.h new file mode 100644 index 0000000..2db79e6 --- /dev/null +++ b/src/agent/StatusWorker.h @@ -0,0 +1,37 @@ +#pragma once + +#include + +#include "netignite/Protocol.h" + +/** + * Runs STATUS checks (blocking ping/TCP/DNS) in their own FreeRTOS task, one at a time. The main + * loop submits jobs and polls for results, so the WebSocket keeps being served meanwhile. + */ +class StatusWorker { + public: + struct Done { + uint32_t requestId; + /** Connection the request came from; results for older connections are dropped. */ + uint32_t generation; + bool online; + char error[96]; + }; + + void begin(); + /** False if too many checks are queued. */ + bool submit(uint32_t requestId, uint32_t generation, const netignite::protocol::StatusRequest& request); + bool poll(Done& out); + + private: + struct Job { + uint32_t requestId; + uint32_t generation; + netignite::protocol::StatusRequest request; + }; + + QueueHandle_t jobs_ = nullptr; + QueueHandle_t results_ = nullptr; + + static void run(void* self); +}; diff --git a/src/agent/agent.h b/src/agent/agent.h deleted file mode 100644 index 76ddbe1..0000000 --- a/src/agent/agent.h +++ /dev/null @@ -1,122 +0,0 @@ -#ifndef AGENT_H -#define AGENT_H - -#include - -#include "utils/logger.h" -#include "utils/wol.h" -#include "utils/utils.h" -#include "agent/message.h" -#include "agent/cmd_registry.h" - -extern const uint8_t rootca_crt_bundle_start[] asm( - "_binary_data_cert_x509_crt_bundle_bin_start"); - -extern WebSocketsClient webSocket; - -struct AgentConfig { - const char* host; - uint16_t port; - bool useSSL; - const char* authId; - const char* authSecret; -}; - -class Agent { - public: - - static void setup(const AgentConfig& config) { - - host = config.host; - port = config.port; - useSSL = config.useSSL; - authId = config.authId; - authSecret = config.authSecret; - - if (strlen(authId) == 0 || strlen(authSecret) == 0) { - Logger::errorln("Missing credentials!"); - return; - } - - if (useSSL) { - webSocket.beginSslWithBundle(host, port, "/api/agent-control-service", rootca_crt_bundle_start, ""); - } else { - webSocket.begin(host, port, "/api/agent-control-service"); - } - - webSocket.setAuthorization(authId, authSecret); - - webSocket.onEvent(webSocketEvent); - } - - static void loop() { - webSocket.loop(); - } - - private: - - static inline const char* host = nullptr; - static inline uint16_t port = 0; - static inline bool useSSL = false; - static inline const char* authId = nullptr; - static inline const char* authSecret = nullptr; - - static void webSocketEvent(WStype_t type, uint8_t* payload, size_t length) { - - switch(type) { - case WStype_DISCONNECTED: - - webSocket.setReconnectInterval(60000); // 60 seconds - - Logger::infoln("Websocket Disconnected! Retrying in 60 seconds..."); - break; - - case WStype_CONNECTED: - - Logger::infoln("Websocket Connected to " + String(host) + ":" + String(port) + " SSL: " + String(useSSL ? "true" : "false")); - break; - - case WStype_TEXT: { - - AgentMessage* message = nullptr; - - if (AgentMessage::decode((const char*)payload, message)) { - - JsonDocument responsePayload; - - if (!CommandRegistry::executeCommand(message->cmd.c_str(), message->payload, responsePayload)) { - responsePayload["status"] = "ERROR"; - responsePayload["message"] = "Unknown command"; - } - - const AgentMessage responseMessage(message->cmd, message->id, responsePayload); - - String encodedResponse = responseMessage.encode(); - webSocket.sendTXT(encodedResponse); - } - - delete message; - message = nullptr; - - break; - } - - case WStype_ERROR: - - Logger::errorln("Websocket Error: " + String((char*)payload)); - - break; - - case WStype_BIN: - case WStype_FRAGMENT_TEXT_START: - case WStype_FRAGMENT_BIN_START: - case WStype_FRAGMENT: - case WStype_FRAGMENT_FIN: - break; - } - - } - -}; - -#endif // AGENT_H \ No newline at end of file diff --git a/src/agent/cmd_registry.h b/src/agent/cmd_registry.h deleted file mode 100644 index 82a878c..0000000 --- a/src/agent/cmd_registry.h +++ /dev/null @@ -1,49 +0,0 @@ -#ifndef CMD_REGISTRY_H -#define CMD_REGISTRY_H - -#include -#include - -#include "commands/wakeup.h" -#include "commands/heartbeat.h" -#include "commands/get_status.h" - -using FuncPtr = JsonDocument (*)(JsonDocument& payload); - -struct CommandEntry { - const char *name; - FuncPtr handler; -}; - -class CommandRegistry { - public: - - static const CommandEntry* getCommand(const char* cmd) { - if (!cmd) return nullptr; - for (const auto& entry : registry) { - if (strcmp(entry.name, cmd) == 0) { - return &entry; - } - } - return nullptr; - } - - static bool executeCommand(const char* cmd, JsonDocument& payload, JsonDocument& result) { - const CommandEntry* entry = getCommand(cmd); - if (entry) { - result = entry->handler(payload); - return true; - } - return false; - } - - private: - static const inline CommandEntry registry[] = { - {"WAKEUP", wakeup}, - {"HEARTBEAT", heartbeat}, - {"GET_STATUS", get_status}, - }; -}; - - -#endif // CMD_REGISTRY_H \ No newline at end of file diff --git a/src/agent/commands/get_status.h b/src/agent/commands/get_status.h deleted file mode 100644 index a974630..0000000 --- a/src/agent/commands/get_status.h +++ /dev/null @@ -1,53 +0,0 @@ -#ifndef GET_STATUS_H -#define GET_STATUS_H - -#include -#include -#include "utils/inputValidation.h" -#include "utils/utils.h" -#include "utils/icmp.h" - -JsonDocument get_status(JsonDocument& input) { - - JsonDocument response; - - response["status"] = "ERROR"; - response["online"] = "unknown"; - - if (!input["address"].is()) { - response["message"] = "Missing or invalid payload"; - return response; - } - - if (InputValidation::ipv4Address(input["address"])) { - - IPAddress ipAddress; - if (!ipAddress.fromString(static_cast(input["address"]))) { - response["message"] = "Invalid IPv4 address format"; - return response; - } - - response["online"] = ICMP::ping(ipAddress, 1) ? "online" : "offline"; - - } else if (InputValidation::macAddress(input["address"])) { - - String linkLocal = Utils::macToLinkLocalIPv6(input["address"]); - IPv6Address linkLocalAddress; - if (linkLocal.length() == 0 || !linkLocalAddress.fromString(linkLocal.c_str())) { - response["message"] = "Invalid MAC address format"; - return response; - } - - response["online"] = ICMP::ping(linkLocalAddress, 1) ? "online" : "offline"; - - } else { - response["message"] = "Invalid address format: must be a valid IPv4 address or a MAC address"; - return response; - } - - response["status"] = "OK"; - response["message"] = "Status retrieved successfully"; - return response; -} - -#endif // GET_STATUS_H \ No newline at end of file diff --git a/src/agent/commands/heartbeat.h b/src/agent/commands/heartbeat.h deleted file mode 100644 index b242ece..0000000 --- a/src/agent/commands/heartbeat.h +++ /dev/null @@ -1,16 +0,0 @@ -#ifndef HEARTBEAT_H -#define HEARTBEAT_H - -#include -#include - -JsonDocument heartbeat(JsonDocument& input) { - - JsonDocument response; - - // currently no handle needed - - return response; -} - -#endif // HEARTBEAT_H \ No newline at end of file diff --git a/src/agent/commands/wakeup.h b/src/agent/commands/wakeup.h deleted file mode 100644 index c287fdd..0000000 --- a/src/agent/commands/wakeup.h +++ /dev/null @@ -1,34 +0,0 @@ -#ifndef WAKEUP_H -#define WAKEUP_H - -#include -#include -#include "utils/wol.h" - -JsonDocument wakeup(JsonDocument& input) { - - JsonDocument response; - - if (!input["macAddress"].is() || !input["port"].is()) { - response["status"] = "ERROR"; - response["message"] = "Missing or invalid payload"; - return response; - } - - const char *macAddress = input["macAddress"]; - uint16_t port = input["port"]; - - const bool result = WOL::wakeup(macAddress, port); - if (!result) { - response["status"] = "ERROR"; - response["message"] = "Wakeup failed: Invalid MAC address or port"; - return response; - } - - response["status"] = "OK"; - response["message"] = "Wakeup successful"; - - return response; -} - -#endif // WAKEUP_H \ No newline at end of file diff --git a/src/agent/message.h b/src/agent/message.h deleted file mode 100644 index b916c3e..0000000 --- a/src/agent/message.h +++ /dev/null @@ -1,66 +0,0 @@ -#ifndef MESSAGE_H -#define MESSAGE_H - -#include -#include -#include -#include - -class AgentMessage { - public: - - const String cmd; - const uint32_t id; - JsonDocument payload; - - AgentMessage(const String& cmd, uint32_t id, const JsonDocument& payloadDoc) - : cmd(cmd), id(id) { - this->payload.set(payloadDoc); - } - - String encode() const { - String encodedString = cmd + ":" + String(id) + ":"; - - String serializedPayload; - serializeJson(payload, serializedPayload); - - encodedString += serializedPayload; - return encodedString; - } - - static bool decode(const String& message, AgentMessage*& result) { - - result = nullptr; - - if (message.length() == 0) return false; - - int firstColon = message.indexOf(':'); - int secondColon = message.indexOf(':', firstColon + 1); - - if (firstColon == -1 || secondColon == -1) { - return false; - } - - String cmdStr = message.substring(0, firstColon); - String idStr = message.substring(firstColon + 1, secondColon); - String jsonStr = message.substring(secondColon + 1); - - char* endPtr = nullptr; - unsigned long parsedId = strtoul(idStr.c_str(), &endPtr, 10); - if (endPtr == idStr.c_str() || parsedId > 0xFFFFFFFFUL) { - return false; - } - - JsonDocument doc; // or appropriate size - DeserializationError error = deserializeJson(doc, jsonStr); - if (error || !doc.is()) { - return false; - } - - result = new AgentMessage(cmdStr, static_cast(parsedId), doc); - return true; - } - -}; - -#endif // MESSAGE_H diff --git a/src/config/config.sample.h b/src/config/config.sample.h deleted file mode 100644 index e821187..0000000 --- a/src/config/config.sample.h +++ /dev/null @@ -1,23 +0,0 @@ -#ifndef SECRETS_H -#define SECRETS_H - - -#define C_WIFI_SSID "WIFI_SSID" -#define C_WIFI_PASSWORD "WIFI_PASSWORD" - -#define C_WIFI_LOCAL_IP IPAddress(192, 168, 1, 32) -#define C_WIFI_GATEWAY_IP IPAddress(192, 168, 1, 1) -#define C_WIFI_SUBNET_MASK IPAddress(255, 255, 255, 0) - -#define C_WIFI_PRIMARY_DNS IPAddress(1, 1, 1, 1) -#define C_WIFI_SECONDARY_DNS IPAddress(1, 0, 0, 1) - -#define C_CONTROL_SERVER_HOST "example.com" -#define C_CONTROL_SERVER_PORT (uint16_t) 80 -#define C_CONTROL_SERVER_USE_SSL (bool) false - -#define C_CONTROL_SERVER_AUTH_ID "AUTH_ID" -#define C_CONTROL_SERVER_AUTH_SECRET "AUTH_SECRET" - - -#endif \ No newline at end of file diff --git a/src/core/CaBundle.cpp b/src/core/CaBundle.cpp new file mode 100644 index 0000000..1b9cc77 --- /dev/null +++ b/src/core/CaBundle.cpp @@ -0,0 +1,17 @@ +#include "core/CaBundle.h" + +// Symbols created by board_build.embed_files (binary, not NUL-terminated). +extern const uint8_t ca_bundle_start[] asm("_binary_data_cert_x509_crt_bundle_bin_start"); +extern const uint8_t ca_bundle_end[] asm("_binary_data_cert_x509_crt_bundle_bin_end"); + +namespace caBundle { + +const uint8_t* data() { + return ca_bundle_start; +} + +size_t size() { + return static_cast(ca_bundle_end - ca_bundle_start); +} + +} // namespace caBundle diff --git a/src/core/CaBundle.h b/src/core/CaBundle.h new file mode 100644 index 0000000..69d669b --- /dev/null +++ b/src/core/CaBundle.h @@ -0,0 +1,15 @@ +#pragma once + +#include +#include + +/** + * The trusted root certificates (data/cert/x509_crt_bundle.bin, generated at build time from + * Mozilla's list plus data/cert/custom/), embedded into the firmware. + */ +namespace caBundle { + +const uint8_t* data(); +size_t size(); + +} // namespace caBundle diff --git a/src/core/DeviceInfo.cpp b/src/core/DeviceInfo.cpp new file mode 100644 index 0000000..cbd8504 --- /dev/null +++ b/src/core/DeviceInfo.cpp @@ -0,0 +1,73 @@ +#include "core/DeviceInfo.h" + +#include +#include +#include +#include + +#include "Config.h" + +namespace deviceInfo { + +const char* resetReason() { + switch (esp_reset_reason()) { + case ESP_RST_POWERON: + return "power-on"; + case ESP_RST_SW: + return "restart"; + case ESP_RST_PANIC: + return "crash"; + case ESP_RST_INT_WDT: + case ESP_RST_TASK_WDT: + case ESP_RST_WDT: + return "watchdog"; + case ESP_RST_BROWNOUT: + return "brownout"; + case ESP_RST_DEEPSLEEP: + return "deep sleep"; + case ESP_RST_EXT: + return "reset pin"; + default: + return "unknown"; + } +} + +String chip() { + return String(ESP.getChipModel()) + " rev " + (ESP.getChipRevision() / 100) + "." + (ESP.getChipRevision() % 100); +} + +String mac() { + uint8_t bytes[6] = {}; + esp_read_mac(bytes, ESP_MAC_WIFI_STA); + char text[18]; + snprintf(text, sizeof(text), "%02X:%02X:%02X:%02X:%02X:%02X", bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], + bytes[5]); + return text; +} + +const char* runningPartition() { + const esp_partition_t* partition = esp_ota_get_running_partition(); + return partition ? partition->label : "?"; +} + +void fill(JsonObject info, const Settings& settings, const WifiLink& wifi) { + info["name"] = settings.deviceName(); + info["mac"] = mac(); + info["ipv4"] = wifi.ipv4(); + const String linkLocal = wifi.linkLocalIpv6(); + if (linkLocal != "::") info["ipv6_link_local"] = linkLocal; + const String global = wifi.globalIpv6(); + if (global != "::") info["ipv6_global"] = global; + info["wifi"] = wifi.ssid(); + info["rssi_dbm"] = wifi.rssi(); + info["ip_mode"] = settings.staticIp().enabled ? "static" : "dhcp"; + info["chip"] = chip(); + info["partition"] = runningPartition(); + info["reset_reason"] = resetReason(); + info["uptime_s"] = static_cast(millis() / 1000); + info["free_heap"] = ESP.getFreeHeap(); + info["min_free_heap"] = ESP.getMinFreeHeap(); + info["esp_idf"] = esp_get_idf_version(); +} + +} // namespace deviceInfo diff --git a/src/core/DeviceInfo.h b/src/core/DeviceInfo.h new file mode 100644 index 0000000..7b7e1af --- /dev/null +++ b/src/core/DeviceInfo.h @@ -0,0 +1,22 @@ +#pragma once + +#include +#include + +#include "core/Settings.h" +#include "net/WifiLink.h" + +namespace deviceInfo { + +/** Diagnostics for the HELLO message (shown on the agent's page in the dashboard). */ +void fill(JsonObject info, const Settings& settings, const WifiLink& wifi); + +const char* resetReason(); +/** `ESP32-D0WD-V3 rev 3.1` */ +String chip(); +/** `AA:BB:CC:DD:EE:FF` of the Wi-Fi station interface. */ +String mac(); +/** Label of the running app partition (`app0`/`app1`). */ +const char* runningPartition(); + +} // namespace deviceInfo diff --git a/src/core/Log.cpp b/src/core/Log.cpp new file mode 100644 index 0000000..f43d163 --- /dev/null +++ b/src/core/Log.cpp @@ -0,0 +1,149 @@ +#include "core/Log.h" + +#include + +#include + +LogLevel Log::level_ = LogLevel::Info; +SemaphoreHandle_t Log::mutex_ = nullptr; + +namespace { + +netignite::LogRing ring(Log::RING_BYTES); +vprintf_like_t idfOriginalVprintf = nullptr; + +/** Recursive: ESP-IDF may log from inside a serial write. */ +struct Lock { + explicit Lock(SemaphoreHandle_t mutex) : mutex_(mutex) { + if (mutex_) xSemaphoreTakeRecursive(mutex_, portMAX_DELAY); + } + ~Lock() { + if (mutex_) xSemaphoreGiveRecursive(mutex_); + } + SemaphoreHandle_t mutex_; +}; + +/** Removes ANSI color codes and trailing line breaks (ESP-IDF's log format). */ +std::string cleanLine(const char* text) { + std::string line; + for (const char* p = text; *p; p++) { + if (*p == '\x1b') { + while (*p && *p != 'm') p++; + if (!*p) break; + continue; + } + line += *p; + } + while (!line.empty() && (line.back() == '\n' || line.back() == '\r')) line.pop_back(); + return line; +} + +} // namespace + +void Log::begin(uint32_t baud) { + mutex_ = xSemaphoreCreateRecursiveMutex(); +#ifdef NETIGNITE_EMULATOR + // The emulator test streams firmware images over the serial port. + Serial.setRxBufferSize(8192); +#endif + Serial.begin(baud); + // Start on a fresh line after the ROM bootloader's output. + Serial.print("\n"); + // Keep ESP-IDF's messages (Wi-Fi driver, TLS errors) for the dashboard too. + idfOriginalVprintf = esp_log_set_vprintf(&Log::idfVprintf); +} + +const char* Log::levelName(LogLevel level) { + switch (level) { + case LogLevel::Error: + return "error"; + case LogLevel::Warn: + return "warn"; + case LogLevel::Info: + return "info"; + case LogLevel::Debug: + return "debug"; + } + return "info"; +} + +bool Log::parseLevel(const String& text, LogLevel& out) { + for (LogLevel level : {LogLevel::Error, LogLevel::Warn, LogLevel::Info, LogLevel::Debug}) { + if (text.equalsIgnoreCase(levelName(level))) { + out = level; + return true; + } + } + return false; +} + +void Log::write(LogLevel level, const char* prefix, const char* format, va_list args) { + if (level > level_) return; + + char message[256]; + vsnprintf(message, sizeof(message), format, args); + + char line[288]; + snprintf(line, sizeof(line), "[%8lu] %s %s", static_cast(millis() / 1000), prefix, message); + + Lock lock(mutex_); + ring.push(line); + Serial.print(line); + Serial.print("\n"); +} + +int Log::idfVprintf(const char* format, va_list args) { + char message[256]; + va_list copy; + va_copy(copy, args); + vsnprintf(message, sizeof(message), format, copy); + va_end(copy); + + { + Lock lock(mutex_); + const std::string line = cleanLine(message); + if (!line.empty()) ring.push(line); + } + return idfOriginalVprintf ? idfOriginalVprintf(format, args) : vprintf(format, args); +} + +#define NETIGNITE_LOG_IMPL(name, level, prefix) \ + void Log::name(const char* format, ...) { \ + va_list args; \ + va_start(args, format); \ + write(level, prefix, format, args); \ + va_end(args); \ + } + +NETIGNITE_LOG_IMPL(error, LogLevel::Error, "E") +NETIGNITE_LOG_IMPL(warn, LogLevel::Warn, "W") +NETIGNITE_LOG_IMPL(info, LogLevel::Info, "I") +NETIGNITE_LOG_IMPL(debug, LogLevel::Debug, "D") + +void Log::print(const char* format, ...) { + char message[512]; + va_list args; + va_start(args, format); + vsnprintf(message, sizeof(message), format, args); + va_end(args); + + Lock lock(mutex_); + Serial.print(message); + Serial.print("\n"); +} + +void Log::writeRaw(const uint8_t* data, size_t length) { + Lock lock(mutex_); + Serial.write(data, length); + Serial.flush(); +} + +uint32_t Log::nextSeq() { + Lock lock(mutex_); + return ring.nextSeq(); +} + +netignite::LogRing::ReadResult Log::read(uint32_t from, size_t maxBytes, std::vector& out) { + Lock lock(mutex_); + return ring.read(from, maxBytes, out); +} diff --git a/src/core/Log.h b/src/core/Log.h new file mode 100644 index 0000000..7143652 --- /dev/null +++ b/src/core/Log.h @@ -0,0 +1,60 @@ +#pragma once + +#include + +#include +#include +#include + +#include "netignite/LogRing.h" + +enum class LogLevel : uint8_t { + Error = 0, + Warn = 1, + Info = 2, + Debug = 3, +}; + +/** + * Logging to the serial port. Thread-safe, and every message ends with a newline: the web + * installer only recognizes Improv packets at the start of a line. + * + * The most recent lines (also ESP-IDF's own log output) are kept in RAM for the dashboard's + * console (`GET_LOGS`, `LOG` events). + */ +class Log { + public: + /** RAM for the lines kept for the dashboard. */ + static constexpr size_t RING_BYTES = 16 * 1024; + + static void begin(uint32_t baud); + + static void setLevel(LogLevel level) { level_ = level; } + static LogLevel level() { return level_; } + static const char* levelName(LogLevel level); + /** Parses `error`, `warn`, `info` or `debug`. */ + static bool parseLevel(const String& text, LogLevel& out); + + static void error(const char* format, ...) __attribute__((format(printf, 1, 2))); + static void warn(const char* format, ...) __attribute__((format(printf, 1, 2))); + static void info(const char* format, ...) __attribute__((format(printf, 1, 2))); + static void debug(const char* format, ...) __attribute__((format(printf, 1, 2))); + + /** Unprefixed console output (command answers), regardless of the log level. */ + static void print(const char* format, ...) __attribute__((format(printf, 1, 2))); + + /** Writes raw bytes (Improv packets) without interleaving with log lines. */ + static void writeRaw(const uint8_t* data, size_t length); + + /** Sequence number of the next stored line. */ + static uint32_t nextSeq(); + /** Stored lines from sequence `from` on, at most `maxBytes` of text. */ + static netignite::LogRing::ReadResult read(uint32_t from, size_t maxBytes, std::vector& out); + + private: + static LogLevel level_; + static SemaphoreHandle_t mutex_; + + static void write(LogLevel level, const char* prefix, const char* format, va_list args); + static int idfVprintf(const char* format, va_list args); +}; diff --git a/src/core/Settings.cpp b/src/core/Settings.cpp new file mode 100644 index 0000000..b2bc72f --- /dev/null +++ b/src/core/Settings.cpp @@ -0,0 +1,169 @@ +#include "core/Settings.h" + +#include +#include + +#include "Config.h" + +namespace k = config::keys; + +namespace { + +/** Opens the namespace for one operation; closes it again in the destructor. */ +class Store { + public: + explicit Store(bool readOnly) { ok_ = prefs_.begin(k::NAMESPACE, readOnly); } + ~Store() { + if (ok_) prefs_.end(); + } + Preferences& operator*() { return prefs_; } + Preferences* operator->() { return &prefs_; } + bool ok() const { return ok_; } + + private: + Preferences prefs_; + bool ok_ = false; +}; + +String readString(Preferences& prefs, const char* key) { + return prefs.isKey(key) ? prefs.getString(key, "") : String(); +} + +void writeString(Preferences& prefs, const char* key, const String& value) { + if (value.length() == 0) { + if (prefs.isKey(key)) prefs.remove(key); + } else { + prefs.putString(key, value); + } +} + +} // namespace + +String Settings::defaultDeviceName() { + uint8_t mac[6] = {}; + esp_read_mac(mac, ESP_MAC_WIFI_STA); + char name[32]; + snprintf(name, sizeof(name), "NetIgnite-%02X%02X%02X", mac[3], mac[4], mac[5]); + return name; +} + +void Settings::load() { + { + // Creates the namespace on first boot, so the read-only open below succeeds. + Store create(false); + } + Store store(true); + if (!store.ok()) { + Log::error("Settings: can't open NVS namespace '%s'", k::NAMESPACE); + deviceName_ = defaultDeviceName(); + return; + } + + serverUrl_ = readString(*store, k::SERVER_URL); + enrollToken_ = readString(*store, k::ENROLL_TOKEN); + agentId_ = store->getUInt(k::AGENT_ID, 0); + tokenId_ = readString(*store, k::TOKEN_ID); + secret_ = readString(*store, k::SECRET); + wifiSsid_ = readString(*store, k::WIFI_SSID); + wifiPassword_ = readString(*store, k::WIFI_PASSWORD); + deviceName_ = readString(*store, k::DEVICE_NAME); + if (deviceName_.length() == 0) deviceName_ = defaultDeviceName(); + + staticIp_.enabled = store->getUChar(k::IP_MODE, 0) == 1; + staticIp_.ip = readString(*store, k::STATIC_IP); + staticIp_.gateway = readString(*store, k::GATEWAY); + staticIp_.netmask = readString(*store, k::NETMASK); + staticIp_.dns1 = readString(*store, k::DNS1); + staticIp_.dns2 = readString(*store, k::DNS2); + + otaVersion_ = readString(*store, k::OTA_VERSION); + otaPartition_ = readString(*store, k::OTA_PARTITION); + + const uint8_t level = store->getUChar(k::LOG_LEVEL, static_cast(LogLevel::Info)); + logLevel_ = level <= static_cast(LogLevel::Debug) ? static_cast(level) : LogLevel::Info; +} + +void Settings::setServerUrl(const String& url) { + Store store(false); + writeString(*store, k::SERVER_URL, url); + serverUrl_ = url; +} + +void Settings::setEnrollToken(const String& token) { + Store store(false); + writeString(*store, k::ENROLL_TOKEN, token); + enrollToken_ = token; +} + +void Settings::setCredentials(uint32_t agentId, const String& tokenId, const String& secret) { + Store store(false); + store->putUInt(k::AGENT_ID, agentId); + writeString(*store, k::TOKEN_ID, tokenId); + writeString(*store, k::SECRET, secret); + writeString(*store, k::ENROLL_TOKEN, ""); + agentId_ = agentId; + tokenId_ = tokenId; + secret_ = secret; + enrollToken_ = ""; +} + +void Settings::clearCredentials() { + Store store(false); + if (store->isKey(k::AGENT_ID)) store->remove(k::AGENT_ID); + writeString(*store, k::TOKEN_ID, ""); + writeString(*store, k::SECRET, ""); + agentId_ = 0; + tokenId_ = ""; + secret_ = ""; +} + +void Settings::setWifi(const String& ssid, const String& password) { + Store store(false); + writeString(*store, k::WIFI_SSID, ssid); + writeString(*store, k::WIFI_PASSWORD, password); + wifiSsid_ = ssid; + wifiPassword_ = password; +} + +void Settings::setDeviceName(const String& name) { + Store store(false); + writeString(*store, k::DEVICE_NAME, name); + deviceName_ = name.length() > 0 ? name : defaultDeviceName(); +} + +void Settings::setStaticIp(const StaticIp& config) { + Store store(false); + store->putUChar(k::IP_MODE, config.enabled ? 1 : 0); + writeString(*store, k::STATIC_IP, config.enabled ? config.ip : String()); + writeString(*store, k::GATEWAY, config.enabled ? config.gateway : String()); + writeString(*store, k::NETMASK, config.enabled ? config.netmask : String()); + writeString(*store, k::DNS1, config.enabled ? config.dns1 : String()); + writeString(*store, k::DNS2, config.enabled ? config.dns2 : String()); + staticIp_ = config.enabled ? config : StaticIp{}; +} + +void Settings::setLogLevel(LogLevel level) { + Store store(false); + store->putUChar(k::LOG_LEVEL, static_cast(level)); + logLevel_ = level; +} + +void Settings::setOtaMarker(const String& version, const String& partition) { + Store store(false); + writeString(*store, k::OTA_VERSION, version); + writeString(*store, k::OTA_PARTITION, partition); + otaVersion_ = version; + otaPartition_ = partition; +} + +void Settings::clearOtaMarker() { + if (otaVersion_.length() == 0 && otaPartition_.length() == 0) return; + setOtaMarker("", ""); +} + +void Settings::factoryReset() { + Store store(false); + store->clear(); + *this = Settings(); + deviceName_ = defaultDeviceName(); +} diff --git a/src/core/Settings.h b/src/core/Settings.h new file mode 100644 index 0000000..bb33d17 --- /dev/null +++ b/src/core/Settings.h @@ -0,0 +1,66 @@ +#pragma once + +#include + +#include "core/Log.h" + +/** Persistent configuration in NVS (namespace `netignite`, see `config::keys`). */ +class Settings { + public: + struct StaticIp { + bool enabled = false; + String ip, gateway, netmask, dns1, dns2; + }; + + /** Loads all values; call once in setup(). */ + void load(); + + const String& serverUrl() const { return serverUrl_; } + const String& enrollToken() const { return enrollToken_; } + uint32_t agentId() const { return agentId_; } + const String& tokenId() const { return tokenId_; } + const String& secret() const { return secret_; } + const String& wifiSsid() const { return wifiSsid_; } + const String& wifiPassword() const { return wifiPassword_; } + const String& deviceName() const { return deviceName_; } + const StaticIp& staticIp() const { return staticIp_; } + LogLevel logLevel() const { return logLevel_; } + + bool hasWifi() const { return wifiSsid_.length() > 0; } + bool hasCredentials() const { return tokenId_.length() > 0 && secret_.length() > 0; } + + void setServerUrl(const String& url); + void setEnrollToken(const String& token); + /** Stores the credential from a successful enrollment and removes the enrollment token. */ + void setCredentials(uint32_t agentId, const String& tokenId, const String& secret); + void clearCredentials(); + void setWifi(const String& ssid, const String& password); + void setDeviceName(const String& name); + void setStaticIp(const StaticIp& config); + void setLogLevel(LogLevel level); + + /** + * Changes the server URL or IP settings for this boot only (CONFIG_SET trial); a restart + * returns to the saved values. Save them with setServerUrl() / setStaticIp(). + */ + void setServerUrlUnsaved(const String& url) { serverUrl_ = url; } + void setStaticIpUnsaved(const StaticIp& config) { staticIp_ = config.enabled ? config : StaticIp{}; } + + /** An installed update that hasn't confirmed itself yet (see ota/Rollback.h). */ + String otaVersion() const { return otaVersion_; } + String otaPartition() const { return otaPartition_; } + void setOtaMarker(const String& version, const String& partition); + void clearOtaMarker(); + + /** Erases everything (credentials, Wi-Fi, server). */ + void factoryReset(); + + private: + String serverUrl_, enrollToken_, tokenId_, secret_, wifiSsid_, wifiPassword_, deviceName_; + String otaVersion_, otaPartition_; + uint32_t agentId_ = 0; + StaticIp staticIp_; + LogLevel logLevel_ = LogLevel::Info; + + static String defaultDeviceName(); +}; diff --git a/src/main.cpp b/src/main.cpp index 56c0653..9d10ae8 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,58 +1,270 @@ +// NetIgnite ESP32 agent, firmware v2. +// +// Flow: Wi-Fi (from Improv or the console) → time sync (for HTTPS) → enrollment with a +// single-use token (from the web installer's NVS image or the console) → WebSocket to the +// server, which then sends WAKE / STATUS / PING requests, OTA updates and console commands. + #include -#include -#include -#include - -#include "config/config.h" -#include "utils/logger.h" -#include "utils/wlan.h" -#include "utils/utils.h" -#include "agent/agent.h" -#include "utils/icmp.h" - -WebSocketsClient webSocket; -WiFiUDP UDPClient; -WakeOnLan WakeOnLanClient(UDPClient); - -WLANConfig wlanConfig = { - C_WIFI_SSID, - C_WIFI_PASSWORD, - C_WIFI_LOCAL_IP, - C_WIFI_GATEWAY_IP, - C_WIFI_SUBNET_MASK, - C_WIFI_PRIMARY_DNS, - C_WIFI_SECONDARY_DNS -}; - -AgentConfig agentConfig = { - C_CONTROL_SERVER_HOST, - C_CONTROL_SERVER_PORT, - C_CONTROL_SERVER_USE_SSL, - C_CONTROL_SERVER_AUTH_ID, - C_CONTROL_SERVER_AUTH_SECRET -}; - - -void setup() -{ - Logger::init(115200); - Logger::setLevel(LogLevel::INFO); - - Logger::infoln("Starting..."); - - WLAN::setup(wlanConfig); - - delay(1000); - - // Don't block the boot forever: WiFi reconnects are only handled in loop(). - Utils::setClock(30000); - - Agent::setup(agentConfig); -} - - -void loop() -{ - WLAN::loop(); - Agent::loop(); -} \ No newline at end of file + +#include + +#include "Config.h" +#include "agent/AgentClient.h" +#include "agent/Enrollment.h" +#include "core/DeviceInfo.h" +#include "core/Log.h" +#include "core/Settings.h" +#include "net/TimeSync.h" +#include "net/WifiLink.h" +#include "netignite/ServerUrl.h" +#include "ota/Rollback.h" +#include "serial/SerialConsole.h" + +namespace { + +Settings settings; +WifiLink wifi(settings); +TimeSync timeSync; +AgentClient agent(settings, wifi); + +/** What the main loop is waiting for; logged once per change. */ +enum class Phase { NoWifi, NoServer, WaitingForNetwork, WaitingForTime, Enrolling, NotEnrolled, Running }; +Phase phase = Phase::NoWifi; +bool phaseLogged = false; + +std::optional server; +uint32_t nextEnrollAttempt = 0; + +/** A restart requested by the server or an installed update, after the answer went out. */ +struct PendingRestart { + bool active = false; + bool factoryReset = false; + uint32_t at = 0; +} pendingRestart; + +/** + * A CONFIG_SET that changed the server URL or the IP settings: applied for this boot only, and + * saved once the agent reaches the server with it. Otherwise the agent restarts with the saved + * settings when `deadline` passes. + */ +struct ConfigTrial { + bool active = false; + bool applyPending = false; + bool serverChanged = false; + bool ipChanged = false; + uint32_t applyAt = 0; + uint32_t deadline = 0; +} trial; + +void reloadServer() { + server = netignite::parseServerUrl(settings.serverUrl().c_str()); + if (settings.serverUrl().length() > 0 && !server) { + Log::error("The saved server URL \"%s\" is invalid", settings.serverUrl().c_str()); + } +} + +void onConfigChanged() { + agent.stop(); + reloadServer(); + nextEnrollAttempt = 0; + phaseLogged = false; +} + +void scheduleRestart(bool factoryReset, uint32_t delayMs) { + pendingRestart = {true, factoryReset, millis() + delayMs}; +} + +bool applyConfig(const netignite::protocol::ConfigRequest& request, std::string& error) { + if ((request.serverUrl || request.ip) && trial.active) { + error = "A previous server or network change is still being tried"; + return false; + } + + if (request.name) { + settings.setDeviceName(request.name->c_str()); + Log::info("Name changed to %s (host name after a restart)", request.name->c_str()); + } + if (request.logLevel) { + LogLevel level; + Log::parseLevel(request.logLevel->c_str(), level); + settings.setLogLevel(level); + Log::setLevel(level); + Log::info("Log level: %s", Log::levelName(level)); + } + + const bool serverChanged = request.serverUrl && settings.serverUrl() != request.serverUrl->c_str(); + if (serverChanged) settings.setServerUrlUnsaved(request.serverUrl->c_str()); + + if (request.ip) { + Settings::StaticIp ip; + ip.enabled = request.ip->isStatic; + ip.ip = request.ip->address.c_str(); + ip.gateway = request.ip->gateway.c_str(); + ip.netmask = request.ip->netmask.c_str(); + ip.dns1 = request.ip->dns1.c_str(); + ip.dns2 = request.ip->dns2.c_str(); + settings.setStaticIpUnsaved(ip); + } + + if (serverChanged || request.ip) { + // Applied a moment later, so the answer still goes out over the current connection. + trial = {true, true, serverChanged, request.ip.has_value(), millis() + 500, + millis() + config::CONFIG_TRIAL_MS}; + Log::info("Trying the new %s; it is kept once the agent reaches the server (within %lu s)", + serverChanged && request.ip ? "server and network settings" + : serverChanged ? "server address" + : "network settings", + static_cast(config::CONFIG_TRIAL_MS / 1000)); + } + return true; +} + +/** The agent reached the server: the running firmware and any tried settings work. */ +void onServerConfirmed() { + rollback::confirm(settings); + + if (trial.active && !trial.applyPending) { + if (trial.serverChanged) settings.setServerUrl(settings.serverUrl()); + if (trial.ipChanged) settings.setStaticIp(settings.staticIp()); + trial = {}; + Log::info("New settings work and are saved"); + } +} + +void loopTrialAndRestart() { + if (trial.active && trial.applyPending && static_cast(millis() - trial.applyAt) >= 0) { + trial.applyPending = false; + if (trial.ipChanged) wifi.reconnect(); + onConfigChanged(); + } + if (trial.active && !trial.applyPending && static_cast(millis() - trial.deadline) >= 0) { + Log::warn("The agent didn't reach the server with the new settings; restarting with the previous ones"); + scheduleRestart(false, 200); + trial = {}; + } + + if (pendingRestart.active && static_cast(millis() - pendingRestart.at) >= 0) { + if (pendingRestart.factoryReset) { + settings.factoryReset(); + Log::info("All settings erased"); + } + Log::info("Restarting"); + delay(100); + ESP.restart(); + } +} + +SerialConsole console(settings, wifi, agent, onConfigChanged); + +void setPhase(Phase next) { + if (next == phase && phaseLogged) return; + phase = next; + phaseLogged = true; + + switch (phase) { + case Phase::NoWifi: + Log::info("Waiting for Wi-Fi settings (web installer or \"wifi\" command)"); + break; + case Phase::NoServer: + Log::info("Not connected to a server yet. Type: setup "); + break; + case Phase::WaitingForNetwork: + Log::info("Waiting for the network"); + break; + case Phase::WaitingForTime: + Log::info("Waiting for the time (needed to check the server's certificate)"); + break; + case Phase::Enrolling: + break; + case Phase::NotEnrolled: + Log::warn("This agent has no credential. Create an enrollment token in the dashboard, then type: enroll "); + break; + case Phase::Running: + break; + } +} + +void runEnrollment() { + if (millis() < nextEnrollAttempt) return; + + const enrollment::Result result = enrollment::enroll(*server, settings.enrollToken()); + switch (result.status) { + case enrollment::Result::Status::Ok: + settings.setCredentials(result.agentId, result.tokenId, result.secret); + Log::info("Enrolled as agent %lu", static_cast(result.agentId)); + break; + case enrollment::Result::Status::Rejected: + // Retrying can't help: the token is used, expired or for another agent kind. + settings.setEnrollToken(""); + Log::error("Enrollment rejected: %s. Create a new token in the dashboard.", result.message.c_str()); + phaseLogged = false; + break; + case enrollment::Result::Status::Failed: + nextEnrollAttempt = millis() + config::ENROLL_RETRY_MS; + Log::warn("Enrollment failed: %s. Retrying in %lu s.", result.message.c_str(), + static_cast(config::ENROLL_RETRY_MS / 1000)); + break; + } +} + +void runAgent() { + if (!settings.hasWifi()) return setPhase(Phase::NoWifi); + if (!server) return setPhase(Phase::NoServer); + + const bool online = wifi.connected() && wifi.hasIp(); + if (!online) { + // The WebSocket client keeps retrying on its own once it has been started. + if (!agent.running()) setPhase(Phase::WaitingForNetwork); + return; + } + timeSync.start(); + if (server->secure && !timeSync.synced()) return setPhase(Phase::WaitingForTime); + + if (settings.enrollToken().length() > 0) { + agent.stop(); + setPhase(Phase::Enrolling); + runEnrollment(); + return; + } + if (!settings.hasCredentials()) return setPhase(Phase::NotEnrolled); + + setPhase(Phase::Running); + if (!agent.running()) agent.start(*server, settings.tokenId(), settings.secret()); +} + +} // namespace + +void setup() { + Log::begin(config::SERIAL_BAUD); + settings.load(); + Log::setLevel(settings.logLevel()); + + Log::info("%s %s on %s", config::FIRMWARE_NAME, config::FIRMWARE_VERSION, deviceInfo::chip().c_str()); + Log::info("Device %s (%s), partition %s, reset reason: %s", settings.deviceName().c_str(), deviceInfo::mac().c_str(), + deviceInfo::runningPartition(), deviceInfo::resetReason()); + rollback::begin(settings); + + reloadServer(); + wifi.begin(); + agent.begin({ + .applyConfig = applyConfig, + .restart = [](bool factoryReset) { scheduleRestart(factoryReset, 500); }, + .serverConfirmed = onServerConfirmed, + .updateInstalled = + [](const std::string& version) { + rollback::recordInstalled(settings, version.c_str()); + Log::info("Restarting into firmware %s", version.c_str()); + scheduleRestart(false, 1000); + }, + }); +} + +void loop() { + console.loop(); + wifi.loop(); + runAgent(); + agent.loop(); + rollback::loop(); + loopTrialAndRestart(); + delay(2); +} diff --git a/src/net/Probes.cpp b/src/net/Probes.cpp new file mode 100644 index 0000000..c9f0326 --- /dev/null +++ b/src/net/Probes.cpp @@ -0,0 +1,255 @@ +#include "net/Probes.h" + +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include "Config.h" +#include "core/Log.h" +#include "netignite/Mac.h" + +using netignite::protocol::StatusRequest; + +namespace probes { + +namespace { + +int staNetifIndex() { + esp_netif_t* netif = esp_netif_get_handle_from_ifkey("WIFI_STA_DEF"); + return netif ? esp_netif_get_netif_impl_index(netif) : 0; +} + +bool isLinkLocal(const ip_addr_t& address) { + return IP_IS_V6_VAL(address) && ip6_addr_islinklocal(ip_2_ip6(&address)); +} + +/** Link-local IPv6 addresses only make sense on the Wi-Fi interface. */ +void scopeToWifi(ip_addr_t& address) { + if (isLinkLocal(address)) ip6_addr_set_zone(ip_2_ip6(&address), static_cast(staNetifIndex())); +} + +String formatAddress(const ip_addr_t& address) { + char text[IPADDR_STRLEN_MAX]; + ipaddr_ntoa_r(&address, text, sizeof(text)); + return text; +} + +bool resolve(const StatusRequest& request, std::vector& out, String& error) { + if (request.target == StatusRequest::Target::LinkLocal) { + const netignite::IPv6Bytes bytes = netignite::linkLocalFromMac(request.mac); + ip_addr_t address = {}; + IP_SET_TYPE_VAL(address, IPADDR_TYPE_V6); + std::memcpy(ip_2_ip6(&address)->addr, bytes.data(), bytes.size()); + scopeToWifi(address); + out.push_back(address); + return true; + } + + if (request.target == StatusRequest::Target::Ip) { + ip_addr_t address = {}; + if (!ipaddr_aton(request.address.c_str(), &address)) { + error = "Invalid IP address"; + return false; + } + scopeToWifi(address); + out.push_back(address); + return true; + } + + addrinfo hints = {}; + hints.ai_family = AF_UNSPEC; + hints.ai_socktype = SOCK_STREAM; + addrinfo* results = nullptr; + if (getaddrinfo(request.address.c_str(), nullptr, &hints, &results) != 0 || !results) { + error = "Host name doesn't resolve"; + return false; + } + + for (addrinfo* entry = results; entry && out.size() < 2; entry = entry->ai_next) { + ip_addr_t address = {}; + if (entry->ai_family == AF_INET) { + const auto* sin = reinterpret_cast(entry->ai_addr); + IP_SET_TYPE_VAL(address, IPADDR_TYPE_V4); + inet_addr_to_ip4addr(ip_2_ip4(&address), &sin->sin_addr); + } else if (entry->ai_family == AF_INET6) { + const auto* sin6 = reinterpret_cast(entry->ai_addr); + IP_SET_TYPE_VAL(address, IPADDR_TYPE_V6); + inet6_addr_to_ip6addr(ip_2_ip6(&address), &sin6->sin6_addr); + scopeToWifi(address); + } else { + continue; + } + out.push_back(address); + } + freeaddrinfo(results); + + if (out.empty()) { + error = "Host name doesn't resolve"; + return false; + } + return true; +} + +// --- ICMP --------------------------------------------------------------------------------------- + +/** + * Shared between the caller and the ping session's callbacks. The ping task may finish after the + * caller gave up waiting, so whoever finishes last frees it. + */ +struct PingContext { + SemaphoreHandle_t done = xSemaphoreCreateBinary(); + std::atomic replied{false}; + /** 0 = running, 1 = ended, 2 = abandoned by the caller */ + std::atomic state{0}; + esp_ping_handle_t session = nullptr; + + ~PingContext() { vSemaphoreDelete(done); } +}; + +void onPingSuccess(esp_ping_handle_t, void* arg) { + static_cast(arg)->replied = true; +} + +void onPingEnd(esp_ping_handle_t, void* arg) { + auto* context = static_cast(arg); + if (context->state.exchange(1) == 2) { + // The caller stopped waiting: clean up here. + esp_ping_delete_session(context->session); + delete context; + return; + } + xSemaphoreGive(context->done); +} + +bool ping(const ip_addr_t& address, uint32_t timeoutMs) { + auto* context = new PingContext(); + + esp_ping_config_t config = ESP_PING_DEFAULT_CONFIG(); + config.target_addr = address; + config.count = 1; + config.timeout_ms = timeoutMs; + if (isLinkLocal(address)) config.interface = static_cast(staNetifIndex()); + + esp_ping_callbacks_t callbacks = {}; + callbacks.cb_args = context; + callbacks.on_ping_success = onPingSuccess; + callbacks.on_ping_end = onPingEnd; + + if (esp_ping_new_session(&config, &callbacks, &context->session) != ESP_OK) { + delete context; + return false; + } + if (esp_ping_start(context->session) != ESP_OK) { + esp_ping_delete_session(context->session); + delete context; + return false; + } + + const bool ended = xSemaphoreTake(context->done, pdMS_TO_TICKS(timeoutMs + 1500)) == pdTRUE; + const bool replied = context->replied; + + if (ended || context->state.exchange(2) == 1) { + // Ended normally (or just now): the session is ours to delete. + if (!ended) xSemaphoreTake(context->done, portMAX_DELAY); + esp_ping_delete_session(context->session); + delete context; + } + return replied; +} + +// --- TCP ---------------------------------------------------------------------------------------- + +bool tcpConnect(const ip_addr_t& address, uint16_t port, uint32_t timeoutMs) { + sockaddr_storage storage = {}; + socklen_t length = 0; + int family = AF_INET; + + if (IP_IS_V6_VAL(address)) { + family = AF_INET6; + auto* sin6 = reinterpret_cast(&storage); + sin6->sin6_family = AF_INET6; + sin6->sin6_port = htons(port); + inet6_addr_from_ip6addr(&sin6->sin6_addr, ip_2_ip6(&address)); + sin6->sin6_scope_id = ip6_addr_zone(ip_2_ip6(&address)); + length = sizeof(sockaddr_in6); + } else { + auto* sin = reinterpret_cast(&storage); + sin->sin_family = AF_INET; + sin->sin_port = htons(port); + inet_addr_from_ip4addr(&sin->sin_addr, ip_2_ip4(&address)); + length = sizeof(sockaddr_in); + } + + const int sock = socket(family, SOCK_STREAM, IPPROTO_TCP); + if (sock < 0) return false; + fcntl(sock, F_SETFL, fcntl(sock, F_GETFL, 0) | O_NONBLOCK); + + bool up = false; + if (connect(sock, reinterpret_cast(&storage), length) == 0) { + up = true; + } else if (errno == EINPROGRESS) { + fd_set writable; + FD_ZERO(&writable); + FD_SET(sock, &writable); + timeval timeout = {static_cast(timeoutMs / 1000), static_cast((timeoutMs % 1000) * 1000)}; + if (select(sock + 1, nullptr, &writable, nullptr, &timeout) > 0) { + int error = 0; + socklen_t errorLength = sizeof(error); + getsockopt(sock, SOL_SOCKET, SO_ERROR, &error, &errorLength); + // A refused (reset) connection still proves the host is up. + up = error == 0 || error == ECONNREFUSED || error == ECONNRESET; + } + } else { + up = errno == ECONNREFUSED || errno == ECONNRESET; + } + + close(sock); + return up; +} + +} // namespace + +Result check(const StatusRequest& request) { + Result result; + std::vector addresses; + if (!resolve(request, addresses, result.error)) return result; + + const uint32_t start = millis(); + auto remaining = [&]() -> int32_t { + return static_cast(config::STATUS_BUDGET_MS) - static_cast(millis() - start); + }; + + for (const ip_addr_t& address : addresses) { + if (request.icmp) { + // Two echoes: the first one often only triggers ARP/NDP resolution. + for (int attempt = 0; attempt < 2 && remaining() > static_cast(config::PING_TIMEOUT_MS); attempt++) { + if (ping(address, config::PING_TIMEOUT_MS)) { + Log::debug("Status: %s answers ping", formatAddress(address).c_str()); + result.online = true; + return result; + } + } + } + if (request.tcpPort != 0 && remaining() > 200) { + const uint32_t timeout = min(config::TCP_TIMEOUT_MS, static_cast(remaining())); + if (tcpConnect(address, request.tcpPort, timeout)) { + Log::debug("Status: %s answers on TCP port %u", formatAddress(address).c_str(), request.tcpPort); + result.online = true; + return result; + } + } + if (remaining() <= 200) break; + } + + Log::debug("Status: %s doesn't answer", formatAddress(addresses.front()).c_str()); + return result; +} + +} // namespace probes diff --git a/src/net/Probes.h b/src/net/Probes.h new file mode 100644 index 0000000..dd8bfec --- /dev/null +++ b/src/net/Probes.h @@ -0,0 +1,26 @@ +#pragma once + +#include + +#include "netignite/Protocol.h" + +/** + * Status checks for STATUS requests. Blocking; run them from the status worker task, not the + * main loop. A device counts as online if any requested probe answers: + * + * - ICMP echo (IPv4 or IPv6; link-local addresses are sent on the Wi-Fi interface) + * - TCP connect to a port: an accepted *or refused* connection both mean the host is up + * + * Host names are resolved to IPv4 and IPv6 addresses (A/AAAA). + */ +namespace probes { + +struct Result { + bool online = false; + /** Set when the check couldn't run (bad address, name doesn't resolve, no network). */ + String error; +}; + +Result check(const netignite::protocol::StatusRequest& request); + +} // namespace probes diff --git a/src/net/TimeSync.cpp b/src/net/TimeSync.cpp new file mode 100644 index 0000000..7950173 --- /dev/null +++ b/src/net/TimeSync.cpp @@ -0,0 +1,17 @@ +#include "net/TimeSync.h" + +#include + +#include "core/Log.h" + +void TimeSync::start() { + if (started_) return; + started_ = true; + configTime(0, 0, "pool.ntp.org", "time.cloudflare.com", "time.google.com"); + Log::info("Time sync started"); +} + +bool TimeSync::synced() const { + // Any time after 2024-01-01 means SNTP has answered. + return time(nullptr) > 1704067200; +} diff --git a/src/net/TimeSync.h b/src/net/TimeSync.h new file mode 100644 index 0000000..22302c7 --- /dev/null +++ b/src/net/TimeSync.h @@ -0,0 +1,14 @@ +#pragma once + +#include + +/** SNTP. TLS certificate checks need the real time, so HTTPS waits until it is synced. */ +class TimeSync { + public: + /** Starts SNTP once the network is up. */ + void start(); + bool synced() const; + + private: + bool started_ = false; +}; diff --git a/src/net/WakeSender.cpp b/src/net/WakeSender.cpp new file mode 100644 index 0000000..a1fd368 --- /dev/null +++ b/src/net/WakeSender.cpp @@ -0,0 +1,66 @@ +#include "net/WakeSender.h" + +#include +#include + +#include "Config.h" +#include "core/Log.h" +#include "netignite/MagicPacket.h" + +namespace wake { + +namespace { + +uint32_t toHostOrder(const IPAddress& address) { + return (static_cast(address[0]) << 24) | (static_cast(address[1]) << 16) | + (static_cast(address[2]) << 8) | static_cast(address[3]); +} + +} // namespace + +bool send(const netignite::MacAddress& mac, uint16_t port, String& error) { + const IPAddress local = WiFi.localIP(); + if (WiFi.status() != WL_CONNECTED || local == IPAddress(0, 0, 0, 0)) { + error = "The agent has no network connection"; + return false; + } + + const int sock = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); + if (sock < 0) { + error = "Couldn't open a UDP socket"; + return false; + } + const int enable = 1; + setsockopt(sock, SOL_SOCKET, SO_BROADCAST, &enable, sizeof(enable)); + + const auto packet = netignite::buildMagicPacket(mac); + const uint32_t targets[] = { + netignite::directedBroadcast(toHostOrder(local), toHostOrder(WiFi.subnetMask())), + 0xffffffff, + }; + + int sent = 0; + for (int round = 0; round < config::WOL_ROUNDS; round++) { + if (round > 0) delay(config::WOL_ROUND_DELAY_MS); + for (uint32_t target : targets) { + sockaddr_in destination = {}; + destination.sin_family = AF_INET; + destination.sin_port = htons(port); + destination.sin_addr.s_addr = htonl(target); + if (sendto(sock, packet.data(), packet.size(), 0, reinterpret_cast(&destination), + sizeof(destination)) == static_cast(packet.size())) { + sent++; + } + } + } + close(sock); + + if (sent == 0) { + error = "Sending the magic packet failed"; + return false; + } + Log::info("Wake-on-LAN sent to %s (port %u, %d packets)", netignite::formatMac(mac).c_str(), port, sent); + return true; +} + +} // namespace wake diff --git a/src/net/WakeSender.h b/src/net/WakeSender.h new file mode 100644 index 0000000..79542d9 --- /dev/null +++ b/src/net/WakeSender.h @@ -0,0 +1,15 @@ +#pragma once + +#include + +#include "netignite/Mac.h" + +namespace wake { + +/** + * Sends Wake-on-LAN magic packets to the subnet's broadcast address and to 255.255.255.255, + * `config::WOL_ROUNDS` times. Returns false (with `error`) if no packet could be sent. + */ +bool send(const netignite::MacAddress& mac, uint16_t port, String& error); + +} // namespace wake diff --git a/src/net/WifiLink.cpp b/src/net/WifiLink.cpp new file mode 100644 index 0000000..ae90170 --- /dev/null +++ b/src/net/WifiLink.cpp @@ -0,0 +1,174 @@ +#include "net/WifiLink.h" + +#include + +#include "Config.h" +#include "core/Log.h" + +namespace { + +bool parseIp(const String& text, IPAddress& out) { + return text.length() > 0 && out.fromString(text); +} + +} // namespace + +// The emulator build (env:esp32dev-qemu) has no Wi-Fi radio: touching the driver would abort. +#ifdef NETIGNITE_EMULATOR +constexpr bool WIFI_AVAILABLE = false; +#else +constexpr bool WIFI_AVAILABLE = true; +#endif + +void WifiLink::begin() { + if (!WIFI_AVAILABLE) { + Log::warn("Emulator build: Wi-Fi is disabled"); + return; + } + WiFi.persistent(false); // credentials live in our own NVS namespace + WiFi.setHostname(settings_.deviceName().c_str()); + WiFi.mode(WIFI_STA); + WiFi.setSleep(false); // lower latency for status checks and wake commands + WiFi.enableIPv6(true); + WiFi.setAutoReconnect(true); + + if (!settings_.hasWifi()) { + Log::warn("Wi-Fi isn't set up yet. Use the web installer, or type: wifi \"\" \"\""); + return; + } + applyIpConfig(); + connect(settings_.wifiSsid(), settings_.wifiPassword()); +} + +void WifiLink::applyIpConfig() { + const Settings::StaticIp& config = settings_.staticIp(); + if (!config.enabled) { + WiFi.config(INADDR_NONE, INADDR_NONE, INADDR_NONE); // DHCP + return; + } + + IPAddress ip, gateway, netmask, dns1, dns2; + if (!parseIp(config.ip, ip) || !parseIp(config.gateway, gateway) || !parseIp(config.netmask, netmask)) { + Log::error("Static IP settings are invalid, using DHCP"); + WiFi.config(INADDR_NONE, INADDR_NONE, INADDR_NONE); + return; + } + if (!parseIp(config.dns1, dns1)) dns1 = gateway; + if (!parseIp(config.dns2, dns2)) dns2 = IPAddress(0, 0, 0, 0); + + if (!WiFi.config(ip, gateway, netmask, dns1, dns2)) { + Log::error("Couldn't apply the static IP configuration"); + } +} + +void WifiLink::connect(const String& ssid, const String& password) { + Log::info("Connecting to Wi-Fi \"%s\"", ssid.c_str()); + lastAttempt_ = millis(); + WiFi.begin(ssid.c_str(), password.length() > 0 ? password.c_str() : nullptr); +} + +void WifiLink::loop() { + if (!WIFI_AVAILABLE) return; + const bool isConnected = connected() && hasIp(); + + if (isConnected && !wasConnected_) { + Log::info("Wi-Fi connected: %s, IPv4 %s, RSSI %d dBm", ssid().c_str(), ipv4().c_str(), rssi()); + disconnectedSince_ = 0; + } else if (!isConnected && wasConnected_) { + Log::warn("Wi-Fi connection lost"); + disconnectedSince_ = millis(); + } + wasConnected_ = isConnected; + + if (isConnected || !settings_.hasWifi()) return; + + if (disconnectedSince_ == 0) disconnectedSince_ = millis(); + // The driver reconnects on its own; restart the attempt if it stays down. + if (millis() - disconnectedSince_ > config::WIFI_RECONNECT_AFTER_MS && + millis() - lastAttempt_ > config::WIFI_RECONNECT_AFTER_MS) { + WiFi.disconnect(); + connect(settings_.wifiSsid(), settings_.wifiPassword()); + } +} + +bool WifiLink::tryNetwork(const String& ssid, const String& password) { + if (!WIFI_AVAILABLE) { + Log::warn("Emulator build: can't connect to \"%s\"", ssid.c_str()); + return false; + } + const String previousSsid = settings_.wifiSsid(); + const String previousPassword = settings_.wifiPassword(); + + WiFi.disconnect(); + delay(100); + applyIpConfig(); + connect(ssid, password); + + const uint32_t start = millis(); + while (millis() - start < config::WIFI_TRY_CONNECT_MS) { + if (connected() && hasIp()) { + settings_.setWifi(ssid, password); + Log::info("Wi-Fi \"%s\" saved", ssid.c_str()); + return true; + } + delay(100); + } + + Log::warn("Couldn't connect to \"%s\"", ssid.c_str()); + WiFi.disconnect(); + if (previousSsid.length() > 0) connect(previousSsid, previousPassword); + return false; +} + +void WifiLink::reconnect() { + if (!WIFI_AVAILABLE) return; + WiFi.disconnect(); + delay(100); + applyIpConfig(); + if (settings_.hasWifi()) connect(settings_.wifiSsid(), settings_.wifiPassword()); +} + +bool WifiLink::connected() const { + return WIFI_AVAILABLE && WiFi.status() == WL_CONNECTED; +} + +bool WifiLink::hasIp() const { + return WIFI_AVAILABLE && WiFi.localIP() != IPAddress(0, 0, 0, 0); +} + +std::vector WifiLink::scan() { + std::vector networks; + if (!WIFI_AVAILABLE) return networks; + + const int count = WiFi.scanNetworks(); + for (int i = 0; i < count && networks.size() < 20; i++) { + const String ssid = WiFi.SSID(i); + if (ssid.length() == 0) continue; + bool duplicate = false; + for (const Network& network : networks) duplicate = duplicate || network.ssid == ssid; + // Results are sorted by signal strength, so the first entry of an SSID is its strongest. + if (!duplicate) networks.push_back({ssid, WiFi.RSSI(i), WiFi.encryptionType(i) != WIFI_AUTH_OPEN}); + } + WiFi.scanDelete(); + return networks; +} + +String WifiLink::ipv4() const { + return WiFi.localIP().toString(); +} + +String WifiLink::linkLocalIpv6() const { + return WiFi.linkLocalIPv6().toString(); +} + +String WifiLink::globalIpv6() const { + return WiFi.globalIPv6().toString(); +} + +int WifiLink::rssi() const { + return WIFI_AVAILABLE ? WiFi.RSSI() : 0; +} + +String WifiLink::ssid() const { + return WiFi.SSID(); +} diff --git a/src/net/WifiLink.h b/src/net/WifiLink.h new file mode 100644 index 0000000..d012add --- /dev/null +++ b/src/net/WifiLink.h @@ -0,0 +1,55 @@ +#pragma once + +#include + +#include + +#include "core/Settings.h" + +/** + * Station-mode Wi-Fi: DHCP or a static IPv4 configuration, IPv6 (link-local and SLAAC/DHCPv6), + * automatic reconnects. Credentials come from Settings, never from compiled-in values. + */ +class WifiLink { + public: + explicit WifiLink(Settings& settings) : settings_(settings) {} + + void begin(); + void loop(); + + bool connected() const; + /** True once the current connection has an IPv4 address. */ + bool hasIp() const; + + /** + * Connects to a new network and stores it only if the connection succeeds within + * `config::WIFI_TRY_CONNECT_MS`. Otherwise reconnects to the previous network. + */ + bool tryNetwork(const String& ssid, const String& password); + + /** Applies changed IP settings (static/DHCP) by reconnecting. */ + void reconnect(); + + struct Network { + String ssid; + int rssi; + bool secured; + }; + /** Scans for networks (blocking, a few seconds); at most 20, strongest first, no duplicates. */ + std::vector scan(); + + String ipv4() const; + String linkLocalIpv6() const; + String globalIpv6() const; + int rssi() const; + String ssid() const; + + private: + Settings& settings_; + bool wasConnected_ = false; + uint32_t disconnectedSince_ = 0; + uint32_t lastAttempt_ = 0; + + void applyIpConfig(); + void connect(const String& ssid, const String& password); +}; diff --git a/src/ota/OtaUpdater.cpp b/src/ota/OtaUpdater.cpp new file mode 100644 index 0000000..68656bf --- /dev/null +++ b/src/ota/OtaUpdater.cpp @@ -0,0 +1,187 @@ +#include "ota/OtaUpdater.h" + +#include +#include +#include + +#include + +#include "core/CaBundle.h" +#include "core/Log.h" + +namespace { + +constexpr uint32_t TASK_STACK = 12 * 1024; +constexpr uint32_t READ_TIMEOUT_MS = 30000; +constexpr size_t CHUNK = 4096; + +} // namespace + +const char* OtaUpdater::stateName(State state) { + switch (state) { + case State::Idle: + return "idle"; + case State::Downloading: + return "downloading"; + case State::Installed: + return "installed"; + case State::Failed: + return "failed"; + } + return "idle"; +} + +bool OtaUpdater::start(const netignite::ServerUrl& server, const std::string& authorization, + const netignite::protocol::OtaRequest& request, std::string& error) { + if (busy()) { + error = "An update is already running"; + return false; + } + if (OtaWriter::requiresSignature() && request.signature.empty()) { + error = "This firmware only installs signed updates, and the update has no signature"; + return false; + } + + server_ = server; + authorization_ = authorization; + request_ = request; + { + portENTER_CRITICAL(&lock_); + progress_.version = request.version; + progress_.error.clear(); + portEXIT_CRITICAL(&lock_); + } + setProgress(State::Downloading, 0); + + if (xTaskCreate(&OtaUpdater::taskMain, "ota", TASK_STACK, this, 1, &task_) != pdPASS) { + task_ = nullptr; + setProgress(State::Failed, 0, "Not enough memory to start the update"); + error = "Not enough memory to start the update"; + return false; + } + return true; +} + +bool OtaUpdater::busy() const { + portENTER_CRITICAL(&lock_); + const bool running = progress_.state == State::Downloading; + portEXIT_CRITICAL(&lock_); + return running; +} + +OtaUpdater::Progress OtaUpdater::progress() const { + portENTER_CRITICAL(&lock_); + Progress copy = progress_; + portEXIT_CRITICAL(&lock_); + return copy; +} + +void OtaUpdater::setProgress(State state, uint8_t percent, const char* error) { + portENTER_CRITICAL(&lock_); + if (progress_.state != state || progress_.percent != percent) progress_.revision++; + progress_.state = state; + progress_.percent = percent; + if (error) progress_.error = error; + portEXIT_CRITICAL(&lock_); +} + +void OtaUpdater::taskMain(void* self) { + static_cast(self)->run(); + static_cast(self)->task_ = nullptr; + vTaskDelete(nullptr); +} + +void OtaUpdater::run() { + const std::string url = server_.url(request_.path); + Log::info("Downloading firmware %s (%lu bytes)", request_.version.c_str(), + static_cast(request_.size)); + + std::unique_ptr client; + if (server_.secure) { + auto* secure = new NetworkClientSecure(); + secure->setCACertBundle(caBundle::data(), caBundle::size()); + client.reset(secure); + } else { + client.reset(new NetworkClient()); + } + + auto fail = [this](const String& message) { + writer_.abort(); + Log::error("Update failed: %s", message.c_str()); + setProgress(State::Failed, 0, message.c_str()); + }; + + HTTPClient http; + http.setTimeout(READ_TIMEOUT_MS); + http.setReuse(false); + if (!http.begin(*client, url.c_str())) { + fail("Invalid download URL"); + return; + } + http.addHeader("Authorization", authorization_.c_str()); + + const int status = http.GET(); + if (status != HTTP_CODE_OK) { + fail(status > 0 ? String("The server answered ") + status : String("Download failed: ") + + HTTPClient::errorToString(status)); + http.end(); + return; + } + if (http.getSize() != static_cast(request_.size)) { + fail("The server sent a file of the wrong size"); + http.end(); + return; + } + + String error; + if (!writer_.begin(request_, error)) { + fail(error); + http.end(); + return; + } + + NetworkClient* stream = http.getStreamPtr(); + std::unique_ptr buffer(new uint8_t[CHUNK]); + uint32_t lastData = millis(); + uint8_t percent = 0; + + while (writer_.written() < request_.size) { + const size_t available = stream->available(); + if (available == 0) { + if (!stream->connected() || millis() - lastData > READ_TIMEOUT_MS) { + fail("The download stopped"); + http.end(); + return; + } + delay(5); + continue; + } + + const size_t wanted = std::min({available, CHUNK, request_.size - writer_.written()}); + const int read = stream->read(buffer.get(), wanted); + if (read <= 0) continue; + lastData = millis(); + + if (!writer_.write(buffer.get(), static_cast(read), error)) { + fail(error); + http.end(); + return; + } + // A fast download could keep this task busy for seconds; let the idle task (task watchdog) run. + delay(1); + + const uint8_t now = static_cast(static_cast(writer_.written()) * 100 / request_.size); + if (now != percent) { + percent = now; + setProgress(State::Downloading, percent); + } + } + http.end(); + + if (!writer_.finish(error)) { + fail(error); + return; + } + Log::info("Firmware %s installed", request_.version.c_str()); + setProgress(State::Installed, 100); +} diff --git a/src/ota/OtaUpdater.h b/src/ota/OtaUpdater.h new file mode 100644 index 0000000..a90a1ee --- /dev/null +++ b/src/ota/OtaUpdater.h @@ -0,0 +1,50 @@ +#pragma once + +#include + +#include + +#include "netignite/Protocol.h" +#include "netignite/ServerUrl.h" +#include "ota/OtaWriter.h" + +/** + * Runs an OTA update in its own task: downloads the image from the agent's server (with the agent + * credential) into an OtaWriter. The main loop polls progress() and reports it to the server. + */ +class OtaUpdater { + public: + enum class State : uint8_t { Idle, Downloading, Installed, Failed }; + + struct Progress { + State state = State::Idle; + uint8_t percent = 0; + /** Changes whenever state or percent change. */ + uint32_t revision = 0; + std::string version; + std::string error; + }; + + static const char* stateName(State state); + + /** Starts the download task. False (with an error) if an update is running or can't start. */ + bool start(const netignite::ServerUrl& server, const std::string& authorization, + const netignite::protocol::OtaRequest& request, std::string& error); + + bool busy() const; + Progress progress() const; + + private: + netignite::ServerUrl server_; + std::string authorization_; + netignite::protocol::OtaRequest request_; + OtaWriter writer_; + + mutable portMUX_TYPE lock_ = portMUX_INITIALIZER_UNLOCKED; + Progress progress_; + TaskHandle_t task_ = nullptr; + + static void taskMain(void* self); + void run(); + void setProgress(State state, uint8_t percent, const char* error = nullptr); +}; diff --git a/src/ota/OtaWriter.cpp b/src/ota/OtaWriter.cpp new file mode 100644 index 0000000..f239e16 --- /dev/null +++ b/src/ota/OtaWriter.cpp @@ -0,0 +1,125 @@ +#include "ota/OtaWriter.h" + +#include +#include + +#include + +#include "core/Log.h" +// Generated by scripts/signing_key.py: defines NETIGNITE_OTA_PUBLIC_KEY if a key is configured. +#include "netignite_signing_key.h" + +bool OtaWriter::requiresSignature() { +#ifdef NETIGNITE_OTA_PUBLIC_KEY + return true; +#else + return false; +#endif +} + +bool OtaWriter::begin(const netignite::protocol::OtaRequest& request, String& error) { + abort(); + + if (requiresSignature() && request.signature.empty()) { + error = "This firmware only installs signed updates, and the update has no signature"; + return false; + } + if (!Update.begin(request.size, U_FLASH)) { + error = String("Can't start the update: ") + Update.errorString(); + return false; + } + + request_ = request; + written_ = 0; + mbedtls_sha256_init(&sha_); + mbedtls_sha256_starts(&sha_, 0); + active_ = true; + return true; +} + +bool OtaWriter::write(const uint8_t* data, size_t length, String& error) { + if (!active_) { + error = "No update in progress"; + return false; + } + if (written_ + length > request_.size) { + error = "The download is larger than announced"; + abort(); + return false; + } + + mbedtls_sha256_update(&sha_, data, length); + if (Update.write(const_cast(data), length) != length) { + error = String("Writing to flash failed: ") + Update.errorString(); + abort(); + return false; + } + written_ += length; + return true; +} + +bool OtaWriter::finish(String& error) { + if (!active_) { + error = "No update in progress"; + return false; + } + if (written_ != request_.size) { + error = "The download is incomplete"; + abort(); + return false; + } + + uint8_t hash[32]; + mbedtls_sha256_finish(&sha_, hash); + mbedtls_sha256_free(&sha_); + + if (memcmp(hash, request_.sha256.data(), sizeof(hash)) != 0) { + error = "The download doesn't match its SHA-256"; + abort(); + return false; + } + if (!verifySignature(hash, error)) { + abort(); + return false; + } + if (!Update.end()) { + error = String("Can't activate the update: ") + Update.errorString(); + abort(); + return false; + } + + active_ = false; + return true; +} + +void OtaWriter::abort() { + if (!active_) return; + active_ = false; + mbedtls_sha256_free(&sha_); + Update.abort(); +} + +bool OtaWriter::verifySignature(const uint8_t hash[32], String& error) const { +#ifdef NETIGNITE_OTA_PUBLIC_KEY + static const char KEY[] = NETIGNITE_OTA_PUBLIC_KEY; + + mbedtls_pk_context key; + mbedtls_pk_init(&key); + int result = mbedtls_pk_parse_public_key(&key, reinterpret_cast(KEY), sizeof(KEY)); + if (result == 0) { + result = mbedtls_pk_verify(&key, MBEDTLS_MD_SHA256, hash, 32, request_.signature.data(), + request_.signature.size()); + } + mbedtls_pk_free(&key); + + if (result != 0) { + error = "The update's signature is invalid"; + return false; + } + Log::info("Update signature verified"); +#else + (void)hash; + (void)error; +#endif + return true; +} diff --git a/src/ota/OtaWriter.h b/src/ota/OtaWriter.h new file mode 100644 index 0000000..af0f866 --- /dev/null +++ b/src/ota/OtaWriter.h @@ -0,0 +1,39 @@ +#pragma once + +#include +#include + +#include "netignite/Protocol.h" + +/** + * Writes a firmware image into the inactive OTA slot while hashing it. `finish()` only activates + * the image if its size and SHA-256 match and, when a public key is compiled in + * (`scripts/signing_key.py`), its ECDSA P-256 signature is valid. Used by OTA downloads and, in + * emulator builds, by the serial `ota-*` test commands. + */ +class OtaWriter { + public: + ~OtaWriter() { abort(); } + + /** Whether this firmware only accepts signed images. */ + static bool requiresSignature(); + + bool begin(const netignite::protocol::OtaRequest& request, String& error); + bool write(const uint8_t* data, size_t length, String& error); + /** Verifies the image and makes it the boot image. */ + bool finish(String& error); + void abort(); + + bool active() const { return active_; } + uint32_t written() const { return written_; } + uint32_t size() const { return request_.size; } + const std::string& version() const { return request_.version; } + + private: + netignite::protocol::OtaRequest request_; + mbedtls_sha256_context sha_{}; + bool active_ = false; + uint32_t written_ = 0; + + bool verifySignature(const uint8_t hash[32], String& error) const; +}; diff --git a/src/ota/Rollback.cpp b/src/ota/Rollback.cpp new file mode 100644 index 0000000..b891d73 --- /dev/null +++ b/src/ota/Rollback.cpp @@ -0,0 +1,87 @@ +#include "ota/Rollback.h" + +#include + +#include "Config.h" +#include "core/Log.h" + +/** + * Arduino's core marks a new firmware valid on its first boot unless this returns true; the agent + * confirms it itself once it has reached the server (rollback::confirm). + */ +extern "C" bool verifyRollbackLater() { + return true; +} + +namespace rollback { + +namespace { + +bool pending_ = false; +uint32_t deadline_ = 0; +String failedUpdate_; + +} // namespace + +void begin(Settings& settings) { + const esp_partition_t* running = esp_ota_get_running_partition(); + esp_ota_img_states_t state; + pending_ = running && esp_ota_get_state_partition(running, &state) == ESP_OK && + state == ESP_OTA_IMG_PENDING_VERIFY; + deadline_ = millis() + config::OTA_CONFIRM_TIMEOUT_MS; + + const String target = settings.otaVersion(); + if (target.length() > 0) { + if (running && settings.otaPartition() == running->label) { + Log::info("Updated to firmware %s", config::FIRMWARE_VERSION); + if (!pending_) settings.clearOtaMarker(); + } else { + // The bootloader went back to this partition: the update crashed or never confirmed. + failedUpdate_ = target; + settings.clearOtaMarker(); + Log::warn("The update to %s failed; running %s again", target.c_str(), config::FIRMWARE_VERSION); + } + } + + if (pending_) { + Log::info("New firmware on probation: it stays installed once it reaches the server (within %lu s)", + static_cast(config::OTA_CONFIRM_TIMEOUT_MS / 1000)); + } +} + +bool pending() { + return pending_; +} + +void confirm(Settings& settings) { + if (!pending_) return; + if (esp_ota_mark_app_valid_cancel_rollback() != ESP_OK) { + Log::error("Couldn't confirm the new firmware"); + return; + } + pending_ = false; + settings.clearOtaMarker(); + Log::info("Firmware %s confirmed", config::FIRMWARE_VERSION); +} + +void loop() { + if (!pending_ || static_cast(millis() - deadline_) < 0) return; + + Log::error("The new firmware didn't reach the server in time; going back to the previous firmware"); + delay(200); + esp_ota_mark_app_invalid_rollback_and_reboot(); + // Only returns if there is no previous firmware to go back to. + pending_ = false; + Log::error("No previous firmware to go back to; keeping this one"); +} + +const String& failedUpdate() { + return failedUpdate_; +} + +void recordInstalled(Settings& settings, const String& version) { + const esp_partition_t* next = esp_ota_get_boot_partition(); + settings.setOtaMarker(version, next ? next->label : ""); +} + +} // namespace rollback diff --git a/src/ota/Rollback.h b/src/ota/Rollback.h new file mode 100644 index 0000000..c53f322 --- /dev/null +++ b/src/ota/Rollback.h @@ -0,0 +1,32 @@ +#pragma once + +#include + +#include "core/Settings.h" + +/** + * After an update the new firmware is on probation (ESP-IDF app rollback): it marks itself valid + * once it has reached the server. If it crashes, or doesn't reach the server within + * `config::OTA_CONFIRM_TIMEOUT_MS`, the bootloader starts the previous firmware again. + */ +namespace rollback { + +/** Call early in setup(), after Settings::load(). */ +void begin(Settings& settings); + +/** True while the running firmware is on probation. */ +bool pending(); + +/** Marks the running firmware as good. Call once the agent is connected to the server. */ +void confirm(Settings& settings); + +/** Rolls back when the probation time is over. */ +void loop(); + +/** Version of an update that failed and was rolled back (reported once per boot), or empty. */ +const String& failedUpdate(); + +/** Remembers an installed update, so the next boot can tell whether it started. */ +void recordInstalled(Settings& settings, const String& version); + +} // namespace rollback diff --git a/src/serial/SerialConsole.cpp b/src/serial/SerialConsole.cpp new file mode 100644 index 0000000..c2d5c01 --- /dev/null +++ b/src/serial/SerialConsole.cpp @@ -0,0 +1,442 @@ +#include "serial/SerialConsole.h" + +#include + +#include "Config.h" +#include "core/DeviceInfo.h" +#include "core/Log.h" +#include "netignite/CommandLine.h" +#include "netignite/Hex.h" +#include "netignite/ServerUrl.h" +#include "netignite/Validation.h" +#include "ota/Rollback.h" + +namespace improv = netignite::improv; + +using netignite::isValidHostname; +using netignite::isValidIpv4; + +void SerialConsole::loop() { + while (Serial.available() > 0) { + const int byte = Serial.read(); + if (byte < 0) break; + + const auto event = demux_.feed(static_cast(byte)); + switch (event.type) { + case netignite::SerialDemux::Event::Type::Line: + handleLine(event.line); + break; + case netignite::SerialDemux::Event::Type::ImprovPacket: + handleImprov(event.improvType, event.improvData); + break; + case netignite::SerialDemux::Event::Type::ImprovBadChecksum: + sendImprov(improv::buildErrorPacket(improv::Error::InvalidRpc)); + break; + case netignite::SerialDemux::Event::Type::None: + break; + } + } +} + +// --- Improv ------------------------------------------------------------------------------------- + +void SerialConsole::sendImprov(const std::vector& packet) { + Log::writeRaw(packet.data(), packet.size()); +} + +improv::State SerialConsole::improvState() const { + return wifi_.connected() && wifi_.hasIp() ? improv::State::Provisioned : improv::State::Authorized; +} + +std::string SerialConsole::dashboardUrl() const { + auto server = netignite::parseServerUrl(settings_.serverUrl().c_str()); + if (!server) return ""; + if (settings_.agentId() != 0) return server->url("/dashboard/agents/" + std::to_string(settings_.agentId())); + return server->url("/dashboard/agents"); +} + +void SerialConsole::handleImprov(uint8_t type, const std::vector& data) { + if (type != static_cast(improv::PacketType::Rpc)) return; + + improv::RpcCommand command; + if (!improv::parseRpc(data, command)) { + sendImprov(improv::buildErrorPacket(improv::Error::InvalidRpc)); + return; + } + sendImprov(improv::buildErrorPacket(improv::Error::None)); + + switch (command.command) { + case improv::Command::GetCurrentState: { + const improv::State state = improvState(); + sendImprov(improv::buildStatePacket(state)); + if (state == improv::State::Provisioned) { + const std::string url = dashboardUrl(); + sendImprov(improv::buildRpcResult(command.command, url.empty() ? std::vector{} + : std::vector{url})); + } + break; + } + + case improv::Command::GetDeviceInfo: + sendImprov(improv::buildRpcResult(command.command, {config::FIRMWARE_NAME, config::FIRMWARE_VERSION, "ESP32", + settings_.deviceName().c_str()})); + break; + + case improv::Command::WifiSettings: { + Log::info("Improv: Wi-Fi settings for \"%s\" received", command.ssid.c_str()); + sendImprov(improv::buildStatePacket(improv::State::Provisioning)); + if (wifi_.tryNetwork(command.ssid.c_str(), command.password.c_str())) { + sendImprov(improv::buildStatePacket(improv::State::Provisioned)); + const std::string url = dashboardUrl(); + sendImprov(improv::buildRpcResult(command.command, url.empty() ? std::vector{} + : std::vector{url})); + } else { + sendImprov(improv::buildErrorPacket(improv::Error::UnableToConnect)); + sendImprov(improv::buildStatePacket(improvState())); + } + break; + } + + case improv::Command::GetWifiNetworks: { + for (const WifiLink::Network& network : wifi_.scan()) { + sendImprov(improv::buildRpcResult( + command.command, {network.ssid.c_str(), std::to_string(network.rssi), network.secured ? "YES" : "NO"})); + } + // An empty result ends the list. + sendImprov(improv::buildRpcResult(command.command, {})); + break; + } + + case improv::Command::Hostname: + case improv::Command::DeviceName: { + // The device name doubles as the network host name; setting it applies after a restart. + // Set: command | length | name length | name + if (data.size() > 2) { + if (static_cast(data[2]) + 3 != data.size()) { + sendImprov(improv::buildErrorPacket(improv::Error::InvalidRpc)); + break; + } + const std::string name(data.begin() + 3, data.end()); + if (!isValidHostname(name)) { + sendImprov(improv::buildErrorPacket(improv::Error::BadHostname)); + break; + } + settings_.setDeviceName(name.c_str()); + } + sendImprov(improv::buildRpcResult(command.command, {settings_.deviceName().c_str()})); + break; + } + + case improv::Command::GetNetworkState: { + const bool online = improvState() == improv::State::Provisioned; + // 0x01 online, 0x02 supports Wi-Fi + std::vector result = {std::to_string((online ? 0x01 : 0x00) | 0x02)}; + const std::string url = dashboardUrl(); + if (online && !url.empty()) result.push_back(url); + sendImprov(improv::buildRpcResult(command.command, result)); + break; + } + + default: + sendImprov(improv::buildErrorPacket(improv::Error::UnknownRpc)); + break; + } +} + +// --- Console ------------------------------------------------------------------------------------ + +void SerialConsole::handleLine(const std::string& line) { + std::vector args; + if (!netignite::splitCommandLine(line, args)) { + Log::print("Unterminated quote. Type \"help\" for the commands."); + return; + } + if (args.empty()) return; + + const std::string& command = args[0]; + if (command == "help" || command == "?") { + printHelp(); + } else if (command == "status") { + printStatus(); + } else if (command == "wifi") { + cmdWifi(args); + } else if (command == "server") { + cmdServer(args); + } else if (command == "enroll") { + cmdEnroll(args); + } else if (command == "setup") { + cmdSetup(args); + } else if (command == "ip") { + cmdIp(args); + } else if (command == "name") { + cmdName(args); + } else if (command == "log") { + cmdLog(args); + } else if (command == "reboot" || command == "restart") { + Log::print("Restarting..."); + delay(200); + ESP.restart(); + } else if (command == "factory-reset") { + cmdFactoryReset(args); +#ifdef NETIGNITE_EMULATOR + } else if (command.rfind("ota-", 0) == 0) { + cmdOta(args); +#endif + } else { + Log::print("Unknown command \"%s\". Type \"help\" for the commands.", command.c_str()); + } +} + +void SerialConsole::printHelp() { + // One line per print: Log::print has a fixed-size buffer. + static const char* const LINES[] = { + "NetIgnite agent commands:", + " status show the agent's state", + " setup connect this agent to a NetIgnite server", + " wifi [password] connect to a Wi-Fi network (quote names with spaces)", + " server change the server address", + " enroll set the agent up again with a new token", + " ip dhcp get the IP address via DHCP (default)", + " ip static [dns1] [dns2]", + " name network host name (letters, digits, -)", + " log log level", + " reboot restart the agent", + " factory-reset confirm erase all settings and credentials", + }; + for (const char* line : LINES) Log::print("%s", line); +} + +void SerialConsole::printStatus() { + const char* agentState = "not configured"; + if (settings_.enrollToken().length() > 0) { + agentState = "waiting to enroll"; + } else if (settings_.hasCredentials()) { + switch (agent_.state()) { + case AgentClient::State::Online: + agentState = "online"; + break; + case AgentClient::State::Connecting: + agentState = "connecting"; + break; + case AgentClient::State::Stopped: + agentState = "waiting for the network"; + break; + } + } + + Log::print("%s %s", config::FIRMWARE_NAME, config::FIRMWARE_VERSION); + Log::print(" device: %s (%s, %s)", settings_.deviceName().c_str(), deviceInfo::mac().c_str(), + deviceInfo::chip().c_str()); + if (wifi_.connected() && wifi_.hasIp()) { + Log::print(" wifi: %s, %d dBm", wifi_.ssid().c_str(), wifi_.rssi()); + Log::print(" ipv4: %s (%s)", wifi_.ipv4().c_str(), settings_.staticIp().enabled ? "static" : "DHCP"); + Log::print(" ipv6: %s %s", wifi_.linkLocalIpv6().c_str(), wifi_.globalIpv6().c_str()); + } else { + Log::print(" wifi: %s", settings_.hasWifi() ? "not connected" : "not set up"); + } + Log::print(" server: %s", settings_.serverUrl().length() > 0 ? settings_.serverUrl().c_str() : "not set"); + if (settings_.agentId() != 0) Log::print(" agent id: %lu", static_cast(settings_.agentId())); + Log::print(" agent: %s", agentState); + Log::print(" partition: %s, reset reason: %s, free heap: %lu", deviceInfo::runningPartition(), + deviceInfo::resetReason(), static_cast(ESP.getFreeHeap())); + + const OtaUpdater::Progress update = agent_.ota().progress(); + if (update.state != OtaUpdater::State::Idle) { + Log::print(" update: %s %s%s%s", update.version.c_str(), OtaUpdater::stateName(update.state), + update.error.empty() ? "" : ": ", update.error.c_str()); + } + if (rollback::pending()) Log::print(" firmware: on probation until it reaches the server"); + if (rollback::failedUpdate().length() > 0) { + Log::print(" firmware: the update to %s failed and was rolled back", rollback::failedUpdate().c_str()); + } + Log::print(" updates: %s", OtaWriter::requiresSignature() ? "signed only" : "checked by SHA-256"); +} + +void SerialConsole::cmdWifi(const std::vector& args) { + if (args.size() < 2 || args.size() > 3 || args[1].empty() || args[1].size() > 32) { + Log::print("Usage: wifi [password]"); + return; + } + const std::string password = args.size() == 3 ? args[2] : ""; + if (password.size() > 64) { + Log::print("The password is too long (max. 64 characters)."); + return; + } + Log::print("Connecting to \"%s\"...", args[1].c_str()); + if (wifi_.tryNetwork(args[1].c_str(), password.c_str())) { + Log::print("Connected and saved."); + } else { + Log::print("Couldn't connect; the previous network is kept."); + } +} + +bool SerialConsole::applyServer(const std::string& url) { + auto server = netignite::parseServerUrl(url); + if (!server) { + Log::print("Invalid server URL. Example: https://app.netignite.dev"); + return false; + } + settings_.setServerUrl(server->url("").c_str()); + return true; +} + +bool SerialConsole::applyEnrollToken(const std::string& token) { + if (token.rfind("netignite_enroll_", 0) != 0 || token.size() > 200) { + Log::print("That isn't an enrollment token. Create one on the agent's page in the dashboard."); + return false; + } + settings_.setEnrollToken(token.c_str()); + return true; +} + +void SerialConsole::cmdServer(const std::vector& args) { + if (args.size() != 2) { + Log::print("Usage: server "); + return; + } + if (!applyServer(args[1])) return; + Log::print("Server set to %s.", settings_.serverUrl().c_str()); + onConfigChanged_(); +} + +void SerialConsole::cmdEnroll(const std::vector& args) { + if (args.size() != 2) { + Log::print("Usage: enroll "); + return; + } + if (!applyEnrollToken(args[1])) return; + Log::print("Enrollment token saved; the agent enrolls as soon as it is online."); + onConfigChanged_(); +} + +void SerialConsole::cmdSetup(const std::vector& args) { + if (args.size() != 3) { + Log::print("Usage: setup "); + return; + } + if (!applyServer(args[1]) || !applyEnrollToken(args[2])) return; + Log::print("Saved. The agent enrolls with %s as soon as it is online.", settings_.serverUrl().c_str()); + onConfigChanged_(); +} + +void SerialConsole::cmdIp(const std::vector& args) { + if (args.size() == 2 && args[1] == "dhcp") { + settings_.setStaticIp({}); + Log::print("Using DHCP. Reconnecting..."); + wifi_.reconnect(); + return; + } + if (args.size() >= 5 && args.size() <= 7 && args[1] == "static") { + for (size_t i = 2; i < args.size(); i++) { + if (!isValidIpv4(args[i])) { + Log::print("\"%s\" isn't an IPv4 address.", args[i].c_str()); + return; + } + } + Settings::StaticIp ip; + ip.enabled = true; + ip.ip = args[2].c_str(); + ip.gateway = args[3].c_str(); + ip.netmask = args[4].c_str(); + if (args.size() > 5) ip.dns1 = args[5].c_str(); + if (args.size() > 6) ip.dns2 = args[6].c_str(); + settings_.setStaticIp(ip); + Log::print("Static IP %s saved. Reconnecting...", ip.ip.c_str()); + wifi_.reconnect(); + return; + } + Log::print("Usage: ip dhcp | ip static [dns1] [dns2]"); +} + +void SerialConsole::cmdName(const std::vector& args) { + if (args.size() != 2 || !isValidHostname(args[1])) { + Log::print("Usage: name (letters, digits and -, max. 63)"); + return; + } + settings_.setDeviceName(args[1].c_str()); + Log::print("Name saved; it is used as the host name after a restart."); +} + +void SerialConsole::cmdLog(const std::vector& args) { + LogLevel level; + if (args.size() != 2 || !Log::parseLevel(args[1].c_str(), level)) { + Log::print("Usage: log "); + return; + } + settings_.setLogLevel(level); + Log::setLevel(level); + Log::print("Log level: %s", Log::levelName(level)); +} + +void SerialConsole::cmdFactoryReset(const std::vector& args) { + if (args.size() != 2 || args[1] != "confirm") { + Log::print("This erases Wi-Fi, server and agent credentials. Type \"factory-reset confirm\"."); + return; + } + settings_.factoryReset(); + Log::print("All settings erased. Restarting..."); + delay(200); + ESP.restart(); +} + +#ifdef NETIGNITE_EMULATOR + +void SerialConsole::cmdOta(const std::vector& args) { + String error; + const std::string& command = args[0]; + + if (command == "ota-begin" && (args.size() == 3 || args.size() == 4)) { + netignite::protocol::OtaRequest request; + request.version = "emulator"; + request.size = static_cast(strtoul(args[1].c_str(), nullptr, 10)); + std::vector digest; + if (request.size == 0 || !netignite::fromHex(args[2], digest) || digest.size() != 32 || + (args.size() == 4 && !netignite::fromHex(args[3], request.signature))) { + Log::print("OTA error: invalid arguments"); + return; + } + std::copy(digest.begin(), digest.end(), request.sha256.begin()); + if (!otaWriter_.begin(request, error)) { + Log::print("OTA error: %s", error.c_str()); + return; + } + Log::print("OTA ready for %lu bytes", static_cast(request.size)); + return; + } + + if (command == "ota-data" && args.size() == 2) { + std::vector data; + if (!netignite::fromHex(args[1], data)) { + Log::print("OTA error: invalid data"); + otaWriter_.abort(); + return; + } + if (!otaWriter_.active()) return; + if (!otaWriter_.write(data.data(), data.size(), error)) { + Log::print("OTA error: %s", error.c_str()); + return; + } + // The emulated UART has no flow control: the test waits for this before sending more. + const uint32_t written = otaWriter_.written(); + if (written % 2000 < data.size() || written == otaWriter_.size()) { + Log::print("OTA ok %lu", static_cast(written)); + } + return; + } + + if (command == "ota-end" && args.size() == 1) { + if (!otaWriter_.finish(error)) { + Log::print("OTA error: %s", error.c_str()); + return; + } + rollback::recordInstalled(settings_, "emulator"); + Log::print("OTA installed. Restarting..."); + delay(200); + ESP.restart(); + return; + } + + Log::print("Usage: ota-begin [signature] | ota-data | ota-end"); +} + +#endif diff --git a/src/serial/SerialConsole.h b/src/serial/SerialConsole.h new file mode 100644 index 0000000..3b4624b --- /dev/null +++ b/src/serial/SerialConsole.h @@ -0,0 +1,63 @@ +#pragma once + +#include + +#include +#include +#include + +#include "agent/AgentClient.h" +#include "core/Settings.h" +#include "net/WifiLink.h" +#include "netignite/Improv.h" +#include "netignite/SerialDemux.h" +#include "ota/OtaWriter.h" + +/** + * The serial port: Improv Wi-Fi for the web installer, and a small text console for setting the + * agent up by hand (see `help`). Secrets are never printed. + */ +class SerialConsole { + public: + SerialConsole(Settings& settings, WifiLink& wifi, AgentClient& agent, std::function onConfigChanged) + : settings_(settings), wifi_(wifi), agent_(agent), onConfigChanged_(std::move(onConfigChanged)) {} + + void loop(); + + private: + Settings& settings_; + WifiLink& wifi_; + AgentClient& agent_; + std::function onConfigChanged_; + netignite::SerialDemux demux_; + + // Improv + void handleImprov(uint8_t type, const std::vector& data); + void sendImprov(const std::vector& packet); + netignite::improv::State improvState() const; + /** Where the web installer's "Visit device" button leads: the agent's dashboard page. */ + std::string dashboardUrl() const; + + // Console + void handleLine(const std::string& line); + void printHelp(); + void printStatus(); + void cmdWifi(const std::vector& args); + void cmdServer(const std::vector& args); + void cmdEnroll(const std::vector& args); + void cmdSetup(const std::vector& args); + void cmdIp(const std::vector& args); + void cmdName(const std::vector& args); + void cmdLog(const std::vector& args); + void cmdFactoryReset(const std::vector& args); + + bool applyServer(const std::string& url); + bool applyEnrollToken(const std::string& token); + +#ifdef NETIGNITE_EMULATOR + // Emulator builds can't download updates (no network): `ota-begin`, `ota-data` and `ota-end` + // feed an image over the serial port instead, to test installing, signatures and rollback. + OtaWriter otaWriter_; + void cmdOta(const std::vector& args); +#endif +}; diff --git a/src/utils/icmp.h b/src/utils/icmp.h deleted file mode 100644 index cd55ab5..0000000 --- a/src/utils/icmp.h +++ /dev/null @@ -1,128 +0,0 @@ -#ifndef ICMP_H -#define ICMP_H - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#define PING_TIMEOUT_MS 5000 - -// State shared between the caller and the ping task. It lives on the heap and is freed by -// whichever side releases it last, so a ping that outlives PING_TIMEOUT_MS can't write to memory -// that is gone. -struct ping_context_t { - std::atomic success{false}; - std::atomic finished{false}; - std::atomic released{false}; -}; - -class ICMP { - public: - - static bool ping(IPAddress dest, size_t count = 1) { - - ip_addr_t target_addr; - target_addr.type = IPADDR_TYPE_V4; - target_addr.u_addr.ip4.addr = static_cast(dest); - - return do_ping(target_addr, count); - } - - static bool ping(IPv6Address dest, size_t count = 1) { - - ip_addr_t target_addr; - target_addr.type = IPADDR_TYPE_V6; - // target_addr.u_addr.ip6.zone = get_netif_index(); - - memcpy(target_addr.u_addr.ip6.addr, static_cast(dest), sizeof(target_addr.u_addr.ip6.addr)); - - return do_ping(target_addr, count); - } - - private: - static bool do_ping(ip_addr_t target_addr, size_t count) { - - esp_ping_config_t ping_config = ESP_PING_DEFAULT_CONFIG(); - ping_config.target_addr = target_addr; - ping_config.count = count; - // ping_config.interface = get_netif_index(); - - ping_context_t *context = new (std::nothrow) ping_context_t(); - if (!context) return false; - - esp_ping_callbacks_t cbs; - cbs.on_ping_success = on_ping_success; - cbs.on_ping_timeout = on_ping_timeout; - cbs.on_ping_end = on_ping_end; - cbs.cb_args = context; - - esp_ping_handle_t ping = nullptr; - if (esp_ping_new_session(&ping_config, &cbs, &ping) != ESP_OK) { - delete context; - return false; - } - if (esp_ping_start(ping) != ESP_OK) { - esp_ping_delete_session(ping); - delete context; - return false; - } - - unsigned long start_time = millis(); - while (!context->finished && (millis() - start_time < PING_TIMEOUT_MS)) { - delay(10); - } - - bool success = context->success; - release(context); - return success; - } - - static void release(ping_context_t *context) { - // The second of the two releases (caller and on_ping_end) frees the context. - if (context->released.exchange(true)) { - delete context; - } - } - - static void on_ping_success(esp_ping_handle_t hdl, void *args) { - ping_context_t *context = static_cast(args); - - context->success = true; - } - - static void on_ping_timeout(esp_ping_handle_t hdl, void *args) {} - - static void on_ping_end(esp_ping_handle_t hdl, void *args) { - ping_context_t *context = static_cast(args); - context->finished = true; - release(context); - - esp_ping_delete_session(hdl); - } - - static int get_netif_index() - { - struct netif *netif = netif_list; - while (netif) { - if (netif->name[0] == 's' && netif->name[1] == 't') { // "st" usually stands for STA - return netif_get_index(netif); // this gives the scope ID - } - netif = netif->next; - } - // Serial.println("No suitable network interface found for ping."); - return 0; - } - - -}; - -#endif // ICMP_H \ No newline at end of file diff --git a/src/utils/inputValidation.h b/src/utils/inputValidation.h deleted file mode 100644 index 509494c..0000000 --- a/src/utils/inputValidation.h +++ /dev/null @@ -1,53 +0,0 @@ -#ifndef INPUT_VALIDATION_H -#define INPUT_VALIDATION_H - -#include "utils/regexp.h" - -class InputValidation { - public: - - static bool macAddressDashed(const char *macAddress) { - if (!macAddress) return false; - - const char* pattern = "^([0-9A-Fa-f]{2}-){5}[0-9A-Fa-f]{2}$"; - return RegExp::match(macAddress, pattern); - } - - static bool macAddressColon(const char *macAddress) { - if (!macAddress) return false; - - const char* pattern = "^([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}$"; - return RegExp::match(macAddress, pattern); - } - - static bool macAddress(const char *macAddress) { - return InputValidation::macAddressColon(macAddress) || InputValidation::macAddressDashed(macAddress); - } - - - static bool port(uint16_t port) { - return port > 0 && port <= 65535; - } - - static bool ipv4Address(const char *ipAddress) { - if (!ipAddress) return false; - - const char* pattern = "^(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])(\\.(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])){3}$"; - return RegExp::match(ipAddress, pattern); - } - - static bool ipv6Address(const char *ipAddress) { - if (!ipAddress) return false; - - const char* pattern = "^(([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|::|(([0-9a-fA-F]{1,4}:){1,7}:)|(([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4})|(([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2})|(([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3})|(([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4})|(([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5})|([0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6}))|(:((:[0-9a-fA-F]{1,4}){1,7}|:))|(::ffff:(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])(\\.(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])){3}))$"; - return RegExp::match(ipAddress, pattern); - } - - static bool ipAddress(const char *ipAddress) { - return InputValidation::ipv4Address(ipAddress) || InputValidation::ipv6Address(ipAddress); - } - -}; - - -#endif // INPUT_VALIDATION_H diff --git a/src/utils/logger.h b/src/utils/logger.h deleted file mode 100644 index 9274862..0000000 --- a/src/utils/logger.h +++ /dev/null @@ -1,76 +0,0 @@ -#ifndef LOGGER_H -#define LOGGER_H - -#include - -enum class LogLevel : uint8_t { - FATAL = 0, - ERROR = 1, - WARN = 2, - INFO = 3, - DEBUG = 4, -}; - -class Logger { - public: - static void init(unsigned long baud = 115200) { - Serial.begin(baud); - while (!Serial) { - delay(10); - } - } - - static void setLevel(LogLevel lvl) { level = lvl; } - static LogLevel getLevel() { return level; } - - static void debug(const String& msg) { log(LogLevel::DEBUG, msg); } - static void info(const String& msg) { log(LogLevel::INFO, msg); } - static void warn(const String& msg) { log(LogLevel::WARN, msg); } - static void error(const String& msg) { log(LogLevel::ERROR, msg); } - static void fatal(const String& msg) { log(LogLevel::FATAL, msg); } - - static void debugln(const String& msg) { logln(LogLevel::DEBUG, msg); } - static void infoln(const String& msg) { logln(LogLevel::INFO, msg); } - static void warnln(const String& msg) { logln(LogLevel::WARN, msg); } - static void errorln(const String& msg) { logln(LogLevel::ERROR, msg); } - static void fatalln(const String& msg) { logln(LogLevel::FATAL, msg); } - - static void print(const String& msg) { Serial.print(msg); } - static void println(const String& msg) { Serial.println(msg); } - - private: - static inline LogLevel level = LogLevel::INFO; - - static const char* levelToStr(LogLevel lvl) { - switch (lvl) { - case LogLevel::DEBUG: - return "DEBUG"; - case LogLevel::INFO: - return "INFO"; - case LogLevel::WARN: - return "WARN"; - case LogLevel::ERROR: - return "ERROR"; - case LogLevel::FATAL: - return "FATAL"; - default: - return "UNKNOWN"; - } - } - - static void log(LogLevel msgLvl, const String& msg) { - if (msgLvl > level) return; - - Serial.printf("[%s] ", levelToStr(msgLvl)); - Serial.print(msg); - } - - static void logln(LogLevel msgLvl, const String& msg) { - if (msgLvl > level) return; - - Serial.printf("[%s] ", levelToStr(msgLvl)); - Serial.println(msg); - } -}; - -#endif // LOGGER_H \ No newline at end of file diff --git a/src/utils/regexp.h b/src/utils/regexp.h deleted file mode 100644 index c9d0003..0000000 --- a/src/utils/regexp.h +++ /dev/null @@ -1,25 +0,0 @@ -#ifndef REGEXP_H -#define REGEXP_H - -#include - -class RegExp { - public: - - static bool match(const char* str, const char* pattern) { - if (!str || !pattern) return false; - - regex_t regex; - if (regcomp(®ex, pattern, REG_EXTENDED | REG_NOSUB) != 0) { - // regex compilation failed - return false; - } - - int result = regexec(®ex, str, 0, NULL, 0); - regfree(®ex); - return result == 0; - - } -}; - -#endif // REGEXP_H diff --git a/src/utils/utils.h b/src/utils/utils.h deleted file mode 100644 index 09a2ec6..0000000 --- a/src/utils/utils.h +++ /dev/null @@ -1,94 +0,0 @@ -#ifndef UTILS_H -#define UTILS_H - -#include "utils/logger.h" -#include "utils/inputValidation.h" -#include - -class Utils { - public: - - // Starts NTP and waits up to `timeoutMs` for the first sync. Returns whether the clock is set. - // SNTP keeps retrying in the background, so a timeout (e.g. no network at boot) only delays TLS - // connections until the time is known instead of blocking the boot forever. - static bool setClock(unsigned long timeoutMs) { - configTime(0, 0, "pool.ntp.org", "time.nist.gov"); - - Logger::info(F("Waiting for NTP time sync: ")); - unsigned long start = millis(); - time_t nowSecs = time(nullptr); - while(nowSecs < 8 * 3600 * 2) { - if (millis() - start >= timeoutMs) { - Logger::println(""); - Logger::warnln(F("NTP time sync timed out. Continuing; the time is set once the network is available.")); - return false; - } - delay(500); - Logger::print(F(".")); - yield(); - nowSecs = time(nullptr); - } - - Logger::println(""); - struct tm timeinfo; - gmtime_r(&nowSecs, &timeinfo); - Logger::info(F("Current UTC time: ")); - Logger::print(asctime(&timeinfo)); - return true; - } - - - static String utf8ToHexStr(const char* str) { - String hexStr = ""; - - for (int i = 0; str[i] != '\0'; i++) { - char hex[3]; - snprintf(hex, sizeof(hex), "%02X", str[i]); - hexStr += hex; - } - - return hexStr; - } - - // Returns the EUI-64 IPv6 link-local address for a MAC address, or an empty string if the MAC - // address is invalid. - static String macToLinkLocalIPv6(const char* macStr) { - - if (!macStr) return String(); - - // Convert MAC string with colons or dashes to byte array - uint8_t mac[6]; - if (InputValidation::macAddressColon(macStr)) { - sscanf(macStr, "%hhx:%hhx:%hhx:%hhx:%hhx:%hhx", &mac[0], &mac[1], &mac[2], &mac[3], &mac[4], &mac[5]); - } else if (InputValidation::macAddressDashed(macStr)) { - sscanf(macStr, "%hhx-%hhx-%hhx-%hhx-%hhx-%hhx", &mac[0], &mac[1], &mac[2], &mac[3], &mac[4], &mac[5]); - } else { - return String(); - } - - char buffer[40]; - - uint8_t eui64[8]; - eui64[0] = mac[0] ^ 0x02; - eui64[1] = mac[1]; - eui64[2] = mac[2]; - eui64[3] = 0xFF; - eui64[4] = 0xFE; - eui64[5] = mac[3]; - eui64[6] = mac[4]; - eui64[7] = mac[5]; - - snprintf(buffer, sizeof(buffer), - "fe80:0000:0000:0000:%02x%02x:%02x%02x:%02x%02x:%02x%02x", - eui64[0], eui64[1], // first 16 bits - eui64[2], eui64[3], // second 16 bits - eui64[4], eui64[5], // third 16 bits - eui64[6], eui64[7] // fourth 16 bits - ); - - // Returned by value: the caller previously had to free a strdup'd buffer and never did. - return String(buffer); - } -}; - -#endif // UTILS_H \ No newline at end of file diff --git a/src/utils/wlan.h b/src/utils/wlan.h deleted file mode 100644 index 8e5834c..0000000 --- a/src/utils/wlan.h +++ /dev/null @@ -1,107 +0,0 @@ -#ifndef WLAN_H -#define WLAN_H - -#include - -#include "utils/logger.h" - -struct WLANConfig { - const char* ssid; - const char* password; - IPAddress local_IP; - IPAddress gateway; - IPAddress subnet_mask; - IPAddress primary_DNS = (uint32_t) 0; - IPAddress secondary_DNS = (uint32_t) 0; -}; - -class WLAN { - public: - static void setup(const WLANConfig& config) { - - ssid = config.ssid; - password = config.password; - - WiFi.disconnect(true); - delay(1000); - - Logger::infoln("Connecting to Wifi..."); - - if (!WiFi.config(config.local_IP, config.gateway, config.subnet_mask, config.primary_DNS, config.secondary_DNS)) { - Logger::errorln("STA Failed to configure Wifi"); - } else { - Logger::infoln("STA Configured"); - } - - WiFi.onEvent(onWiFiConnect, WiFiEvent_t::ARDUINO_EVENT_WIFI_STA_CONNECTED); - WiFi.onEvent(onWiFiGotIP, WiFiEvent_t::ARDUINO_EVENT_WIFI_STA_GOT_IP); - WiFi.onEvent(onWiFiDisconnect, WiFiEvent_t::ARDUINO_EVENT_WIFI_STA_DISCONNECTED); - - Logger::infoln("Connecting to " + String(ssid) + "..."); - WiFi.begin(ssid, password); - - WiFi.setAutoReconnect(false); // Disable auto reconnect to handle it manually - } - - static void loop() { - if (shouldReconnect && (millis() >= lastDisconnectTime + reconnectInterval)) { - Logger::infoln("Trying to Reconnect to WiFi..."); - WiFi.begin(ssid, password); - shouldReconnect = false; // reset flag - } - } - - private: - static inline const char* ssid = nullptr; - static inline const char* password = nullptr; - - static inline unsigned long lastDisconnectTime = 0; - static inline bool shouldReconnect = false; - static inline const unsigned long reconnectInterval = 60000; // 60 seconds - - - static void onWiFiConnect(WiFiEvent_t event, WiFiEventInfo_t info) { - Logger::infoln("Wifi connected to " + String(ssid) + ". Waiting for IP..."); - } - - static void onWiFiGotIP(WiFiEvent_t event, WiFiEventInfo_t info) { - - if (WiFi.enableIpV6()) { - Logger::infoln("IPv6 enabled on WiFi"); - } else { - Logger::errorln("Failed to enable IPv6 on WiFi"); - } - - Logger::infoln("Got IP address!"); - Logger::infoln("IP address: " + WiFi.localIP().toString()); - - esp_netif_t* netif = esp_netif_get_handle_from_ifkey("WIFI_STA_DEF"); - - esp_ip6_addr_t ip6_addrs[CONFIG_LWIP_IPV6_NUM_ADDRESSES]; - int num_ipv6_addrs = esp_netif_get_all_ip6(netif, ip6_addrs); - - // Optional: print or process the IPv6 addresses - for (int i = 0; i < num_ipv6_addrs; i++) { - - String ip_str = IPv6Address(ip6_addrs[i].addr).toString(); - - Logger::infoln("IPv6 address: " + ip_str); - } - - - } - - static void onWiFiDisconnect(WiFiEvent_t event, WiFiEventInfo_t info) { - - Logger::infoln("Disconnected from WiFi!"); - Logger::info("WiFi lost connection. Reason: "); - Logger::infoln(String(info.wifi_sta_disconnected.reason)); - Logger::infoln("Trying to reconnect in " + String(reconnectInterval / 1000) + " seconds..."); - - lastDisconnectTime = millis(); - shouldReconnect = true; - } - -}; - -#endif // WLAN_H \ No newline at end of file diff --git a/src/utils/wol.h b/src/utils/wol.h deleted file mode 100644 index 495784d..0000000 --- a/src/utils/wol.h +++ /dev/null @@ -1,30 +0,0 @@ -#ifndef WOL_H -#define WOL_H - -#include -#include -#include -#include "utils/inputValidation.h" -#include "utils/utils.h" - -extern WakeOnLan WakeOnLanClient; - -class WOL { - public: - - static bool wakeup(const char *target_mac_address, const uint16_t port) { - - if (!InputValidation::macAddress(target_mac_address) || !InputValidation::port(port)) { - return false; - } - - WakeOnLanClient.setRepeat(3, 100); - WakeOnLanClient.calculateBroadcastAddress(WiFi.localIP(), WiFi.subnetMask()); - WakeOnLanClient.sendMagicPacket(target_mac_address, port); - - return true; - } - -}; - -#endif // WOL_H \ No newline at end of file diff --git a/test/emulator/.gitignore b/test/emulator/.gitignore new file mode 100644 index 0000000..c2658d7 --- /dev/null +++ b/test/emulator/.gitignore @@ -0,0 +1 @@ +node_modules/ diff --git a/test/emulator/README.md b/test/emulator/README.md new file mode 100644 index 0000000..bdf13cb --- /dev/null +++ b/test/emulator/README.md @@ -0,0 +1,39 @@ +# Emulator test + +Boots the firmware in [Espressif's QEMU](https://github.com/espressif/qemu) and checks it without +hardware: + +- `improv-test.ts`: Improv Wi-Fi (with `improv-wifi-serial-sdk`, the client ESP Web Tools uses), + the console commands, and that settings survive a restart and a factory reset. +- `ota-test.ts`: OTA updates. Emulator builds accept an image over the serial port (`ota-begin`, + `ota-data`, `ota-end`) and install it with the same code as a download. The test checks that + unsigned, wrongly signed and corrupted images are rejected, that a signed image boots from the + other slot on probation, and that it rolls back because it can't reach a server. The emulator + firmware has the test key in `keys/` compiled in; see `keys/README.md`. + +QEMU has no Wi-Fi radio, so this uses the `esp32dev-qemu` build, which is the normal firmware with +Wi-Fi switched off. Networking (enrollment, the server connection, Wake-on-LAN, status checks) needs +a real board. + +```bash +pio run -e esp32dev-qemu +cd test/emulator +bun install +bun improv-test.ts /path/to/qemu-system-xtensa ../../.pio/build/esp32dev-qemu/firmware.factory.bin /tmp +bun ota-test.ts /path/to/qemu-system-xtensa ../../.pio/build/esp32dev-qemu/firmware.factory.bin ../../.pio/build/esp32dev-qemu/firmware.bin /tmp +``` + +The OTA upload over the emulated serial port takes a few minutes. + +Download QEMU from the [releases](https://github.com/espressif/qemu/releases) +(`qemu-xtensa-softmmu-*`). The tests start it with the timer-group watchdogs disabled, like +ESP-IDF's own QEMU runner; with them enabled, slow hosts reset or crash the emulated chip during +boot. Two QEMU quirks the tests work around (real chips have neither): + +- QEMU keeps peripheral state across a software restart. An interrupt left over from before (the + bignum accelerator after an update's ECDSA check, or serial input that is still arriving) crashes + the next boot in the task watchdog's handler. So the tests check that the device announces the + restart, then power-cycle QEMU instead; the flash file keeps what was written. +- The emulated UART hands received bytes to the firmware only in blocks of its FIFO threshold, so a + short line can wait for more input. The tests send empty lines after every command. If a check fails, the test prints the device's serial output, and CI keeps it as the +`emulator-serial-log` artifact. diff --git a/test/emulator/bun.lock b/test/emulator/bun.lock new file mode 100644 index 0000000..2ad58a3 --- /dev/null +++ b/test/emulator/bun.lock @@ -0,0 +1,33 @@ +{ + "lockfileVersion": 2, + "configVersion": 1, + "workspaces": { + "": { + "name": "netignite-agent-emulator-test", + "dependencies": { + "improv-wifi-serial-sdk": "2.8.1", + }, + }, + }, + "packages": { + "@lit-labs/ssr-dom-shim": ["@lit-labs/ssr-dom-shim@1.6.0", "", {}, "sha512-VHb0ALPMTlgKjM6yIxxoQNnpKyUKLD04VzeQdsiXkMqkvYlAHxq9glGLmgbb889/1GsohSOAjvQYoiBppXFqrQ=="], + + "@lit/context": ["@lit/context@1.1.6", "", { "dependencies": { "@lit/reactive-element": "^1.6.2 || ^2.1.0" } }, "sha512-M26qDE6UkQbZA2mQ3RjJ3Gzd8TxP+/0obMgE5HfkfLhEEyYE3Bui4A5XHiGPjy0MUGAyxB3QgVuw2ciS0kHn6A=="], + + "@lit/reactive-element": ["@lit/reactive-element@2.1.2", "", { "dependencies": { "@lit-labs/ssr-dom-shim": "^1.5.0" } }, "sha512-pbCDiVMnne1lYUIaYNN5wrwQXDtHaYtg7YEFPeW+hws6U47WeFvISGUWekPGKWOP1ygrs0ef0o1VJMk1exos5A=="], + + "@material/web": ["@material/web@2.5.0", "", { "dependencies": { "@lit/context": "^1.1.6", "lit": "^2.8.0 || ^3.0.0", "tslib": "^2.4.0" } }, "sha512-x2Uovyq8E/Zc1xHXd9s1eBMXF5pMHVAt70pISKd0fL3Ajj4L6zQaQUY/awcfR2RDAMKXC7i4+vr0arv1YaOR/g=="], + + "@types/trusted-types": ["@types/trusted-types@2.0.7", "", {}, "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw=="], + + "improv-wifi-serial-sdk": ["improv-wifi-serial-sdk@2.8.1", "", { "dependencies": { "@material/web": "^2.4.1", "tslib": "^2.8.1" } }, "sha512-KU2D7bNbdUDUX0JK2nQGTGTuxJNH+loXyMR6DXBIzAh4/2dwiR2cxK0igvl+dLWjf4wRv43iUn/eQuUClw5V3g=="], + + "lit": ["lit@3.3.3", "", { "dependencies": { "@lit/reactive-element": "^2.1.0", "lit-element": "^4.2.0", "lit-html": "^3.3.0" } }, "sha512-fycuvZg/hkpozL00lm1pEJH5nN/lr9ZXd6mJI2HSN4+Bzc+LDNdEApJ6HFbPkdFNHLvOplIIuJvxkS4XUxqirw=="], + + "lit-element": ["lit-element@4.2.2", "", { "dependencies": { "@lit-labs/ssr-dom-shim": "^1.5.0", "@lit/reactive-element": "^2.1.0", "lit-html": "^3.3.0" } }, "sha512-aFKhNToWxoyhkNDmWZwEva2SlQia+jfG0fjIWV//YeTaWrVnOxD89dPKfigCUspXFmjzOEUQpOkejH5Ly6sG0w=="], + + "lit-html": ["lit-html@3.3.3", "", { "dependencies": { "@types/trusted-types": "^2.0.2" } }, "sha512-el8M6jK2o3RXBnrSHX3ZKrsN8zEV63pSExTO1wYJz7QndGYZ8353e2a5PPX+qHe2aGayfnchQmkAojaWAREOIA=="], + + "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + } +} diff --git a/test/emulator/improv-test.ts b/test/emulator/improv-test.ts new file mode 100644 index 0000000..a7bc46f --- /dev/null +++ b/test/emulator/improv-test.ts @@ -0,0 +1,248 @@ +// Boots the emulator firmware (pio run -e esp32dev-qemu) in Espressif's QEMU and drives its serial +// port with the official Improv Wi-Fi Serial client (the package ESP Web Tools uses), then the text +// console. See README.md in this folder. +// +// bun improv-test.ts +import { ImprovSerial } from "improv-wifi-serial-sdk/dist/serial.js"; + +const [qemuPath, factoryPath, workDir] = Bun.argv.slice(2); +const PORT = 5599; +// Boots take a few seconds locally but much longer on shared CI runners. +const BOOT_TIMEOUT_MS = 180_000; + +let failures = 0; +function check(name: string, ok: boolean, detail?: unknown) { + console.log(`${ok ? "PASS" : "FAIL"} ${name}${ok || detail === undefined ? "" : ` -> ${JSON.stringify(detail)}`}`); + if (!ok) failures++; +} +const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms)); + +// --- 4 MB flash image ---------------------------------------------------------------------------- +const factory = new Uint8Array(await Bun.file(factoryPath).arrayBuffer()); +const flash = new Uint8Array(4 * 1024 * 1024).fill(0xff); +flash.set(factory, 0); +const flashPath = `${workDir}/flash-improv.bin`; +await Bun.write(flashPath, flash); + +// --- QEMU and a serial port shim (Web Serial-like readable/writable over the TCP socket) ---------- +let transcript = ""; +let controller: ReadableStreamDefaultController | null = null; +let qemu: ReturnType; +let socket: Awaited> | null = null; + +async function startQemu() { + qemu = Bun.spawn( + [ + qemuPath, + "-nographic", + "-machine", + "esp32", + "-drive", + `file=${flashPath},if=mtd,format=raw`, + "-serial", + `tcp:127.0.0.1:${PORT},server,wait=on`, + "-monitor", + "none", + // Like ESP-IDF's own QEMU runner: an emulated chip on a slow host would otherwise be reset by + // the timer-group watchdogs during boot, over and over. + "-global", + "driver=timer.esp32.timg,property=wdt_disable,value=true", + ], + { stdout: "ignore", stderr: "inherit" }, + ); + + socket = null; + for (let attempt = 0; attempt < 50 && !socket; attempt++) { + try { + socket = await Bun.connect({ + hostname: "127.0.0.1", + port: PORT, + socket: { + data(_s, data) { + const bytes = new Uint8Array(data); + transcript += new TextDecoder().decode(bytes); + try { + controller?.enqueue(bytes); + } catch { + controller = null; + } + }, + }, + }); + } catch { + await sleep(200); + } + } + if (!socket) throw new Error("QEMU serial port didn't open"); +} + +/** + * Restarts the emulated chip from power-on (the flash file keeps what was written). Used instead + * of the firmware's own restart: QEMU keeps peripheral state across a software restart that a real + * chip resets, and an interrupt left over from before (e.g. from serial input that is still + * arriving) crashes the next boot in the task watchdog's handler. + */ +async function powerCycle() { + socket?.end(); + qemu.kill(); + await qemu.exited; + transcript += "\n--- power cycle ---\n"; + await startQemu(); +} + +await startQemu(); + +const port = { + get readable() { + return new ReadableStream({ + start(c) { + controller = c; + }, + cancel() { + controller = null; + }, + }); + }, + writable: new WritableStream({ + write(chunk) { + socket!.write(chunk); + }, + }), +}; + +async function waitFor(text: string, timeoutMs: number, from = 0) { + const start = Date.now(); + while (Date.now() - start < timeoutMs) { + const index = transcript.indexOf(text, from); + if (index >= 0) return index; + await sleep(100); + } + return -1; +} + +/** + * Sends a console line, followed by empty lines (which the console ignores): QEMU's UART only hands + * received bytes to the firmware in blocks of its FIFO threshold, so a short line can otherwise + * wait for more input. It did on CI runners after a restart (and made the v2.0.0-beta.1 tag build + * fail). A real UART flushes on its idle timeout. + */ +function sendLine(line: string) { + socket!.write(`${line}\n${"\n".repeat(128)}`); +} + +async function command(line: string, expect: string, timeoutMs = 8000) { + const from = transcript.length; + sendLine(line); + return (await waitFor(expect, timeoutMs, from)) >= 0; +} + +/** Right after a restart the console may not run yet on slow runners: repeat the harmless command. */ +async function commandAfterRestart(line: string, expect: string) { + for (let attempt = 0; attempt < 3; attempt++) { + if (await command(line, expect)) return true; + } + return false; +} + +const logger = { log() {}, error() {}, debug() {} }; + +try { + // --- Boot ---------------------------------------------------------------------------------- + check("firmware boots to the setup prompt", (await waitFor("Waiting for Wi-Fi settings", BOOT_TIMEOUT_MS)) >= 0); + + // --- Improv with the official client ------------------------------------------------------ + const improv = new ImprovSerial(port as any, logger as any); + const info = await improv.initialize(10000); + check("Improv detected; state is Authorized (ready for Wi-Fi)", improv.state === 2, improv.state); + check( + "device info: firmware, version, chip, name", + info?.firmware === "NetIgnite Agent" && info.chipFamily === "ESP32" && /^NetIgnite-[0-9A-F]{6}$/.test(info.name) && info.version.startsWith("2.0.0"), + info, + ); + + const networks = await improv.scan(); + check("Wi-Fi scan returns the (empty) list", Array.isArray(networks) && networks.length === 0, networks); + + let provisionError: unknown = null; + try { + await improv.provision("TestNet", "secret123", 40000); + } catch (error) { + provisionError = error; + } + check("provisioning fails cleanly with 'unable to connect'", provisionError !== null && improv.error === 3, { + provisionError: String(provisionError), + error: improv.error, + }); + await sleep(500); // the state packet follows the error packet + check("state returns to Authorized after the failure", improv.state === 2, improv.state); + + const state = await (improv as any).getNetworkState?.(); + if (state) check("network state: offline, supports Wi-Fi", state.online === false && state.supportsWifi === true, state); + + await improv.close(); + + // --- Console -------------------------------------------------------------------------------- + check("help lists the commands", await command("help", "factory-reset confirm")); + check("status before setup", await command("status", "server: not set")); + check("unknown command is reported", await command("frobnicate", 'Unknown command "frobnicate"')); + check("invalid server URL is rejected", await command("server ftp://nope", "Invalid server URL")); + check("non-token is rejected", await command("enroll hunter2", "isn't an enrollment token")); + check( + "setup stores server and token", + await command("setup https://app.netignite.dev netignite_enroll_abc:def", "Saved. The agent enrolls with https://app.netignite.dev"), + ); + check("server with IPv6 literal and port", await command("server http://[fd00::1]:18232", "Server set to http://[fd00::1]:18232")); + check("wifi with a quoted SSID", await command('wifi "My Home Net" "pass word"', 'Connecting to "My Home Net"')); + check("invalid static IP is rejected", await command("ip static 10.0.0.300 10.0.0.1 255.255.255.0", "isn't an IPv4 address")); + check("name is saved", await command("name kitchen-esp", "Name saved")); + check("log level", await command("log debug", "Log level: debug")); + + // The transcript only holds what the device sent: the token must never appear in it. + check("enrollment token never printed", !transcript.includes("netignite_enroll_abc:def")); + + // --- Settings survive a restart --------------------------------------------------------------- + check("reboot command restarts", await command("reboot", "Restarting...")); + await powerCycle(); + const beforeReboot = transcript.length; + check("boots again with its settings", (await waitFor("I Device kitchen-esp", BOOT_TIMEOUT_MS, beforeReboot)) >= 0); + await waitFor("[", 5000, transcript.length); + await sleep(1500); + let statusFrom = transcript.length; + for (let attempt = 0; attempt < 3; attempt++) { + statusFrom = transcript.length; + sendLine("status"); + if ((await waitFor("partition:", 8000, statusFrom)) >= 0) break; + } + const status = transcript.slice(statusFrom); + check("server URL persisted", status.includes("server: http://[fd00::1]:18232"), status); + check("device name persisted", status.includes("device: kitchen-esp"), status); + check("waiting to enroll with the stored token", status.includes("agent: waiting to enroll"), status); + + const improv2 = new ImprovSerial(port as any, logger as any); + const info2 = await improv2.initialize(10000); + check("Improv device name follows the console name", info2?.name === "kitchen-esp", info2); + await improv2.close(); + + check("factory reset erases everything", await command("factory-reset confirm", "All settings erased")); + await powerCycle(); + const afterReset = transcript.length; + await waitFor("I Device NetIgnite-", BOOT_TIMEOUT_MS, afterReset); + await sleep(1500); + check("after the reset the agent is unconfigured", await commandAfterRestart("status", "server: not set")); +} catch (error) { + check("test ran to the end", false, String(error)); +} finally { + await Bun.write(`${workDir}/improv-transcript.log`, transcript); + socket?.end(); + qemu.kill(); +} + +if (failures > 0) { + // The device's serial output, so a CI log shows where it got stuck. + const printable = transcript.replace(/[^\x09\x0a\x20-\x7e]/g, ""); + console.log("\n--- last 80 lines of serial output ---"); + console.log(printable.split("\n").slice(-80).join("\n")); +} + +console.log(failures === 0 ? "\nALL EMULATOR CHECKS PASSED" : `\n${failures} EMULATOR CHECK(S) FAILED`); +process.exit(failures === 0 ? 0 : 1); diff --git a/test/emulator/keys/README.md b/test/emulator/keys/README.md new file mode 100644 index 0000000..b9706a6 --- /dev/null +++ b/test/emulator/keys/README.md @@ -0,0 +1,9 @@ +# Test keys for the emulator test + +**Not secret, only for `esp32dev-qemu`.** The emulator firmware has `emulator-test.pub.pem` +compiled in (`custom_signing_key` in platformio.ini), so `improv-test.ts` can check that it +installs updates signed with `emulator-test.key.pem` and rejects unsigned ones and ones signed +with `emulator-other.key.pem`. + +`scripts/signing_key.py` refuses to build any other environment with a key from `test/`. Release +firmware uses `keys/firmware-signing.pub.pem` (see `scripts/generate_signing_key.sh`). diff --git a/test/emulator/keys/emulator-other.key.pem b/test/emulator/keys/emulator-other.key.pem new file mode 100644 index 0000000..a90683c --- /dev/null +++ b/test/emulator/keys/emulator-other.key.pem @@ -0,0 +1,5 @@ +-----BEGIN EC PRIVATE KEY----- +MHcCAQEEIOAUFb3myt3vI3gbFrSMSwsxzdTjM6hMQfBVui/Z6i7DoAoGCCqGSM49 +AwEHoUQDQgAEdQd8VFUySn5mSNhAuCamF62KxEWY0bVHd1Ex/QpKZZtHxZQ3+vAX +h6SF2ewrFKWMRP9KcQeT6cTgurYFBhJWrw== +-----END EC PRIVATE KEY----- diff --git a/test/emulator/keys/emulator-test.key.pem b/test/emulator/keys/emulator-test.key.pem new file mode 100644 index 0000000..020587b --- /dev/null +++ b/test/emulator/keys/emulator-test.key.pem @@ -0,0 +1,5 @@ +-----BEGIN EC PRIVATE KEY----- +MHcCAQEEIDCcEFW38hTtnUU1Vnec6HxlXZJiq7VLjI/g6yLmeNGkoAoGCCqGSM49 +AwEHoUQDQgAEwLFH03hAeAWSGiWeT/2rKeIisX1DKeh+rYjKADzuuLgUIGXjlQnB +zmxshkGOEi25o+yHHgG32dSDYa8zwx+lyA== +-----END EC PRIVATE KEY----- diff --git a/test/emulator/keys/emulator-test.pub.pem b/test/emulator/keys/emulator-test.pub.pem new file mode 100644 index 0000000..d35a071 --- /dev/null +++ b/test/emulator/keys/emulator-test.pub.pem @@ -0,0 +1,4 @@ +-----BEGIN PUBLIC KEY----- +MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEwLFH03hAeAWSGiWeT/2rKeIisX1D +Keh+rYjKADzuuLgUIGXjlQnBzmxshkGOEi25o+yHHgG32dSDYa8zwx+lyA== +-----END PUBLIC KEY----- diff --git a/test/emulator/ota-test.ts b/test/emulator/ota-test.ts new file mode 100644 index 0000000..6a55293 --- /dev/null +++ b/test/emulator/ota-test.ts @@ -0,0 +1,250 @@ +// Boots the emulator firmware (pio run -e esp32dev-qemu) in Espressif's QEMU and tests OTA +// updates without a network: the emulator build accepts an image over the serial port +// (`ota-begin`, `ota-data`, `ota-end`) and writes it with the same code as a download. Checks that +// unsigned, wrongly signed and corrupted images are rejected, that a signed image boots from the +// other slot on probation, and that it rolls back because it never reaches a server. +// +// bun ota-test.ts +import { createHash, createPrivateKey, sign } from "node:crypto"; + +const [qemuPath, factoryPath, appPath, workDir] = Bun.argv.slice(2); +const PORT = 5598; +const BOOT_TIMEOUT_MS = 180_000; +const KEYS = `${import.meta.dir}/keys`; + +let failures = 0; +function check(name: string, ok: boolean, detail?: unknown) { + console.log(`${ok ? "PASS" : "FAIL"} ${name}${ok || detail === undefined ? "" : ` -> ${JSON.stringify(detail)}`}`); + if (!ok) failures++; +} +const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms)); + +const testKey = createPrivateKey(await Bun.file(`${KEYS}/emulator-test.key.pem`).text()); +const otherKey = createPrivateKey(await Bun.file(`${KEYS}/emulator-other.key.pem`).text()); +const sha256 = (data: Uint8Array) => createHash("sha256").update(data).digest("hex"); +const signHex = (data: Uint8Array, key: ReturnType) => sign("sha256", data, key).toString("hex"); + +// --- 4 MB flash image ---------------------------------------------------------------------------- +const flash = new Uint8Array(4 * 1024 * 1024).fill(0xff); +flash.set(new Uint8Array(await Bun.file(factoryPath).arrayBuffer()), 0); +const flashPath = `${workDir}/flash-ota.bin`; +await Bun.write(flashPath, flash); + +// --- QEMU, and the serial port over TCP with backpressure for the large uploads --------------------- +let transcript = ""; +let drained: (() => void) | null = null; +let qemu: ReturnType; +let socket: Awaited> | null = null; + +async function startQemu() { + qemu = Bun.spawn( + [ + qemuPath, + "-nographic", + "-machine", + "esp32", + "-drive", + `file=${flashPath},if=mtd,format=raw`, + "-serial", + `tcp:127.0.0.1:${PORT},server,wait=on`, + "-monitor", + "none", + "-global", + "driver=timer.esp32.timg,property=wdt_disable,value=true", + ], + { stdout: "ignore", stderr: "inherit" }, + ); + + socket = null; + for (let attempt = 0; attempt < 50 && !socket; attempt++) { + try { + socket = await Bun.connect({ + hostname: "127.0.0.1", + port: PORT, + socket: { + data(_s, data) { + transcript += new TextDecoder().decode(data); + }, + drain() { + drained?.(); + }, + }, + }); + } catch { + await sleep(200); + } + } + if (!socket) throw new Error("QEMU serial port didn't open"); +} + +/** + * Restarts the emulated chip from power-on. After the bignum accelerator was used (the ECDSA check), + * a software restart crashes in QEMU (a stale interrupt hits the task watchdog's handler on the next + * boot); real chips restart fine. So the test power-cycles whenever the device restarts; the flash + * file keeps what was written. + */ +async function powerCycle(afterText: string, timeoutMs: number, from: number, settleMs = 500) { + const found = (await waitFor(afterText, timeoutMs, from)) >= 0; + await sleep(settleMs); + socket?.end(); + qemu.kill(); + await qemu.exited; + transcript += "\n--- power cycle ---\n"; + await startQemu(); + return found; +} + +await startQemu(); + +/** + * Writes to the serial port, followed by empty lines (which the console ignores): QEMU's UART only + * hands received bytes to the firmware in blocks of its FIFO threshold, so the end of a long line + * would otherwise wait for more input. A real UART flushes on its idle timeout. + */ +async function writeAll(text: string) { + let data = new TextEncoder().encode(`${text}${"\n".repeat(128)}`); + while (data.length > 0) { + const written = socket!.write(data); + data = data.subarray(Math.max(written, 0)); + if (data.length > 0) await new Promise((resolve) => (drained = resolve)); + } +} + +async function waitFor(text: string, timeoutMs: number, from = 0) { + const start = Date.now(); + while (Date.now() - start < timeoutMs) { + const index = transcript.indexOf(text, from); + if (index >= 0) return index; + await sleep(100); + } + return -1; +} + +async function command(line: string, expect: string, timeoutMs = 10_000) { + const from = transcript.length; + await writeAll(`${line}\n`); + return (await waitFor(expect, timeoutMs, from)) >= 0; +} + +async function status() { + const from = transcript.length; + await writeAll("status\n"); + await waitFor("updates:", 10_000, from); + return transcript.slice(from); +} + +/** Uploads an image; resolves with the device's answer to `ota-end`. */ +async function upload(image: Uint8Array, declaredSha: string, signature?: string) { + if (!(await command(`ota-begin ${image.length} ${declaredSha}${signature ? ` ${signature}` : ""}`, "OTA ready", 60_000))) { + return "begin failed"; + } + + // 200 bytes per line. The device acknowledges every 2000 bytes ("OTA ok "): the + // emulated UART has no flow control and drops what doesn't fit into its buffer. + const CHUNK = 200; + const ACK_EVERY = 2000; + for (let offset = 0; offset < image.length; offset += ACK_EVERY) { + const end = Math.min(offset + ACK_EVERY, image.length); + let batch = ""; + for (let i = offset; i < end; i += CHUNK) { + batch += `ota-data ${Buffer.from(image.subarray(i, Math.min(i + CHUNK, end))).toString("hex")}\n`; + } + const from = transcript.length; + await writeAll(batch); + if ((await waitFor(`OTA ok ${end}\n`, 30_000, from)) < 0) { + const error = transcript.slice(from).match(/OTA error: [^\r\n]*/); + return error ? error[0] : `no acknowledgement at ${end} bytes`; + } + } + + const from = transcript.length; + await writeAll("ota-end\n"); + const start = Date.now(); + while (Date.now() - start < 600_000) { + const answer = transcript.slice(from).match(/OTA (installed|error: [^\r\n]*)/); + if (answer) return answer[0]; + await sleep(200); + } + return "no answer"; +} + +try { + check("firmware boots", (await waitFor("Waiting for Wi-Fi settings", BOOT_TIMEOUT_MS)) >= 0); + await sleep(1500); + const before = await status(); + check("runs from app0 and only accepts signed updates", before.includes("partition: app0") && before.includes("updates: signed only"), before); + + // A small fake image (it starts with the ESP image magic byte, which Update checks). + const fake = new Uint8Array(8192).map((_, i) => (i * 7) & 0xff); + fake[0] = 0xe9; + + check( + "unsigned image is refused", + await command(`ota-begin ${fake.length} ${sha256(fake)}`, "OTA error: This firmware only installs signed updates"), + ); + const wrongKey = await upload(fake, sha256(fake), signHex(fake, otherKey)); + check("image signed with another key is rejected", wrongKey === "OTA error: The update's signature is invalid", wrongKey); + + const corrupted = fake.slice(); + corrupted[100] ^= 0xff; + const corruptedResult = await upload(corrupted, sha256(fake), signHex(fake, testKey)); + check( + "corrupted image is rejected by its SHA-256", + corruptedResult === "OTA error: The download doesn't match its SHA-256", + corruptedResult, + ); + const stillOld = await status(); + check("rejected updates leave the firmware untouched", stillOld.includes("partition: app0") && !stillOld.includes("probation"), stillOld); + + // --- A real, signed update --------------------------------------------------------------------- + const app = new Uint8Array(await Bun.file(appPath).arrayBuffer()); + const started = Date.now(); + const result = await upload(app, sha256(app), signHex(app, testKey)); + check(`signed image (${app.length} bytes) is installed`, result === "OTA installed", result); + console.log(` upload took ${Math.round((Date.now() - started) / 1000)} s`); + + // Power-cycle instead of the device's own restart (it has written everything by now; a warm + // restart would crash in QEMU and the bootloader would roll the update back). + await powerCycle("OTA installed", 1000, transcript.lastIndexOf("OTA installed"), 20); + const bootFrom = transcript.length; + check("restarts into the new slot", (await waitFor("partition app1", BOOT_TIMEOUT_MS, bootFrom)) >= 0); + check("new firmware is on probation", (await waitFor("New firmware on probation", 20_000, bootFrom)) >= 0); + await sleep(1500); + const probation = await status(); + check("status shows the probation", probation.includes("partition: app1") && probation.includes("on probation"), probation); + + // The emulator can't reach a server, so the probation (20 s in emulator builds) runs out. + const probationFrom = transcript.length; + const timedOut = await waitFor("didn't reach the server in time", 60_000, probationFrom); + check("rolls back when it doesn't reach the server", timedOut >= 0); + await powerCycle("rst:0xc", 30_000, Math.max(timedOut, probationFrom)); + const rollbackFrom = transcript.length; + check("restarts from the previous slot", (await waitFor("partition app0", BOOT_TIMEOUT_MS, rollbackFrom)) >= 0); + check("reports the failed update", (await waitFor("The update to emulator failed", 20_000, rollbackFrom)) >= 0); + await sleep(1500); + const after = await status(); + check( + "status after the rollback", + after.includes("partition: app0") && after.includes("the update to emulator failed") && !after.includes("probation"), + after, + ); +} catch (error) { + check("test ran to the end", false, String(error)); +} finally { + await Bun.write(`${workDir}/ota-transcript.log`, transcript); + socket?.end(); + qemu.kill(); +} + +if (failures > 0) { + const printable = transcript + .split("\n") + .filter((line) => !line.startsWith("OTA ok")) + .join("\n") + .replace(/[^\x09\x0a\x20-\x7e]/g, ""); + console.log("\n--- last 80 lines of serial output ---"); + console.log(printable.split("\n").slice(-80).join("\n")); +} + +console.log(failures === 0 ? "\nALL OTA CHECKS PASSED" : `\n${failures} OTA CHECK(S) FAILED`); +process.exit(failures === 0 ? 0 : 1); diff --git a/test/emulator/package.json b/test/emulator/package.json new file mode 100644 index 0000000..b5c1c4b --- /dev/null +++ b/test/emulator/package.json @@ -0,0 +1,11 @@ +{ + "name": "netignite-agent-emulator-test", + "private": true, + "type": "module", + "scripts": { + "test": "bun improv-test.ts" + }, + "dependencies": { + "improv-wifi-serial-sdk": "2.8.1" + } +} diff --git a/test/test_core/test_core.cpp b/test/test_core/test_core.cpp new file mode 100644 index 0000000..ec14daf --- /dev/null +++ b/test/test_core/test_core.cpp @@ -0,0 +1,683 @@ +// Host-side unit tests for lib/netignite_core: pio test -e native +#include + +#include +#include +#include + +#include "netignite/CommandLine.h" +#include "netignite/Hex.h" +#include "netignite/Improv.h" +#include "netignite/LogRing.h" +#include "netignite/Mac.h" +#include "netignite/MagicPacket.h" +#include "netignite/Protocol.h" +#include "netignite/SerialDemux.h" +#include "netignite/ServerUrl.h" +#include "netignite/Validation.h" + +using namespace netignite; + +void setUp() {} +void tearDown() {} + +// --- MAC addresses ----------------------------------------------------------------------------- + +void test_parse_mac_formats() { + MacAddress mac{}; + const MacAddress expected = {0xaa, 0xbb, 0xcc, 0x01, 0x02, 0xff}; + + TEST_ASSERT_TRUE(parseMac("AA:BB:CC:01:02:FF", mac)); + TEST_ASSERT_EQUAL_UINT8_ARRAY(expected.data(), mac.data(), 6); + TEST_ASSERT_TRUE(parseMac("aa-bb-cc-01-02-ff", mac)); + TEST_ASSERT_EQUAL_UINT8_ARRAY(expected.data(), mac.data(), 6); + TEST_ASSERT_TRUE(parseMac("aabbcc0102FF", mac)); + TEST_ASSERT_EQUAL_UINT8_ARRAY(expected.data(), mac.data(), 6); +} + +void test_parse_mac_rejects_invalid() { + MacAddress mac{}; + TEST_ASSERT_FALSE(parseMac("", mac)); + TEST_ASSERT_FALSE(parseMac("AA:BB:CC:DD:EE", mac)); + TEST_ASSERT_FALSE(parseMac("AA:BB:CC:DD:EE:GG", mac)); + TEST_ASSERT_FALSE(parseMac("AA:BB-CC:DD:EE:FF", mac)); + TEST_ASSERT_FALSE(parseMac("AABB.CCDD.EEFF", mac)); + TEST_ASSERT_FALSE(parseMac("AA:BB:CC:DD:EE:FF:", mac)); +} + +void test_format_mac() { + TEST_ASSERT_EQUAL_STRING("0A:1B:2C:3D:4E:5F", formatMac({0x0a, 0x1b, 0x2c, 0x3d, 0x4e, 0x5f}).c_str()); +} + +void test_link_local_from_mac() { + // 52:74:f2:b1:a8:7f -> fe80::5074:f2ff:feb1:a87f (RFC 4291 modified EUI-64) + const IPv6Bytes address = linkLocalFromMac({0x52, 0x74, 0xf2, 0xb1, 0xa8, 0x7f}); + TEST_ASSERT_EQUAL_STRING("fe80::5074:f2ff:feb1:a87f", formatIPv6(address).c_str()); +} + +void test_format_ipv6_compression() { + IPv6Bytes loopback{}; + loopback[15] = 1; + TEST_ASSERT_EQUAL_STRING("::1", formatIPv6(loopback).c_str()); + + const IPv6Bytes address = {0x20, 0x01, 0x0d, 0xb8, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1}; + TEST_ASSERT_EQUAL_STRING("2001:db8::1:0:0:1", formatIPv6(address).c_str()); +} + +// --- Server URLs ------------------------------------------------------------------------------- + +void test_server_url_https_default() { + auto url = parseServerUrl("https://app.netignite.dev"); + TEST_ASSERT_TRUE(url.has_value()); + TEST_ASSERT_TRUE(url->secure); + TEST_ASSERT_EQUAL_STRING("app.netignite.dev", url->host.c_str()); + TEST_ASSERT_EQUAL_UINT16(443, url->port); + TEST_ASSERT_EQUAL_STRING("https://app.netignite.dev/api/v1/agents/enroll", + url->url("/api/v1/agents/enroll").c_str()); + TEST_ASSERT_EQUAL_STRING("/ws/agent", url->path("/ws/agent").c_str()); +} + +void test_server_url_http_port_and_path() { + auto url = parseServerUrl(" HTTP://192.168.1.10:18232/netignite/ "); + TEST_ASSERT_TRUE(url.has_value()); + TEST_ASSERT_FALSE(url->secure); + TEST_ASSERT_EQUAL_STRING("192.168.1.10", url->host.c_str()); + TEST_ASSERT_EQUAL_UINT16(18232, url->port); + TEST_ASSERT_EQUAL_STRING("/netignite/ws/agent", url->path("/ws/agent").c_str()); + TEST_ASSERT_EQUAL_STRING("http://192.168.1.10:18232", url->origin().c_str()); +} + +void test_server_url_without_scheme_and_ipv6() { + auto url = parseServerUrl("netignite.example.com"); + TEST_ASSERT_TRUE(url.has_value()); + TEST_ASSERT_TRUE(url->secure); + + auto v6 = parseServerUrl("http://[fd00::10]:18232"); + TEST_ASSERT_TRUE(v6.has_value()); + TEST_ASSERT_EQUAL_STRING("fd00::10", v6->host.c_str()); + TEST_ASSERT_EQUAL_UINT16(18232, v6->port); + TEST_ASSERT_EQUAL_STRING("http://[fd00::10]:18232", v6->origin().c_str()); +} + +void test_server_url_rejects_invalid() { + TEST_ASSERT_FALSE(parseServerUrl("").has_value()); + TEST_ASSERT_FALSE(parseServerUrl("ftp://example.com").has_value()); + TEST_ASSERT_FALSE(parseServerUrl("https://").has_value()); + TEST_ASSERT_FALSE(parseServerUrl("https://example.com:").has_value()); + TEST_ASSERT_FALSE(parseServerUrl("https://example.com:0").has_value()); + TEST_ASSERT_FALSE(parseServerUrl("https://example.com:70000").has_value()); + TEST_ASSERT_FALSE(parseServerUrl("https://user@example.com").has_value()); + TEST_ASSERT_FALSE(parseServerUrl("https://example.com/?x=1").has_value()); + TEST_ASSERT_FALSE(parseServerUrl("https://exa mple.com").has_value()); + TEST_ASSERT_FALSE(parseServerUrl("http://[::1]:").has_value()); +} + +// --- Wake-on-LAN -------------------------------------------------------------------------------- + +void test_magic_packet() { + const MacAddress mac = {1, 2, 3, 4, 5, 6}; + const auto packet = buildMagicPacket(mac); + for (int i = 0; i < 6; i++) TEST_ASSERT_EQUAL_HEX8(0xff, packet[i]); + for (int repeat = 0; repeat < 16; repeat++) { + TEST_ASSERT_EQUAL_UINT8_ARRAY(mac.data(), packet.data() + 6 + repeat * 6, 6); + } +} + +void test_directed_broadcast() { + // 192.168.1.32/24 -> 192.168.1.255 + TEST_ASSERT_EQUAL_HEX32(0xc0a801ff, directedBroadcast(0xc0a80120, 0xffffff00)); + // 10.1.2.3/16 -> 10.1.255.255 + TEST_ASSERT_EQUAL_HEX32(0x0a01ffff, directedBroadcast(0x0a010203, 0xffff0000)); +} + +// --- Improv ------------------------------------------------------------------------------------- + +uint8_t checksum(const std::vector& bytes, size_t count) { + uint8_t sum = 0; + for (size_t i = 0; i < count; i++) sum = static_cast(sum + bytes[i]); + return sum; +} + +void test_improv_state_packet() { + const auto packet = improv::buildStatePacket(improv::State::Authorized); + const std::vector expected = {'I', 'M', 'P', 'R', 'O', 'V', 1, 0x01, 1, 0x02}; + TEST_ASSERT_EQUAL_size_t(expected.size() + 2, packet.size()); + TEST_ASSERT_EQUAL_UINT8_ARRAY(expected.data(), packet.data(), expected.size()); + TEST_ASSERT_EQUAL_HEX8(checksum(packet, expected.size()), packet[expected.size()]); + TEST_ASSERT_EQUAL_HEX8('\n', packet.back()); +} + +void test_improv_rpc_result() { + const auto packet = + improv::buildRpcResult(improv::Command::GetDeviceInfo, {"NetIgnite", "2.0.0", "ESP32", "Office"}); + // data = cmd, total length, then length-prefixed strings + TEST_ASSERT_EQUAL_HEX8(0x04, packet[7]); + const size_t dataLength = packet[8]; + TEST_ASSERT_EQUAL_size_t(2 + 4 + 9 + 5 + 5 + 6, dataLength); + TEST_ASSERT_EQUAL_HEX8(0x03, packet[9]); + TEST_ASSERT_EQUAL_size_t(dataLength - 2, packet[10]); + TEST_ASSERT_EQUAL_size_t(9, packet[11]); + TEST_ASSERT_EQUAL_MEMORY("NetIgnite", packet.data() + 12, 9); + TEST_ASSERT_EQUAL_HEX8(checksum(packet, 9 + dataLength), packet[9 + dataLength]); +} + +void test_improv_parse_wifi_settings() { + // Example from the spec: SSID "MyWirelessAP", password "mysecurepassword" + std::vector data = {0x01, 0x1e, 0x0c}; + for (char c : std::string("MyWirelessAP")) data.push_back(static_cast(c)); + data.push_back(0x10); + for (char c : std::string("mysecurepassword")) data.push_back(static_cast(c)); + + improv::RpcCommand command; + TEST_ASSERT_TRUE(improv::parseRpc(data, command)); + TEST_ASSERT_EQUAL(static_cast(improv::Command::WifiSettings), static_cast(command.command)); + TEST_ASSERT_EQUAL_STRING("MyWirelessAP", command.ssid.c_str()); + TEST_ASSERT_EQUAL_STRING("mysecurepassword", command.password.c_str()); +} + +void test_improv_parse_rejects_bad_lengths() { + improv::RpcCommand command; + TEST_ASSERT_FALSE(improv::parseRpc({0x01}, command)); + TEST_ASSERT_FALSE(improv::parseRpc({0x01, 0x05, 0x01, 'a'}, command)); + // SSID length runs past the end + TEST_ASSERT_FALSE(improv::parseRpc({0x01, 0x03, 0x09, 'a', 'b'}, command)); + // open network: empty password is fine, empty SSID isn't + TEST_ASSERT_TRUE(improv::parseRpc({0x01, 0x03, 0x01, 'a', 0x00}, command)); + TEST_ASSERT_EQUAL_STRING("", command.password.c_str()); + TEST_ASSERT_FALSE(improv::parseRpc({0x01, 0x02, 0x00, 0x00}, command)); + // other commands carry no payload + TEST_ASSERT_TRUE(improv::parseRpc({0x03, 0x00}, command)); + TEST_ASSERT_EQUAL(static_cast(improv::Command::GetDeviceInfo), static_cast(command.command)); +} + +// --- Serial demultiplexer ---------------------------------------------------------------------- + +std::vector feedAll(SerialDemux& demux, const std::vector& bytes) { + std::vector events; + for (uint8_t byte : bytes) { + auto event = demux.feed(byte); + if (event.type != SerialDemux::Event::Type::None) events.push_back(event); + } + return events; +} + +std::vector bytesOf(const std::string& text) { + return std::vector(text.begin(), text.end()); +} + +void test_demux_text_lines() { + SerialDemux demux; + auto events = feedAll(demux, bytesOf("status\r\n wifi \"My Net\" pw \n\n")); + TEST_ASSERT_EQUAL_size_t(2, events.size()); + TEST_ASSERT_EQUAL_STRING("status", events[0].line.c_str()); + TEST_ASSERT_EQUAL_STRING("wifi \"My Net\" pw", events[1].line.c_str()); +} + +void test_demux_improv_packet_with_newline_in_data() { + // An RPC packet whose payload contains 0x0A must not be split at it. + const std::vector rpc = {0x01, 0x05, 0x01, '\n', 0x02, 'p', 'w'}; + auto packet = improv::buildPacket(improv::PacketType::Rpc, rpc); + + SerialDemux demux; + std::vector input = bytesOf("IMPROVE is not a packet\n"); + input.insert(input.end(), packet.begin(), packet.end()); + auto line = bytesOf("help\n"); + input.insert(input.end(), line.begin(), line.end()); + + auto events = feedAll(demux, input); + TEST_ASSERT_EQUAL_size_t(3, events.size()); + TEST_ASSERT_EQUAL(static_cast(SerialDemux::Event::Type::Line), static_cast(events[0].type)); + TEST_ASSERT_EQUAL(static_cast(SerialDemux::Event::Type::ImprovPacket), static_cast(events[1].type)); + TEST_ASSERT_EQUAL_HEX8(0x03, events[1].improvType); + TEST_ASSERT_EQUAL_size_t(rpc.size(), events[1].improvData.size()); + TEST_ASSERT_EQUAL_UINT8_ARRAY(rpc.data(), events[1].improvData.data(), rpc.size()); + TEST_ASSERT_EQUAL_STRING("help", events[2].line.c_str()); + + improv::RpcCommand command; + TEST_ASSERT_TRUE(improv::parseRpc(events[1].improvData, command)); + TEST_ASSERT_EQUAL_STRING("\n", command.ssid.c_str()); +} + +void test_demux_bad_checksum() { + auto packet = improv::buildPacket(improv::PacketType::Rpc, {0x02, 0x00}); + packet[packet.size() - 2] ^= 0xff; + SerialDemux demux; + auto events = feedAll(demux, packet); + TEST_ASSERT_EQUAL_size_t(1, events.size()); + TEST_ASSERT_EQUAL(static_cast(SerialDemux::Event::Type::ImprovBadChecksum), + static_cast(events[0].type)); +} + +void test_demux_overlong_line_is_dropped() { + SerialDemux demux; + std::string longLine(SerialDemux::MAX_LINE + 10, 'x'); + auto events = feedAll(demux, bytesOf(longLine + "\nreboot\n")); + TEST_ASSERT_EQUAL_size_t(1, events.size()); + TEST_ASSERT_EQUAL_STRING("reboot", events[0].line.c_str()); +} + +// --- Protocol ---------------------------------------------------------------------------------- + +void test_parse_request_frame() { + JsonDocument doc; + protocol::Frame frame; + std::string error; + TEST_ASSERT_TRUE(protocol::parseFrame( + R"({"t":"req","id":4294967295,"cmd":"WAKE","p":{"mac":"AA:BB:CC:DD:EE:FF","port":9}})", doc, frame, + error)); + TEST_ASSERT_EQUAL(static_cast(protocol::FrameType::Request), static_cast(frame.type)); + TEST_ASSERT_EQUAL_UINT32(4294967295u, frame.id); + TEST_ASSERT_EQUAL_STRING("WAKE", frame.cmd.c_str()); + + protocol::WakeRequest wake; + TEST_ASSERT_TRUE(protocol::parseWakeRequest(frame.payload, wake, error)); + TEST_ASSERT_EQUAL_UINT16(9, wake.port); + TEST_ASSERT_EQUAL_HEX8(0xff, wake.mac[5]); +} + +void test_parse_frame_rejects_invalid() { + JsonDocument doc; + protocol::Frame frame; + std::string error; + TEST_ASSERT_FALSE(protocol::parseFrame("not json", doc, frame, error)); + TEST_ASSERT_FALSE(protocol::parseFrame("[1,2]", doc, frame, error)); + TEST_ASSERT_FALSE(protocol::parseFrame(R"({"t":"req","cmd":"PING"})", doc, frame, error)); + TEST_ASSERT_FALSE(protocol::parseFrame(R"({"t":"req","id":-1,"cmd":"PING"})", doc, frame, error)); + TEST_ASSERT_FALSE(protocol::parseFrame(R"({"t":"req","id":1})", doc, frame, error)); + TEST_ASSERT_FALSE(protocol::parseFrame(R"({"t":"xyz","id":1,"cmd":"PING"})", doc, frame, error)); +} + +void test_parse_wake_rejects_invalid() { + JsonDocument doc; + protocol::Frame frame; + std::string error; + protocol::WakeRequest wake; + protocol::parseFrame(R"({"t":"req","id":1,"cmd":"WAKE","p":{"mac":"nope","port":9}})", doc, frame, error); + TEST_ASSERT_FALSE(protocol::parseWakeRequest(frame.payload, wake, error)); + protocol::parseFrame(R"({"t":"req","id":1,"cmd":"WAKE","p":{"mac":"AA:BB:CC:DD:EE:FF","port":0}})", doc, + frame, error); + TEST_ASSERT_FALSE(protocol::parseWakeRequest(frame.payload, wake, error)); + protocol::parseFrame(R"({"t":"req","id":1,"cmd":"WAKE","p":{"mac":"AA:BB:CC:DD:EE:FF","port":"9"}})", doc, + frame, error); + TEST_ASSERT_FALSE(protocol::parseWakeRequest(frame.payload, wake, error)); +} + +void test_parse_status_requests() { + JsonDocument doc; + protocol::Frame frame; + std::string error; + protocol::StatusRequest status; + + protocol::parseFrame( + R"({"t":"req","id":2,"cmd":"STATUS","p":{"target":{"type":"link_local","mac":"52:74:F2:B1:A8:7F"},"icmp":true}})", + doc, frame, error); + TEST_ASSERT_TRUE(protocol::parseStatusRequest(frame.payload, status, error)); + TEST_ASSERT_EQUAL(static_cast(protocol::StatusRequest::Target::LinkLocal), static_cast(status.target)); + TEST_ASSERT_EQUAL_UINT16(0, status.tcpPort); + + protocol::parseFrame( + R"({"t":"req","id":3,"cmd":"STATUS","p":{"target":{"type":"ip","address":"fe80::1%eth0"},"icmp":true,"tcp_port":445}})", + doc, frame, error); + TEST_ASSERT_TRUE(protocol::parseStatusRequest(frame.payload, status, error)); + TEST_ASSERT_EQUAL_STRING("fe80::1", status.address.c_str()); + TEST_ASSERT_EQUAL_UINT16(445, status.tcpPort); + + protocol::parseFrame( + R"({"t":"req","id":4,"cmd":"STATUS","p":{"target":{"type":"host","host":"nas.local"},"icmp":false,"tcp_port":22}})", + doc, frame, error); + TEST_ASSERT_TRUE(protocol::parseStatusRequest(frame.payload, status, error)); + TEST_ASSERT_EQUAL(static_cast(protocol::StatusRequest::Target::Host), static_cast(status.target)); + TEST_ASSERT_FALSE(status.icmp); + + protocol::parseFrame( + R"({"t":"req","id":5,"cmd":"STATUS","p":{"target":{"type":"ip","address":"10.0.0.1"},"icmp":false}})", doc, + frame, error); + TEST_ASSERT_FALSE(protocol::parseStatusRequest(frame.payload, status, error)); + protocol::parseFrame(R"({"t":"req","id":6,"cmd":"STATUS","p":{"target":{"type":"carrier-pigeon"}}})", doc, + frame, error); + TEST_ASSERT_FALSE(protocol::parseStatusRequest(frame.payload, status, error)); +} + +void test_encode_frames() { + JsonDocument payload; + payload["online"] = true; + TEST_ASSERT_EQUAL_STRING(R"({"t":"res","id":7,"ok":true,"p":{"online":true}})", + protocol::encodeResponse(7, payload.as()).c_str()); + TEST_ASSERT_EQUAL_STRING(R"({"t":"res","id":8,"ok":true,"p":{}})", protocol::encodeEmptyResponse(8).c_str()); + TEST_ASSERT_EQUAL_STRING(R"({"t":"res","id":9,"ok":false,"err":"Unknown command"})", + protocol::encodeError(9, "Unknown command").c_str()); + + JsonDocument hello; + hello["protocol"] = protocol::VERSION; + hello["kind"] = protocol::AGENT_KIND; + TEST_ASSERT_EQUAL_STRING(R"({"t":"evt","cmd":"HELLO","p":{"protocol":2,"kind":"esp32"}})", + protocol::encodeEvent("HELLO", hello.as()).c_str()); +} + +// --- Console command lines --------------------------------------------------------------------- + +void test_split_command_line() { + std::vector args; + TEST_ASSERT_TRUE(splitCommandLine(R"(wifi "My Home Network" "pa\"ss word")", args)); + TEST_ASSERT_EQUAL_size_t(3, args.size()); + TEST_ASSERT_EQUAL_STRING("wifi", args[0].c_str()); + TEST_ASSERT_EQUAL_STRING("My Home Network", args[1].c_str()); + TEST_ASSERT_EQUAL_STRING("pa\"ss word", args[2].c_str()); + + TEST_ASSERT_TRUE(splitCommandLine(" ip static 192.168.1.5\t192.168.1.1 255.255.255.0 ", args)); + TEST_ASSERT_EQUAL_size_t(5, args.size()); + TEST_ASSERT_EQUAL_STRING("255.255.255.0", args[4].c_str()); + + // An empty quoted argument (open network) is kept. + TEST_ASSERT_TRUE(splitCommandLine(R"(wifi Guest "")", args)); + TEST_ASSERT_EQUAL_size_t(3, args.size()); + TEST_ASSERT_EQUAL_STRING("", args[2].c_str()); + + TEST_ASSERT_FALSE(splitCommandLine(R"(wifi "unterminated)", args)); +} + +void test_hex() { + const uint8_t bytes[] = {0x00, 0x0f, 0xa5, 0xff}; + TEST_ASSERT_EQUAL_STRING("000fa5ff", toHex(bytes, sizeof(bytes)).c_str()); + + std::vector decoded; + TEST_ASSERT_TRUE(fromHex("000FA5ff", decoded)); + TEST_ASSERT_EQUAL_size_t(4, decoded.size()); + TEST_ASSERT_EQUAL_UINT8_ARRAY(bytes, decoded.data(), 4); + TEST_ASSERT_TRUE(fromHex("", decoded)); + TEST_ASSERT_EQUAL_size_t(0, decoded.size()); + TEST_ASSERT_FALSE(fromHex("abc", decoded)); + TEST_ASSERT_FALSE(fromHex("zz", decoded)); + TEST_ASSERT_EQUAL_size_t(0, decoded.size()); +} + +// --- Validation -------------------------------------------------------------------------------- + +void test_validation() { + TEST_ASSERT_TRUE(isValidHostname("Garage-ESP32")); + TEST_ASSERT_TRUE(isValidHostname("a")); + TEST_ASSERT_FALSE(isValidHostname("")); + TEST_ASSERT_FALSE(isValidHostname("-a")); + TEST_ASSERT_FALSE(isValidHostname("a-")); + TEST_ASSERT_FALSE(isValidHostname("Garage ESP32")); + TEST_ASSERT_FALSE(isValidHostname(std::string(64, 'a'))); + + TEST_ASSERT_TRUE(isValidIpv4("192.168.1.5")); + TEST_ASSERT_TRUE(isValidIpv4("0.0.0.0")); + TEST_ASSERT_TRUE(isValidIpv4("255.255.255.255")); + TEST_ASSERT_FALSE(isValidIpv4("256.1.1.1")); + TEST_ASSERT_FALSE(isValidIpv4("1.2.3")); + TEST_ASSERT_FALSE(isValidIpv4("1.2.3.4.5")); + TEST_ASSERT_FALSE(isValidIpv4("01.2.3.4")); + TEST_ASSERT_FALSE(isValidIpv4("1..3.4")); + TEST_ASSERT_FALSE(isValidIpv4("1.2.3.4.")); + TEST_ASSERT_FALSE(isValidIpv4("a.b.c.d")); + TEST_ASSERT_FALSE(isValidIpv4("")); +} + +// --- Log ring ---------------------------------------------------------------------------------- + +void test_log_ring_reads_from_a_cursor() { + LogRing ring(1000); + ring.push("one"); + ring.push("two"); + ring.push("three"); + TEST_ASSERT_EQUAL_UINT32(3, ring.nextSeq()); + + std::vector lines; + auto result = ring.read(1, 1000, lines); + TEST_ASSERT_EQUAL_size_t(2, lines.size()); + TEST_ASSERT_EQUAL_STRING("two", lines[0].c_str()); + TEST_ASSERT_EQUAL_UINT32(3, result.next); + TEST_ASSERT_EQUAL_UINT32(0, result.dropped); + + // Nothing new: empty read, cursor stays. + lines.clear(); + result = ring.read(3, 1000, lines); + TEST_ASSERT_EQUAL_size_t(0, lines.size()); + TEST_ASSERT_EQUAL_UINT32(3, result.next); +} + +void test_log_ring_drops_old_lines_and_reports_them() { + LogRing ring(10); + ring.push("aaaa"); // seq 0 + ring.push("bbbb"); // seq 1 + ring.push("cccc"); // seq 2: drops seq 0 + TEST_ASSERT_EQUAL_size_t(8, ring.bytes()); + + std::vector lines; + const auto result = ring.read(0, 1000, lines); + TEST_ASSERT_EQUAL_UINT32(1, result.dropped); + TEST_ASSERT_EQUAL_size_t(2, lines.size()); + TEST_ASSERT_EQUAL_STRING("bbbb", lines[0].c_str()); + TEST_ASSERT_EQUAL_UINT32(3, result.next); +} + +void test_log_ring_limits_bytes_and_line_length() { + LogRing ring(10000); + ring.push(std::string(500, 'x')); + TEST_ASSERT_EQUAL_size_t(LogRing::MAX_LINE, ring.bytes()); + ring.push("abc"); + ring.push("def"); + + std::vector lines; + auto result = ring.read(0, 245, lines); + // The first line is always returned, even if it alone exceeds the budget. + TEST_ASSERT_EQUAL_size_t(2, lines.size()); + TEST_ASSERT_EQUAL_UINT32(2, result.next); + lines.clear(); + result = ring.read(result.next, 245, lines); + TEST_ASSERT_EQUAL_size_t(1, lines.size()); + TEST_ASSERT_EQUAL_STRING("def", lines[0].c_str()); + + // A cursor from the future (after a reboot) reads from the end. + lines.clear(); + result = ring.read(1000, 245, lines); + TEST_ASSERT_EQUAL_size_t(0, lines.size()); + TEST_ASSERT_EQUAL_UINT32(3, result.next); +} + +// --- OTA, config and log requests -------------------------------------------------------------- + +static const char* const SHA = + "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"; + +void test_parse_ota_request() { + JsonDocument doc; + protocol::Frame frame; + std::string error; + protocol::OtaRequest ota; + + std::string text = std::string(R"({"t":"req","id":1,"cmd":"OTA_BEGIN","p":{"version":"2.1.0-beta.1",)") + + R"("path":"/api/v1/firmware/2.1.0-beta.1/files/firmware.bin","size":1260048,"sha256":")" + SHA + + R"("}})"; + TEST_ASSERT_TRUE(protocol::parseFrame(text, doc, frame, error)); + TEST_ASSERT_TRUE(protocol::parseOtaRequest(frame.payload, ota, error)); + TEST_ASSERT_EQUAL_STRING("2.1.0-beta.1", ota.version.c_str()); + TEST_ASSERT_EQUAL_STRING("/api/v1/firmware/2.1.0-beta.1/files/firmware.bin", ota.path.c_str()); + TEST_ASSERT_EQUAL_UINT32(1260048, ota.size); + TEST_ASSERT_EQUAL_HEX8(0x01, ota.sha256[0]); + TEST_ASSERT_EQUAL_HEX8(0xef, ota.sha256[31]); + TEST_ASSERT_EQUAL_size_t(0, ota.signature.size()); +} + +void test_parse_ota_request_rejects_invalid() { + JsonDocument doc; + protocol::Frame frame; + std::string error; + protocol::OtaRequest ota; + + const std::string valid = std::string(R"("version":"2.1.0","size":1000,"sha256":")") + SHA + "\""; + auto parse = [&](const std::string& payload) { + protocol::parseFrame(R"({"t":"req","id":1,"cmd":"OTA_BEGIN","p":{)" + payload + "}}", doc, frame, error); + return protocol::parseOtaRequest(frame.payload, ota, error); + }; + + TEST_ASSERT_TRUE(parse(valid + R"(,"path":"/api/v1/firmware/2.1.0/files/firmware.bin")")); + TEST_ASSERT_EQUAL_size_t(0, ota.signature.size()); + TEST_ASSERT_TRUE(parse(valid + R"(,"path":"/f.bin","signature":")" + std::string(142, 'a') + "\"")); + TEST_ASSERT_EQUAL_size_t(71, ota.signature.size()); + + TEST_ASSERT_FALSE(parse(valid + R"(,"path":"https://evil.example/f.bin")")); + TEST_ASSERT_FALSE(parse(valid + R"(,"path":"//evil.example/f.bin")")); + TEST_ASSERT_FALSE(parse(valid + R"(,"path":"/api/../../f.bin")")); + TEST_ASSERT_FALSE(parse(valid + R"(,"path":"/f.bin?x=1")")); + TEST_ASSERT_FALSE(parse(valid + R"(,"path":"/f.bin","signature":"xyz")")); + TEST_ASSERT_FALSE(parse(std::string(R"("version":"2.1.0","size":2000000,"sha256":")") + SHA + R"(","path":"/f")")); + TEST_ASSERT_FALSE(parse(std::string(R"("version":"2.1.0","size":0,"sha256":")") + SHA + R"(","path":"/f")")); + TEST_ASSERT_FALSE(parse(R"("version":"2.1.0","size":10,"sha256":"abcd","path":"/f")")); + TEST_ASSERT_FALSE(parse(std::string(R"("version":"2 1","size":10,"sha256":")") + SHA + R"(","path":"/f")")); +} + +void test_parse_config_request() { + JsonDocument doc; + protocol::Frame frame; + std::string error; + protocol::ConfigRequest config; + auto parse = [&](const std::string& payload) { + protocol::parseFrame(R"({"t":"req","id":1,"cmd":"CONFIG_SET","p":)" + payload + "}", doc, frame, error); + return protocol::parseConfigRequest(frame.payload, config, error); + }; + + TEST_ASSERT_TRUE(parse(R"({"server_url":"https://netignite.example/","name":"Garage-ESP32","log_level":"debug"})")); + TEST_ASSERT_EQUAL_STRING("https://netignite.example", config.serverUrl->c_str()); + TEST_ASSERT_EQUAL_STRING("Garage-ESP32", config.name->c_str()); + TEST_ASSERT_EQUAL_STRING("debug", config.logLevel->c_str()); + TEST_ASSERT_FALSE(config.ip.has_value()); + + TEST_ASSERT_TRUE(parse(R"({"ip":{"mode":"static","address":"192.168.1.50","gateway":"192.168.1.1","netmask":"255.255.255.0","dns1":"1.1.1.1"}})")); + TEST_ASSERT_TRUE(config.ip->isStatic); + TEST_ASSERT_EQUAL_STRING("192.168.1.50", config.ip->address.c_str()); + TEST_ASSERT_EQUAL_STRING("1.1.1.1", config.ip->dns1.c_str()); + TEST_ASSERT_EQUAL_STRING("", config.ip->dns2.c_str()); + + TEST_ASSERT_TRUE(parse(R"({"ip":{"mode":"dhcp"}})")); + TEST_ASSERT_FALSE(config.ip->isStatic); + + TEST_ASSERT_FALSE(parse(R"({})")); + TEST_ASSERT_FALSE(parse(R"({"server_url":"ftp://x"})")); + TEST_ASSERT_FALSE(parse(R"({"name":"Garage ESP32"})")); + TEST_ASSERT_FALSE(parse(R"({"log_level":"verbose"})")); + TEST_ASSERT_FALSE(parse(R"({"ip":{"mode":"static","address":"192.168.1.50","gateway":"192.168.1.1"}})")); + TEST_ASSERT_FALSE(parse(R"({"ip":{"mode":"static","address":"192.168.1.500","gateway":"192.168.1.1","netmask":"255.255.255.0"}})")); + TEST_ASSERT_FALSE(parse(R"({"ip":{"mode":"magic"}})")); +} + +void test_parse_log_subscribe() { + JsonDocument doc; + protocol::Frame frame; + std::string error; + bool enabled = false; + protocol::parseFrame(R"({"t":"req","id":1,"cmd":"LOG_SUBSCRIBE","p":{"enabled":true}})", doc, frame, error); + TEST_ASSERT_TRUE(protocol::parseLogSubscribe(frame.payload, enabled, error)); + TEST_ASSERT_TRUE(enabled); + protocol::parseFrame(R"({"t":"req","id":1,"cmd":"LOG_SUBSCRIBE","p":{"enabled":"yes"}})", doc, frame, error); + TEST_ASSERT_FALSE(protocol::parseLogSubscribe(frame.payload, enabled, error)); +} + +// --- SSH --------------------------------------------------------------------------------------- + +void test_base64() { + const auto b64 = [](const char* text, bool pad) { + return toBase64(reinterpret_cast(text), strlen(text), pad); + }; + TEST_ASSERT_EQUAL_STRING("", b64("", true).c_str()); + TEST_ASSERT_EQUAL_STRING("Zg==", b64("f", true).c_str()); + TEST_ASSERT_EQUAL_STRING("Zm8=", b64("fo", true).c_str()); + TEST_ASSERT_EQUAL_STRING("Zm9v", b64("foo", true).c_str()); + TEST_ASSERT_EQUAL_STRING("Zm9vYmFy", b64("foobar", true).c_str()); + TEST_ASSERT_EQUAL_STRING("Zg", b64("f", false).c_str()); + TEST_ASSERT_EQUAL_STRING("Zm8", b64("fo", false).c_str()); + + const uint8_t zeros[32] = {}; + TEST_ASSERT_EQUAL_STRING(("SHA256:" + std::string(43, 'A')).c_str(), protocol::sshFingerprint(zeros).c_str()); +} + +void test_parse_ssh_requests() { + JsonDocument doc; + protocol::Frame frame; + std::string error; + protocol::SshRequest ssh; + const std::string key = "SHA256:" + std::string(43, 'b'); + auto parse = [&](const std::string& payload, bool exec) { + protocol::parseFrame(R"({"t":"req","id":1,"cmd":"SSH","p":)" + payload + "}", doc, frame, error); + return protocol::parseSshRequest(frame.payload, exec, ssh, error); + }; + + TEST_ASSERT_TRUE(parse(R"({"host":"192.168.1.20","port":22,"username":"admin","auth":{"type":"password","password":"pw"},"host_key":null})", false)); + TEST_ASSERT_EQUAL_STRING("192.168.1.20", ssh.host.c_str()); + TEST_ASSERT_EQUAL(static_cast(protocol::SshRequest::Auth::Password), static_cast(ssh.auth)); + TEST_ASSERT_EQUAL_STRING("pw", ssh.secret.c_str()); + TEST_ASSERT_TRUE(ssh.hostKey.empty()); + + const std::string exec = R"({"host":"nas.local","port":2222,"username":"DOMAIN\\admin","auth":{"type":"key","private_key":"-----BEGIN OPENSSH PRIVATE KEY-----\nabc\n"},"host_key":")" + + key + R"(","action":"reboot","os":"posix","sudo":true})"; + TEST_ASSERT_TRUE(parse(exec, true)); + TEST_ASSERT_EQUAL_UINT16(2222, ssh.port); + TEST_ASSERT_EQUAL_STRING("DOMAIN\\admin", ssh.username.c_str()); + TEST_ASSERT_EQUAL(static_cast(protocol::SshRequest::Auth::Key), static_cast(ssh.auth)); + TEST_ASSERT_EQUAL_STRING(key.c_str(), ssh.hostKey.c_str()); + TEST_ASSERT_EQUAL_STRING("sudo -n shutdown -r now", protocol::sshCommand(ssh).c_str()); + + ssh.sudo = false; + ssh.action = protocol::SshRequest::Action::Shutdown; + TEST_ASSERT_EQUAL_STRING("shutdown -h now", protocol::sshCommand(ssh).c_str()); + ssh.windows = true; + TEST_ASSERT_EQUAL_STRING("shutdown /s /t 0", protocol::sshCommand(ssh).c_str()); + ssh.action = protocol::SshRequest::Action::Reboot; + TEST_ASSERT_EQUAL_STRING("shutdown /r /t 0", protocol::sshCommand(ssh).c_str()); + + // SSH_EXEC needs a trusted host key, a known action and OS. + TEST_ASSERT_FALSE(parse(R"({"host":"h","port":22,"username":"a","auth":{"type":"password","password":"pw"},"host_key":null,"action":"reboot","os":"posix","sudo":false})", true)); + TEST_ASSERT_FALSE(parse(R"({"host":"h","port":22,"username":"a","auth":{"type":"password","password":"pw"},"host_key":")" + key + R"(","action":"rm -rf /","os":"posix","sudo":false})", true)); + TEST_ASSERT_FALSE(parse(R"({"host":"h","port":22,"username":"a","auth":{"type":"password","password":"pw"},"host_key":")" + key + R"(","action":"reboot","os":"amiga","sudo":false})", true)); + + TEST_ASSERT_FALSE(parse(R"({"host":"","port":22,"username":"a","auth":{"type":"password","password":"pw"}})", false)); + TEST_ASSERT_FALSE(parse(R"({"host":"a b","port":22,"username":"a","auth":{"type":"password","password":"pw"}})", false)); + TEST_ASSERT_FALSE(parse(R"({"host":"h","port":0,"username":"a","auth":{"type":"password","password":"pw"}})", false)); + TEST_ASSERT_FALSE(parse(R"({"host":"h","port":22,"username":"a b","auth":{"type":"password","password":"pw"}})", false)); + TEST_ASSERT_FALSE(parse(R"({"host":"h","port":22,"username":"a","auth":{"type":"cert","password":"pw"}})", false)); + TEST_ASSERT_FALSE(parse(R"({"host":"h","port":22,"username":"a","auth":{"type":"password","password":""}})", false)); + TEST_ASSERT_FALSE(parse(R"({"host":"h","port":22,"username":"a","auth":{"type":"password","password":"pw"},"host_key":"MD5:aa"})", false)); +} + +int main(int, char**) { + UNITY_BEGIN(); + RUN_TEST(test_parse_mac_formats); + RUN_TEST(test_parse_mac_rejects_invalid); + RUN_TEST(test_format_mac); + RUN_TEST(test_link_local_from_mac); + RUN_TEST(test_format_ipv6_compression); + RUN_TEST(test_server_url_https_default); + RUN_TEST(test_server_url_http_port_and_path); + RUN_TEST(test_server_url_without_scheme_and_ipv6); + RUN_TEST(test_server_url_rejects_invalid); + RUN_TEST(test_magic_packet); + RUN_TEST(test_directed_broadcast); + RUN_TEST(test_improv_state_packet); + RUN_TEST(test_improv_rpc_result); + RUN_TEST(test_improv_parse_wifi_settings); + RUN_TEST(test_improv_parse_rejects_bad_lengths); + RUN_TEST(test_demux_text_lines); + RUN_TEST(test_demux_improv_packet_with_newline_in_data); + RUN_TEST(test_demux_bad_checksum); + RUN_TEST(test_demux_overlong_line_is_dropped); + RUN_TEST(test_parse_request_frame); + RUN_TEST(test_parse_frame_rejects_invalid); + RUN_TEST(test_parse_wake_rejects_invalid); + RUN_TEST(test_parse_status_requests); + RUN_TEST(test_encode_frames); + RUN_TEST(test_split_command_line); + RUN_TEST(test_hex); + RUN_TEST(test_validation); + RUN_TEST(test_log_ring_reads_from_a_cursor); + RUN_TEST(test_log_ring_drops_old_lines_and_reports_them); + RUN_TEST(test_log_ring_limits_bytes_and_line_length); + RUN_TEST(test_parse_ota_request); + RUN_TEST(test_parse_ota_request_rejects_invalid); + RUN_TEST(test_parse_config_request); + RUN_TEST(test_parse_log_subscribe); + RUN_TEST(test_base64); + RUN_TEST(test_parse_ssh_requests); + return UNITY_END(); +}