diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e5f71cf..695bcc7 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -11,9 +11,14 @@ concurrency: cancel-in-progress: true env: + # Ref of gameap/gameap providing web/plugin-sdk (0.3.3+: contentType 'none', + # fileSize/fileMtime props) and the panel that streams HTTPResponse.file. + GAMEAP_REF: main # Ref of gameap/gameap-proto providing rust/gameap-plugin-sdk; pin to a SHA if - # the SDK drifts from Cargo.lock - GAMEAP_PROTO_REF: main + # the SDK drifts from Cargo.lock. Pinned to the head of the proto-updates + # branch: HTTPResponse.file / FileRef, which src/http.rs sets, is not on main + # yet. Restore `main` once proto-updates is merged. + GAMEAP_PROTO_REF: aba9a6c002358ed56691ff1f5c28d448e5f2cdee jobs: build: @@ -22,6 +27,16 @@ jobs: - uses: actions/checkout@v4 with: path: plugin-hex-editor + persist-credentials: false + + # The frontend depends on file:../../gameap-api/web/plugin-sdk, so the + # gameap repo must be checked out as a sibling directory named gameap-api + - uses: actions/checkout@v4 + with: + repository: gameap/gameap + ref: ${{ env.GAMEAP_REF }} + path: gameap-api + persist-credentials: false # Cargo.toml depends on path = "../gameap-proto/rust/gameap-plugin-sdk" - uses: actions/checkout@v4 @@ -29,12 +44,30 @@ jobs: repository: gameap/gameap-proto ref: ${{ env.GAMEAP_PROTO_REF }} path: gameap-proto + persist-credentials: false - uses: actions/setup-node@v4 with: node-version: 22 cache: npm - cache-dependency-path: plugin-hex-editor/frontend/package-lock.json + cache-dependency-path: | + gameap-api/web/plugin-sdk/package-lock.json + plugin-hex-editor/frontend/package-lock.json + + # The @gameap/ui tarball on npm is missing its icons/ directory, so the + # package from the gameap checkout is installed over it. That package + # ships no type declarations, so the tsc half of the SDK's "npm run + # build" fails on the '@gameap/ui' re-export; the ambient shim types it + # as `any` so tsc can emit dist/*.d.ts - without them the frontend + # typecheck hits TS7016 on every '@gameap/plugin-sdk' import. + - name: Build plugin SDK + working-directory: gameap-api/web/plugin-sdk + run: | + npm ci + npm install --no-save ../frontend/packages/gameap-ui + echo "declare module '@gameap/ui';" > src/gameap-ui-shim.d.ts + npx vite build + npx tsc --emitDeclarationOnly - name: Build frontend working-directory: plugin-hex-editor/frontend @@ -42,10 +75,15 @@ jobs: npm ci npm run build - - name: Install binaryen - run: sudo apt-get update && sudo apt-get install -y binaryen + - name: Typecheck frontend + working-directory: plugin-hex-editor/frontend + run: npm run typecheck - # Installs the toolchain and target pinned in rust-toolchain.toml + - name: Test frontend + working-directory: plugin-hex-editor/frontend + run: npm test + + # Installs the toolchain, target and clippy pinned in rust-toolchain.toml - name: Set up Rust working-directory: plugin-hex-editor run: rustup toolchain install @@ -54,6 +92,19 @@ jobs: with: workspaces: plugin-hex-editor + - name: Test + working-directory: plugin-hex-editor + run: cargo test + + - name: Clippy + working-directory: plugin-hex-editor + run: | + cargo clippy --target wasm32-wasip1 -- -D warnings + cargo clippy --all-targets -- -D warnings + + - name: Install binaryen + run: sudo apt-get update && sudo apt-get install -y binaryen + - name: Build wasm working-directory: plugin-hex-editor run: make wasm diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 04e1a39..78aad7b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -8,9 +8,14 @@ permissions: contents: write env: + # Ref of gameap/gameap providing web/plugin-sdk (0.3.3+: contentType 'none', + # fileSize/fileMtime props) and the panel that streams HTTPResponse.file. + GAMEAP_REF: main # Ref of gameap/gameap-proto providing rust/gameap-plugin-sdk; pin to a SHA if - # the SDK drifts from Cargo.lock - GAMEAP_PROTO_REF: main + # the SDK drifts from Cargo.lock. Pinned to the head of the proto-updates + # branch: HTTPResponse.file / FileRef, which src/http.rs sets, is not on main + # yet. Restore `main` once proto-updates is merged. + GAMEAP_PROTO_REF: aba9a6c002358ed56691ff1f5c28d448e5f2cdee jobs: publish: @@ -19,6 +24,16 @@ jobs: - uses: actions/checkout@v4 with: path: plugin-hex-editor + persist-credentials: false + + # The frontend depends on file:../../gameap-api/web/plugin-sdk, so the + # gameap repo must be checked out as a sibling directory named gameap-api + - uses: actions/checkout@v4 + with: + repository: gameap/gameap + ref: ${{ env.GAMEAP_REF }} + path: gameap-api + persist-credentials: false # Cargo.toml depends on path = "../gameap-proto/rust/gameap-plugin-sdk" - uses: actions/checkout@v4 @@ -26,12 +41,11 @@ jobs: repository: gameap/gameap-proto ref: ${{ env.GAMEAP_PROTO_REF }} path: gameap-proto + persist-credentials: false - uses: actions/setup-node@v4 with: node-version: 22 - cache: npm - cache-dependency-path: plugin-hex-editor/frontend/package-lock.json # Bakes the release tag into Cargo.toml version (which get_info reads via # CARGO_PKG_VERSION) and the frontend PluginDefinition before anything is built. @@ -40,18 +54,39 @@ jobs: env: TAG: ${{ github.event.release.tag_name }} run: | + # Only a plain semver tag (optional v prefix, optional pre-release) + # reaches sed: no slashes, ampersands or other replacement syntax. + if ! printf '%s' "$TAG" | grep -Eq '^v?[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$'; then + echo "::error::release tag '$TAG' is not a semver tag (expected v1.2.3 or v1.2.3-rc.1)" + exit 1 + fi VERSION="${TAG#v}" sed -i "s/^version = \".*\"$/version = \"${VERSION}\"/" Cargo.toml sed -i "s/^ version: '.*',$/ version: '${VERSION}',/" frontend/src/index.ts grep -q "^version = \"${VERSION}\"$" Cargo.toml grep -q "version: '${VERSION}'," frontend/src/index.ts + # See the same step in build.yml for why @gameap/ui is installed over + # the npm tarball and why a shim declaration is written. + - name: Build plugin SDK + working-directory: gameap-api/web/plugin-sdk + run: | + npm ci + npm install --no-save ../frontend/packages/gameap-ui + echo "declare module '@gameap/ui';" > src/gameap-ui-shim.d.ts + npx vite build + npx tsc --emitDeclarationOnly + - name: Build frontend working-directory: plugin-hex-editor/frontend run: | npm ci npm run build + - name: Test frontend + working-directory: plugin-hex-editor/frontend + run: npm test + - name: Install binaryen run: sudo apt-get update && sudo apt-get install -y binaryen @@ -64,6 +99,10 @@ jobs: with: workspaces: plugin-hex-editor + - name: Test + working-directory: plugin-hex-editor + run: cargo test + - name: Build wasm working-directory: plugin-hex-editor run: make wasm diff --git a/.gitignore b/.gitignore index 54a46a9..557cf7f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,7 @@ /target -*.wasm -frontend/dist -!frontend/dist/.gitkeep -frontend/node_modules \ No newline at end of file +/hex-editor.wasm +*.wasm.asc +/frontend/dist +/frontend/node_modules +.idea/ +.DS_Store diff --git a/Cargo.lock b/Cargo.lock index 168bc9d..1b4a315 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -59,6 +59,8 @@ name = "hex-editor" version = "1.0.0" dependencies = [ "gameap-plugin-sdk", + "serde", + "serde_json", ] [[package]] @@ -70,12 +72,24 @@ dependencies = [ "either", ] +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + [[package]] name = "libc" version = "0.2.186" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + [[package]] name = "proc-macro2" version = "1.0.107" @@ -105,7 +119,7 @@ dependencies = [ "itertools", "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -126,6 +140,49 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.4", +] + +[[package]] +name = "serde_json" +version = "1.0.151" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + [[package]] name = "syn" version = "2.0.119" @@ -137,6 +194,17 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "syn" +version = "3.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6275cddf4610d1775e6d1fe9469b2e77d0f39fd98fb7450901b821e0c53649f" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "unicode-ident" version = "1.0.24" @@ -157,3 +225,9 @@ checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" dependencies = [ "windows-link", ] + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/Cargo.toml b/Cargo.toml index ea2e0fe..37e5ee9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,10 +7,12 @@ publish = false description = "GameAP plugin: view and edit files in hexadecimal format" [lib] -crate-type = ["cdylib"] +crate-type = ["cdylib", "rlib"] [dependencies] gameap-plugin-sdk = { path = "../gameap-proto/rust/gameap-plugin-sdk" } +serde = { version = "1", features = ["derive"] } +serde_json = "1" [workspace] @@ -20,3 +22,7 @@ lto = true strip = true panic = "abort" codegen-units = 1 + +[lints.clippy] +unwrap_used = "warn" +expect_used = "warn" diff --git a/Makefile b/Makefile index 95200c1..99f4300 100644 --- a/Makefile +++ b/Makefile @@ -2,25 +2,51 @@ PLUGIN_NAME = hex-editor CRATE_WASM = target/wasm32-wasip1/release/hex_editor.wasm WASM_OUT = $(PLUGIN_NAME).wasm -.PHONY: all frontend wasm build clean +.PHONY: all build frontend wasm test lint clean sign verify release info all: build +build: frontend wasm + frontend: cd frontend && npm ci && npm run build wasm: cargo build --target wasm32-wasip1 --release @if command -v wasm-opt >/dev/null 2>&1; then \ - wasm-opt -Oz --enable-bulk-memory --enable-sign-ext --enable-mutable-globals --enable-nontrapping-float-to-int -o $(WASM_OUT) $(CRATE_WASM); \ + wasm-opt -Oz --enable-bulk-memory --enable-sign-ext --enable-mutable-globals \ + --enable-nontrapping-float-to-int -o $(WASM_OUT) $(CRATE_WASM); \ else \ echo "wasm-opt not found; install binaryen for smaller output"; \ cp $(CRATE_WASM) $(WASM_OUT); \ fi -build: frontend wasm +test: + cargo test + cd frontend && npm test + +lint: + cargo clippy --target wasm32-wasip1 -- -D warnings + cargo clippy --all-targets -- -D warnings + cd frontend && npm run typecheck + +sign: + @if [ -n "$(GPG_KEY)" ]; then \ + gpg --detach-sign --armor --local-user $(GPG_KEY) $(WASM_OUT); \ + else \ + gpg --detach-sign --armor $(WASM_OUT); \ + fi + +verify: + gpg --verify $(WASM_OUT).asc $(WASM_OUT) + +release: build sign clean: cargo clean - rm -f $(WASM_OUT) + rm -f $(WASM_OUT) $(WASM_OUT).asc rm -rf frontend/dist + +info: + @echo "Plugin: $(PLUGIN_NAME)" + @echo "Version: $$(grep -m1 '^version' Cargo.toml | cut -d'"' -f2)" diff --git a/README.md b/README.md index 683243f..bf60cef 100644 --- a/README.md +++ b/README.md @@ -6,44 +6,86 @@ A plugin to view and edit files in hexadecimal format. ![Hex Editor Demo](hex-editor.gif) +## Requirements + +**GameAP 4.5 or newer.** The editor reads a file through the plugin's own +routes, and the panel streams it with HTTP `Range` support on the plugin's +behalf (`HTTPResponse.file`). Older panels ignore that and hand a file editor +decoded text instead, so this version refuses to open a file there and says so. + +The plugin needs one permission, **`files_read`**, granted at install or later +under Administration → Plugins → Permissions. With permission enforcement off a +missing grant is only logged at load; with it on, opening a file is refused. +Per-user access is the file manager's own: whoever may browse a server's files +(`game-server-files`, or an administrator) may open them here. + ## Features -- **Hex View**: Display file contents in traditional hexadecimal format with ASCII representation -- **Inline Editing**: Edit bytes directly by typing hex values (0-9, A-F) -- **Data Inspector**: View selected bytes as different data types: - - Binary, Int8, UInt8 - - Int16, UInt16, Int32, UInt32 - - Int64, UInt64 - - Float32, Float64 -- **Endianness Toggle**: Switch between Little Endian and Big Endian byte order -- **Byte Operations**: Add new bytes or delete existing ones (Backspace/Delete keys) -- **Keyboard Navigation**: - - Arrow keys for movement - - Home/End for row start/end - - PageUp/PageDown for fast scrolling -- **Virtual Scrolling**: Efficiently handle large files with smooth scrolling -- **Modification Tracking**: Visual highlighting of modified bytes -- **Dark Mode Support**: Seamless integration with GameAP's dark theme +- **Hex View**: file contents in traditional hexadecimal format with ASCII +- **Files of any size**: larger files are read one window at a time, so a + multi-gigabyte file opens as quickly as a small one +- **Inline Editing**: edit bytes directly by typing hex values (0-9, A-F) +- **Data Inspector**: the selection as Binary, Int8/UInt8, Int16/UInt16, + Int32/UInt32, Int64/UInt64, Float32, Float64, with an endianness toggle +- **Byte Operations**: add and delete bytes (files up to 4 MiB) +- **Go to offset**: jump anywhere by hex offset +- **Keyboard Navigation**: arrows, Home/End, PageUp/PageDown, Ctrl+Home/End +- **Modification Tracking**: modified bytes are highlighted, and a byte typed + back to its original value stops being one +- **Dark Mode Support** + +### How large files are handled + +| File size | What the editor does | +|---|---| +| up to 4 MiB | held in memory; bytes can be overwritten, inserted and deleted | +| 4 MiB to 100 MB | read in 256 KiB windows; bytes can be overwritten only | +| over 100 MB | opened read-only | + +Overwriting is the only edit available above 4 MiB because nothing in the stack +can write part of a file: inserting a byte would move every byte after it, and +the whole file would have to be rewritten. Saving reconstructs the file in the +browser and hands it to the file manager, whose upload endpoint caps a write at +100 MB — hence the read-only threshold. + +Before a save the editor re-reads the file's size and modification time. If the +file changed on the server it asks before overwriting, and if its size changed +while windows were open it reloads instead: every pending edit is an offset +into a file that no longer has that shape. ## Development Setup -1. Install dependencies: +Two sibling checkouts are needed: + +- [`gameap`](https://github.com/gameap/gameap) as `../gameap-api` — the frontend + SDK (`web/plugin-sdk`) and the debug harness +- [`gameap-proto`](https://github.com/gameap/gameap-proto) as `../gameap-proto` — + the Rust plugin SDK. Until the `proto-updates` branch is merged it is the ref + that carries `HTTPResponse.file` (see `GAMEAP_PROTO_REF` in the workflows). + +1. Build the frontend SDK once: ```bash - cd frontend - npm install + cd ../gameap-api/web/plugin-sdk && npm ci && npm run build ``` -2. Build the plugin: +2. Install dependencies: + ```bash + cd frontend && npm install + ``` + +3. Build the plugin: ```bash npm run build ``` -3. Run debug harness (after @gameap/frontend and @gameap/debug are published): +4. Run the debug harness (real panel frontend with a mock API). It ships sample + files under `hex-samples/` covering every size and failure this editor + handles, from an empty file to five gigabytes: ```bash npm run debug ``` -4. Watch mode for development: +5. Watch mode for development: ```bash npm run dev ``` @@ -51,14 +93,21 @@ A plugin to view and edit files in hexadecimal format. ## Compile WebAssembly The plugin backend is written in Rust and compiled to a `wasm32-wasip1` module. +It serves the embedded frontend bundle and two routes: + +| Route | Answers | +|---|---| +| `GET /api/plugins/hexeditor4jm2/servers/{id}/file?path=` | the file, streamed by the panel, `Range` honoured | +| `GET /api/plugins/hexeditor4jm2/servers/{id}/file/info?path=` | `{size, mtime, path}` from a fresh stat | ### Prerequisites -- Rust toolchain (installed automatically from `rust-toolchain.toml`: 1.94.0 + `wasm32-wasip1`) +- Rust toolchain (installed automatically from `rust-toolchain.toml`: 1.94.0 + + `wasm32-wasip1` + clippy) - Node.js (to build the embedded frontend) -- [Binaryen](https://github.com/WebAssembly/binaryen) for `wasm-opt` (e.g. `brew install binaryen`) -- The [`gameap-proto`](https://github.com/gameap/gameap-proto) repository checked out as a - sibling directory (`../gameap-proto`), which provides the Rust plugin SDK +- [Binaryen](https://github.com/WebAssembly/binaryen) for `wasm-opt` + (e.g. `brew install binaryen`) +- The sibling checkouts listed above ### Build @@ -66,8 +115,9 @@ The plugin backend is written in Rust and compiled to a `wasm32-wasip1` module. make build ``` -This builds the frontend (`frontend/dist/plugin.js` + `hex-editor-plugin.css`), compiles the -Rust wasm with size optimizations, runs `wasm-opt -Oz`, and writes `hex-editor.wasm`. +This builds the frontend (`frontend/dist/plugin.js` + `hex-editor-plugin.css`), +compiles the Rust wasm with size optimizations, runs `wasm-opt -Oz`, and writes +`hex-editor.wasm`. Or run the steps manually: @@ -75,3 +125,10 @@ Or run the steps manually: cd frontend && npm ci && npm run build && cd .. make wasm ``` + +## Tests + +```sh +make test # cargo test (against an in-memory host) and vitest +make lint # clippy for both targets and vue-tsc +``` diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 0a1f51e..1b9c6b4 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -8,22 +8,71 @@ "name": "hex-editor-plugin", "version": "1.0.0", "dependencies": { - "@gameap/plugin-sdk": "^0.2.0" + "@gameap/plugin-sdk": "file:../../gameap-api/web/plugin-sdk" }, "devDependencies": { - "@gameap/debug": "^0.2.17", + "@gameap/debug": "file:../../gameap-api/web/frontend/packages/gameap-debug", "@tailwindcss/postcss": "^4.1.18", "@vitejs/plugin-vue": "^6.0.1", "autoprefixer": "^10.4.23", + "axios": "^1.19.0", "browser-sync": "^3.0.4", "concurrently": "^9.2.1", + "naive-ui": "^2.44.1", "postcss": "^8.5.6", "tailwindcss": "^4.1.18", "typescript": "^5.3.0", "vite": "^7.1.0", + "vitest": "^3.0.0", + "vue": "^3.5.22", + "vue-tsc": "^2.0.0" + }, + "peerDependencies": { + "vue": "^3.5.0" + } + }, + "../../gameap-api/web/frontend/packages/gameap-debug": { + "name": "@gameap/debug", + "version": "0.4.0", + "dev": true, + "dependencies": { + "@gameap/frontend": "file:../gameap-frontend", + "@gameap/plugin-sdk": "^0.3.2", + "@gameap/ui": "file:../gameap-ui", + "@originjs/vite-plugin-commonjs": "^1.0.3", + "@vitejs/plugin-vue": "^6.0.8", + "msw": "^2.15.0", + "postcss-preset-env": "^9.5.0", + "sass": "^1.103.1", + "vite": "^7.3.6" + }, + "bin": { + "gameap-debug": "bin/cli.js" + }, + "devDependencies": { + "typescript": "^5.3.0", + "vue-tsc": "^2.2.10" + }, + "peerDependencies": { + "axios": "^1.12.0", + "naive-ui": "^2.43.0", + "pinia": "^3.0.0", + "vue": "^3.5.0", + "vue-router": "^4.6.0" + } + }, + "../../gameap-api/web/plugin-sdk": { + "name": "@gameap/plugin-sdk", + "version": "0.3.3", + "license": "MIT", + "devDependencies": { + "@vitejs/plugin-vue": "^6.0.1", + "typescript": "^5.3.0", + "vite": "^7.1.0", "vue": "^3.5.22" }, "peerDependencies": { + "@gameap/ui": "^1.3.0", "vue": "^3.5.0" } }, @@ -44,6 +93,7 @@ "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -53,6 +103,7 @@ "version": "7.28.5", "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -62,6 +113,7 @@ "version": "7.28.5", "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", + "dev": true, "license": "MIT", "dependencies": { "@babel/types": "^7.28.5" @@ -77,6 +129,7 @@ "version": "7.28.5", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-string-parser": "^7.27.1", @@ -106,1084 +159,655 @@ "vue": "^3.0.11" } }, - "node_modules/@csstools/cascade-layer-name-parser": { - "version": "1.0.13", - "resolved": "https://registry.npmjs.org/@csstools/cascade-layer-name-parser/-/cascade-layer-name-parser-1.0.13.tgz", - "integrity": "sha512-MX0yLTwtZzr82sQ0zOjqimpZbzjMaK/h2pmlrLK7DCzlmiZLYFpoO94WmN1akRVo6ll/TdpHb53vihHLUMyvng==", + "node_modules/@emotion/hash": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.8.0.tgz", + "integrity": "sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow==", "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT", - "engines": { - "node": "^14 || ^16 || >=18" - }, - "peerDependencies": { - "@csstools/css-parser-algorithms": "^2.7.1", - "@csstools/css-tokenizer": "^2.4.1" - } + "license": "MIT" }, - "node_modules/@csstools/color-helpers": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-4.2.1.tgz", - "integrity": "sha512-CEypeeykO9AN7JWkr1OEOQb0HRzZlPWGwV0Ya6DuVgFdDi6g3ma/cPZ5ZPZM4AWQikDpq/0llnGGlIL+j8afzw==", + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz", + "integrity": "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==", + "cpu": [ + "ppc64" + ], "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } + "license": "MIT", + "optional": true, + "os": [ + "aix" ], - "license": "MIT-0", "engines": { - "node": "^14 || ^16 || >=18" + "node": ">=18" } }, - "node_modules/@csstools/css-calc": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-1.2.4.tgz", - "integrity": "sha512-tfOuvUQeo7Hz+FcuOd3LfXVp+342pnWUJ7D2y8NUpu1Ww6xnTbHLpz018/y6rtbHifJ3iIEf9ttxXd8KG7nL0Q==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } + "node_modules/@esbuild/android-arm": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.2.tgz", + "integrity": "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==", + "cpu": [ + "arm" ], + "dev": true, "license": "MIT", + "optional": true, + "os": [ + "android" + ], "engines": { - "node": "^14 || ^16 || >=18" - }, - "peerDependencies": { - "@csstools/css-parser-algorithms": "^2.7.1", - "@csstools/css-tokenizer": "^2.4.1" + "node": ">=18" } }, - "node_modules/@csstools/css-color-parser": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-2.0.5.tgz", - "integrity": "sha512-lRZSmtl+DSjok3u9hTWpmkxFZnz7stkbZxzKc08aDUsdrWwhSgWo8yq9rq9DaFUtbAyAq2xnH92fj01S+pwIww==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } + "node_modules/@esbuild/android-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.2.tgz", + "integrity": "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==", + "cpu": [ + "arm64" ], + "dev": true, "license": "MIT", - "dependencies": { - "@csstools/color-helpers": "^4.2.1", - "@csstools/css-calc": "^1.2.4" - }, + "optional": true, + "os": [ + "android" + ], "engines": { - "node": "^14 || ^16 || >=18" - }, - "peerDependencies": { - "@csstools/css-parser-algorithms": "^2.7.1", - "@csstools/css-tokenizer": "^2.4.1" + "node": ">=18" } }, - "node_modules/@csstools/css-parser-algorithms": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-2.7.1.tgz", - "integrity": "sha512-2SJS42gxmACHgikc1WGesXLIT8d/q2l0UFM7TaEeIzdFCE/FPMtTiizcPGGJtlPo2xuQzY09OhrLTzRxqJqwGw==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } + "node_modules/@esbuild/android-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.2.tgz", + "integrity": "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==", + "cpu": [ + "x64" ], + "dev": true, "license": "MIT", - "peer": true, + "optional": true, + "os": [ + "android" + ], "engines": { - "node": "^14 || ^16 || >=18" - }, - "peerDependencies": { - "@csstools/css-tokenizer": "^2.4.1" + "node": ">=18" } }, - "node_modules/@csstools/css-tokenizer": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-2.4.1.tgz", - "integrity": "sha512-eQ9DIktFJBhGjioABJRtUucoWR2mwllurfnM8LuNGAqX3ViZXaUchqk+1s7jjtkFiT9ySdACsFEA3etErkALUg==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.2.tgz", + "integrity": "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==", + "cpu": [ + "arm64" ], + "dev": true, "license": "MIT", - "peer": true, + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": "^14 || ^16 || >=18" + "node": ">=18" } }, - "node_modules/@csstools/media-query-list-parser": { - "version": "2.1.13", - "resolved": "https://registry.npmjs.org/@csstools/media-query-list-parser/-/media-query-list-parser-2.1.13.tgz", - "integrity": "sha512-XaHr+16KRU9Gf8XLi3q8kDlI18d5vzKSKCY510Vrtc9iNR0NJzbY9hhTmwhzYZj/ZwGL4VmB3TA9hJW0Um2qFA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.2.tgz", + "integrity": "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==", + "cpu": [ + "x64" ], + "dev": true, "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": "^14 || ^16 || >=18" - }, - "peerDependencies": { - "@csstools/css-parser-algorithms": "^2.7.1", - "@csstools/css-tokenizer": "^2.4.1" + "node": ">=18" } }, - "node_modules/@csstools/postcss-cascade-layers": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/@csstools/postcss-cascade-layers/-/postcss-cascade-layers-4.0.6.tgz", - "integrity": "sha512-Xt00qGAQyqAODFiFEJNkTpSUz5VfYqnDLECdlA/Vv17nl/OIV5QfTRHGAXrBGG5YcJyHpJ+GF9gF/RZvOQz4oA==", + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.2.tgz", + "integrity": "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==", + "cpu": [ + "arm64" + ], "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } + "license": "MIT", + "optional": true, + "os": [ + "freebsd" ], - "license": "MIT-0", - "dependencies": { - "@csstools/selector-specificity": "^3.1.1", - "postcss-selector-parser": "^6.0.13" - }, "engines": { - "node": "^14 || ^16 || >=18" - }, - "peerDependencies": { - "postcss": "^8.4" + "node": ">=18" } }, - "node_modules/@csstools/postcss-color-function": { - "version": "3.0.19", - "resolved": "https://registry.npmjs.org/@csstools/postcss-color-function/-/postcss-color-function-3.0.19.tgz", - "integrity": "sha512-d1OHEXyYGe21G3q88LezWWx31ImEDdmINNDy0LyLNN9ChgN2bPxoubUPiHf9KmwypBMaHmNcMuA/WZOKdZk/Lg==", + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.2.tgz", + "integrity": "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==", + "cpu": [ + "x64" + ], "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } + "license": "MIT", + "optional": true, + "os": [ + "freebsd" ], - "license": "MIT-0", - "dependencies": { - "@csstools/css-color-parser": "^2.0.4", - "@csstools/css-parser-algorithms": "^2.7.1", - "@csstools/css-tokenizer": "^2.4.1", - "@csstools/postcss-progressive-custom-properties": "^3.3.0", - "@csstools/utilities": "^1.0.0" - }, "engines": { - "node": "^14 || ^16 || >=18" - }, - "peerDependencies": { - "postcss": "^8.4" + "node": ">=18" } }, - "node_modules/@csstools/postcss-color-mix-function": { - "version": "2.0.19", - "resolved": "https://registry.npmjs.org/@csstools/postcss-color-mix-function/-/postcss-color-mix-function-2.0.19.tgz", - "integrity": "sha512-mLvQlMX+keRYr16AuvuV8WYKUwF+D0DiCqlBdvhQ0KYEtcQl9/is9Ssg7RcIys8x0jIn2h1zstS4izckdZj9wg==", + "node_modules/@esbuild/linux-arm": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.2.tgz", + "integrity": "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==", + "cpu": [ + "arm" + ], "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } + "license": "MIT", + "optional": true, + "os": [ + "linux" ], - "license": "MIT-0", - "dependencies": { - "@csstools/css-color-parser": "^2.0.4", - "@csstools/css-parser-algorithms": "^2.7.1", - "@csstools/css-tokenizer": "^2.4.1", - "@csstools/postcss-progressive-custom-properties": "^3.3.0", - "@csstools/utilities": "^1.0.0" - }, "engines": { - "node": "^14 || ^16 || >=18" - }, - "peerDependencies": { - "postcss": "^8.4" + "node": ">=18" } }, - "node_modules/@csstools/postcss-content-alt-text": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@csstools/postcss-content-alt-text/-/postcss-content-alt-text-1.0.0.tgz", - "integrity": "sha512-SkHdj7EMM/57GVvSxSELpUg7zb5eAndBeuvGwFzYtU06/QXJ/h9fuK7wO5suteJzGhm3GDF/EWPCdWV2h1IGHQ==", + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.2.tgz", + "integrity": "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==", + "cpu": [ + "arm64" + ], "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } + "license": "MIT", + "optional": true, + "os": [ + "linux" ], - "license": "MIT-0", - "dependencies": { - "@csstools/css-parser-algorithms": "^2.7.1", - "@csstools/css-tokenizer": "^2.4.1", - "@csstools/postcss-progressive-custom-properties": "^3.3.0", - "@csstools/utilities": "^1.0.0" - }, "engines": { - "node": "^14 || ^16 || >=18" - }, - "peerDependencies": { - "postcss": "^8.4" + "node": ">=18" } }, - "node_modules/@csstools/postcss-exponential-functions": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/@csstools/postcss-exponential-functions/-/postcss-exponential-functions-1.0.9.tgz", - "integrity": "sha512-x1Avr15mMeuX7Z5RJUl7DmjhUtg+Amn5DZRD0fQ2TlTFTcJS8U1oxXQ9e5mA62S2RJgUU6db20CRoJyDvae2EQ==", + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.2.tgz", + "integrity": "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==", + "cpu": [ + "ia32" + ], "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } + "license": "MIT", + "optional": true, + "os": [ + "linux" ], - "license": "MIT-0", - "dependencies": { - "@csstools/css-calc": "^1.2.4", - "@csstools/css-parser-algorithms": "^2.7.1", - "@csstools/css-tokenizer": "^2.4.1" - }, "engines": { - "node": "^14 || ^16 || >=18" - }, - "peerDependencies": { - "postcss": "^8.4" + "node": ">=18" } }, - "node_modules/@csstools/postcss-font-format-keywords": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@csstools/postcss-font-format-keywords/-/postcss-font-format-keywords-3.0.2.tgz", - "integrity": "sha512-E0xz2sjm4AMCkXLCFvI/lyl4XO6aN1NCSMMVEOngFDJ+k2rDwfr6NDjWljk1li42jiLNChVX+YFnmfGCigZKXw==", + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.2.tgz", + "integrity": "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==", + "cpu": [ + "mips64el" + ], "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } + "license": "MIT", + "optional": true, + "os": [ + "linux" ], - "license": "MIT-0", - "dependencies": { - "@csstools/utilities": "^1.0.0", - "postcss-value-parser": "^4.2.0" - }, "engines": { - "node": "^14 || ^16 || >=18" - }, - "peerDependencies": { - "postcss": "^8.4" + "node": ">=18" } }, - "node_modules/@csstools/postcss-gamut-mapping": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/@csstools/postcss-gamut-mapping/-/postcss-gamut-mapping-1.0.11.tgz", - "integrity": "sha512-KrHGsUPXRYxboXmJ9wiU/RzDM7y/5uIefLWKFSc36Pok7fxiPyvkSHO51kh+RLZS1W5hbqw9qaa6+tKpTSxa5g==", + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.2.tgz", + "integrity": "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==", + "cpu": [ + "ppc64" + ], "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } + "license": "MIT", + "optional": true, + "os": [ + "linux" ], - "license": "MIT-0", - "dependencies": { - "@csstools/css-color-parser": "^2.0.4", - "@csstools/css-parser-algorithms": "^2.7.1", - "@csstools/css-tokenizer": "^2.4.1" - }, "engines": { - "node": "^14 || ^16 || >=18" - }, - "peerDependencies": { - "postcss": "^8.4" + "node": ">=18" } }, - "node_modules/@csstools/postcss-gradients-interpolation-method": { - "version": "4.0.20", - "resolved": "https://registry.npmjs.org/@csstools/postcss-gradients-interpolation-method/-/postcss-gradients-interpolation-method-4.0.20.tgz", - "integrity": "sha512-ZFl2JBHano6R20KB5ZrB8KdPM2pVK0u+/3cGQ2T8VubJq982I2LSOvQ4/VtxkAXjkPkk1rXt4AD1ni7UjTZ1Og==", + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.2.tgz", + "integrity": "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==", + "cpu": [ + "riscv64" + ], "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } + "license": "MIT", + "optional": true, + "os": [ + "linux" ], - "license": "MIT-0", - "dependencies": { - "@csstools/css-color-parser": "^2.0.4", - "@csstools/css-parser-algorithms": "^2.7.1", - "@csstools/css-tokenizer": "^2.4.1", - "@csstools/postcss-progressive-custom-properties": "^3.3.0", - "@csstools/utilities": "^1.0.0" - }, "engines": { - "node": "^14 || ^16 || >=18" - }, - "peerDependencies": { - "postcss": "^8.4" + "node": ">=18" } }, - "node_modules/@csstools/postcss-hwb-function": { - "version": "3.0.18", - "resolved": "https://registry.npmjs.org/@csstools/postcss-hwb-function/-/postcss-hwb-function-3.0.18.tgz", - "integrity": "sha512-3ifnLltR5C7zrJ+g18caxkvSRnu9jBBXCYgnBznRjxm6gQJGnnCO9H6toHfywNdNr/qkiVf2dymERPQLDnjLRQ==", + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.2.tgz", + "integrity": "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==", + "cpu": [ + "s390x" + ], "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } + "license": "MIT", + "optional": true, + "os": [ + "linux" ], - "license": "MIT-0", - "dependencies": { - "@csstools/css-color-parser": "^2.0.4", - "@csstools/css-parser-algorithms": "^2.7.1", - "@csstools/css-tokenizer": "^2.4.1", - "@csstools/postcss-progressive-custom-properties": "^3.3.0", - "@csstools/utilities": "^1.0.0" - }, "engines": { - "node": "^14 || ^16 || >=18" - }, - "peerDependencies": { - "postcss": "^8.4" + "node": ">=18" } }, - "node_modules/@csstools/postcss-ic-unit": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/@csstools/postcss-ic-unit/-/postcss-ic-unit-3.0.7.tgz", - "integrity": "sha512-YoaNHH2wNZD+c+rHV02l4xQuDpfR8MaL7hD45iJyr+USwvr0LOheeytJ6rq8FN6hXBmEeoJBeXXgGmM8fkhH4g==", + "node_modules/@esbuild/linux-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.2.tgz", + "integrity": "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==", + "cpu": [ + "x64" + ], "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } + "license": "MIT", + "optional": true, + "os": [ + "linux" ], - "license": "MIT-0", - "dependencies": { - "@csstools/postcss-progressive-custom-properties": "^3.3.0", - "@csstools/utilities": "^1.0.0", - "postcss-value-parser": "^4.2.0" - }, "engines": { - "node": "^14 || ^16 || >=18" - }, - "peerDependencies": { - "postcss": "^8.4" + "node": ">=18" } }, - "node_modules/@csstools/postcss-initial": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@csstools/postcss-initial/-/postcss-initial-1.0.1.tgz", - "integrity": "sha512-wtb+IbUIrIf8CrN6MLQuFR7nlU5C7PwuebfeEXfjthUha1+XZj2RVi+5k/lukToA24sZkYAiSJfHM8uG/UZIdg==", + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.2.tgz", + "integrity": "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==", + "cpu": [ + "arm64" + ], "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } + "license": "MIT", + "optional": true, + "os": [ + "netbsd" ], - "license": "MIT-0", "engines": { - "node": "^14 || ^16 || >=18" - }, - "peerDependencies": { - "postcss": "^8.4" + "node": ">=18" } }, - "node_modules/@csstools/postcss-is-pseudo-class": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/@csstools/postcss-is-pseudo-class/-/postcss-is-pseudo-class-4.0.8.tgz", - "integrity": "sha512-0aj591yGlq5Qac+plaWCbn5cpjs5Sh0daovYUKJUOMjIp70prGH/XPLp7QjxtbFXz3CTvb0H9a35dpEuIuUi3Q==", + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.2.tgz", + "integrity": "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==", + "cpu": [ + "x64" + ], "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } + "license": "MIT", + "optional": true, + "os": [ + "netbsd" ], - "license": "MIT-0", - "dependencies": { - "@csstools/selector-specificity": "^3.1.1", - "postcss-selector-parser": "^6.0.13" - }, "engines": { - "node": "^14 || ^16 || >=18" - }, - "peerDependencies": { - "postcss": "^8.4" + "node": ">=18" } }, - "node_modules/@csstools/postcss-light-dark-function": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@csstools/postcss-light-dark-function/-/postcss-light-dark-function-1.0.8.tgz", - "integrity": "sha512-x0UtpCyVnERsplUeoaY6nEtp1HxTf4lJjoK/ULEm40DraqFfUdUSt76yoOyX5rGY6eeOUOkurHyYlFHVKv/pew==", + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.2.tgz", + "integrity": "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==", + "cpu": [ + "arm64" + ], "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } + "license": "MIT", + "optional": true, + "os": [ + "openbsd" ], - "license": "MIT-0", - "dependencies": { - "@csstools/css-parser-algorithms": "^2.7.1", - "@csstools/css-tokenizer": "^2.4.1", - "@csstools/postcss-progressive-custom-properties": "^3.3.0", - "@csstools/utilities": "^1.0.0" - }, "engines": { - "node": "^14 || ^16 || >=18" - }, - "peerDependencies": { - "postcss": "^8.4" + "node": ">=18" } }, - "node_modules/@csstools/postcss-logical-float-and-clear": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@csstools/postcss-logical-float-and-clear/-/postcss-logical-float-and-clear-2.0.1.tgz", - "integrity": "sha512-SsrWUNaXKr+e/Uo4R/uIsqJYt3DaggIh/jyZdhy/q8fECoJSKsSMr7nObSLdvoULB69Zb6Bs+sefEIoMG/YfOA==", + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.2.tgz", + "integrity": "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==", + "cpu": [ + "x64" + ], "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } + "license": "MIT", + "optional": true, + "os": [ + "openbsd" ], - "license": "MIT-0", "engines": { - "node": "^14 || ^16 || >=18" - }, - "peerDependencies": { - "postcss": "^8.4" + "node": ">=18" } }, - "node_modules/@csstools/postcss-logical-overflow": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@csstools/postcss-logical-overflow/-/postcss-logical-overflow-1.0.1.tgz", - "integrity": "sha512-Kl4lAbMg0iyztEzDhZuQw8Sj9r2uqFDcU1IPl+AAt2nue8K/f1i7ElvKtXkjhIAmKiy5h2EY8Gt/Cqg0pYFDCw==", + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.2.tgz", + "integrity": "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==", + "cpu": [ + "arm64" + ], "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } + "license": "MIT", + "optional": true, + "os": [ + "openharmony" ], - "license": "MIT-0", "engines": { - "node": "^14 || ^16 || >=18" - }, - "peerDependencies": { - "postcss": "^8.4" + "node": ">=18" } }, - "node_modules/@csstools/postcss-logical-overscroll-behavior": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@csstools/postcss-logical-overscroll-behavior/-/postcss-logical-overscroll-behavior-1.0.1.tgz", - "integrity": "sha512-+kHamNxAnX8ojPCtV8WPcUP3XcqMFBSDuBuvT6MHgq7oX4IQxLIXKx64t7g9LiuJzE7vd06Q9qUYR6bh4YnGpQ==", + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.2.tgz", + "integrity": "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==", + "cpu": [ + "x64" + ], "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } + "license": "MIT", + "optional": true, + "os": [ + "sunos" ], - "license": "MIT-0", "engines": { - "node": "^14 || ^16 || >=18" - }, - "peerDependencies": { - "postcss": "^8.4" + "node": ">=18" } }, - "node_modules/@csstools/postcss-logical-resize": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@csstools/postcss-logical-resize/-/postcss-logical-resize-2.0.1.tgz", - "integrity": "sha512-W5Gtwz7oIuFcKa5SmBjQ2uxr8ZoL7M2bkoIf0T1WeNqljMkBrfw1DDA8/J83k57NQ1kcweJEjkJ04pUkmyee3A==", + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.2.tgz", + "integrity": "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==", + "cpu": [ + "arm64" + ], "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } + "license": "MIT", + "optional": true, + "os": [ + "win32" ], - "license": "MIT-0", - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, "engines": { - "node": "^14 || ^16 || >=18" - }, - "peerDependencies": { - "postcss": "^8.4" + "node": ">=18" } }, - "node_modules/@csstools/postcss-logical-viewport-units": { - "version": "2.0.11", - "resolved": "https://registry.npmjs.org/@csstools/postcss-logical-viewport-units/-/postcss-logical-viewport-units-2.0.11.tgz", - "integrity": "sha512-ElITMOGcjQtvouxjd90WmJRIw1J7KMP+M+O87HaVtlgOOlDt1uEPeTeii8qKGe2AiedEp0XOGIo9lidbiU2Ogg==", + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.2.tgz", + "integrity": "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==", + "cpu": [ + "ia32" + ], "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } + "license": "MIT", + "optional": true, + "os": [ + "win32" ], - "license": "MIT-0", - "dependencies": { - "@csstools/css-tokenizer": "^2.4.1", - "@csstools/utilities": "^1.0.0" - }, "engines": { - "node": "^14 || ^16 || >=18" - }, - "peerDependencies": { - "postcss": "^8.4" + "node": ">=18" } }, - "node_modules/@csstools/postcss-media-minmax": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/@csstools/postcss-media-minmax/-/postcss-media-minmax-1.1.8.tgz", - "integrity": "sha512-KYQCal2i7XPNtHAUxCECdrC7tuxIWQCW+s8eMYs5r5PaAiVTeKwlrkRS096PFgojdNCmHeG0Cb7njtuNswNf+w==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } + "node_modules/@esbuild/win32-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.2.tgz", + "integrity": "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==", + "cpu": [ + "x64" ], + "dev": true, "license": "MIT", - "dependencies": { - "@csstools/css-calc": "^1.2.4", - "@csstools/css-parser-algorithms": "^2.7.1", - "@csstools/css-tokenizer": "^2.4.1", - "@csstools/media-query-list-parser": "^2.1.13" - }, + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": "^14 || ^16 || >=18" - }, - "peerDependencies": { - "postcss": "^8.4" + "node": ">=18" } }, - "node_modules/@csstools/postcss-media-queries-aspect-ratio-number-values": { - "version": "2.0.11", - "resolved": "https://registry.npmjs.org/@csstools/postcss-media-queries-aspect-ratio-number-values/-/postcss-media-queries-aspect-ratio-number-values-2.0.11.tgz", - "integrity": "sha512-YD6jrib20GRGQcnOu49VJjoAnQ/4249liuz7vTpy/JfgqQ1Dlc5eD4HPUMNLOw9CWey9E6Etxwf/xc/ZF8fECA==", + "node_modules/@gameap/debug": { + "resolved": "../../gameap-api/web/frontend/packages/gameap-debug", + "link": true + }, + "node_modules/@gameap/plugin-sdk": { + "resolved": "../../gameap-api/web/plugin-sdk", + "link": true + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT-0", + "license": "MIT", "dependencies": { - "@csstools/css-parser-algorithms": "^2.7.1", - "@csstools/css-tokenizer": "^2.4.1", - "@csstools/media-query-list-parser": "^2.1.13" - }, - "engines": { - "node": "^14 || ^16 || >=18" - }, - "peerDependencies": { - "postcss": "^8.4" + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" } }, - "node_modules/@csstools/postcss-nested-calc": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@csstools/postcss-nested-calc/-/postcss-nested-calc-3.0.2.tgz", - "integrity": "sha512-ySUmPyawiHSmBW/VI44+IObcKH0v88LqFe0d09Sb3w4B1qjkaROc6d5IA3ll9kjD46IIX/dbO5bwFN/swyoyZA==", + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT-0", + "license": "MIT", "dependencies": { - "@csstools/utilities": "^1.0.0", - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^14 || ^16 || >=18" - }, - "peerDependencies": { - "postcss": "^8.4" + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" } }, - "node_modules/@csstools/postcss-normalize-display-values": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@csstools/postcss-normalize-display-values/-/postcss-normalize-display-values-3.0.2.tgz", - "integrity": "sha512-fCapyyT/dUdyPtrelQSIV+d5HqtTgnNP/BEG9IuhgXHt93Wc4CfC1bQ55GzKAjWrZbgakMQ7MLfCXEf3rlZJOw==", + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT-0", - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, + "license": "MIT", "engines": { - "node": "^14 || ^16 || >=18" - }, - "peerDependencies": { - "postcss": "^8.4" + "node": ">=6.0.0" } }, - "node_modules/@csstools/postcss-oklab-function": { - "version": "3.0.19", - "resolved": "https://registry.npmjs.org/@csstools/postcss-oklab-function/-/postcss-oklab-function-3.0.19.tgz", - "integrity": "sha512-e3JxXmxjU3jpU7TzZrsNqSX4OHByRC3XjItV3Ieo/JEQmLg5rdOL4lkv/1vp27gXemzfNt44F42k/pn0FpE21Q==", + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT-0", - "dependencies": { - "@csstools/css-color-parser": "^2.0.4", - "@csstools/css-parser-algorithms": "^2.7.1", - "@csstools/css-tokenizer": "^2.4.1", - "@csstools/postcss-progressive-custom-properties": "^3.3.0", - "@csstools/utilities": "^1.0.0" - }, - "engines": { - "node": "^14 || ^16 || >=18" - }, - "peerDependencies": { - "postcss": "^8.4" - } + "license": "MIT" }, - "node_modules/@csstools/postcss-progressive-custom-properties": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/@csstools/postcss-progressive-custom-properties/-/postcss-progressive-custom-properties-3.3.0.tgz", - "integrity": "sha512-W2oV01phnILaRGYPmGFlL2MT/OgYjQDrL9sFlbdikMFi6oQkFki9B86XqEWR7HCsTZFVq7dbzr/o71B75TKkGg==", + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT-0", + "license": "MIT", "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^14 || ^16 || >=18" - }, - "peerDependencies": { - "postcss": "^8.4" + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" } }, - "node_modules/@csstools/postcss-relative-color-syntax": { - "version": "2.0.19", - "resolved": "https://registry.npmjs.org/@csstools/postcss-relative-color-syntax/-/postcss-relative-color-syntax-2.0.19.tgz", - "integrity": "sha512-MxUMSNvio1WwuS6WRLlQuv6nNPXwIWUFzBBAvL/tBdWfiKjiJnAa6eSSN5gtaacSqUkQ/Ce5Z1OzLRfeaWhADA==", + "node_modules/@juggle/resize-observer": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/@juggle/resize-observer/-/resize-observer-3.4.0.tgz", + "integrity": "sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA==", "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT-0", + "license": "Apache-2.0" + }, + "node_modules/@parcel/watcher": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.1.tgz", + "integrity": "sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "peer": true, "dependencies": { - "@csstools/css-color-parser": "^2.0.4", - "@csstools/css-parser-algorithms": "^2.7.1", - "@csstools/css-tokenizer": "^2.4.1", - "@csstools/postcss-progressive-custom-properties": "^3.3.0", - "@csstools/utilities": "^1.0.0" + "detect-libc": "^1.0.3", + "is-glob": "^4.0.3", + "micromatch": "^4.0.5", + "node-addon-api": "^7.0.0" }, "engines": { - "node": "^14 || ^16 || >=18" + "node": ">= 10.0.0" }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/@csstools/postcss-scope-pseudo-class": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@csstools/postcss-scope-pseudo-class/-/postcss-scope-pseudo-class-3.0.1.tgz", - "integrity": "sha512-3ZFonK2gfgqg29gUJ2w7xVw2wFJ1eNWVDONjbzGkm73gJHVCYK5fnCqlLr+N+KbEfv2XbWAO0AaOJCFB6Fer6A==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT-0", - "dependencies": { - "postcss-selector-parser": "^6.0.13" - }, - "engines": { - "node": "^14 || ^16 || >=18" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" }, - "peerDependencies": { - "postcss": "^8.4" + "optionalDependencies": { + "@parcel/watcher-android-arm64": "2.5.1", + "@parcel/watcher-darwin-arm64": "2.5.1", + "@parcel/watcher-darwin-x64": "2.5.1", + "@parcel/watcher-freebsd-x64": "2.5.1", + "@parcel/watcher-linux-arm-glibc": "2.5.1", + "@parcel/watcher-linux-arm-musl": "2.5.1", + "@parcel/watcher-linux-arm64-glibc": "2.5.1", + "@parcel/watcher-linux-arm64-musl": "2.5.1", + "@parcel/watcher-linux-x64-glibc": "2.5.1", + "@parcel/watcher-linux-x64-musl": "2.5.1", + "@parcel/watcher-win32-arm64": "2.5.1", + "@parcel/watcher-win32-ia32": "2.5.1", + "@parcel/watcher-win32-x64": "2.5.1" } }, - "node_modules/@csstools/postcss-stepped-value-functions": { - "version": "3.0.10", - "resolved": "https://registry.npmjs.org/@csstools/postcss-stepped-value-functions/-/postcss-stepped-value-functions-3.0.10.tgz", - "integrity": "sha512-MZwo0D0TYrQhT5FQzMqfy/nGZ28D1iFtpN7Su1ck5BPHS95+/Y5O9S4kEvo76f2YOsqwYcT8ZGehSI1TnzuX2g==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } + "node_modules/@parcel/watcher-android-arm64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.1.tgz", + "integrity": "sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==", + "cpu": [ + "arm64" ], - "license": "MIT-0", - "dependencies": { - "@csstools/css-calc": "^1.2.4", - "@csstools/css-parser-algorithms": "^2.7.1", - "@csstools/css-tokenizer": "^2.4.1" - }, - "engines": { - "node": "^14 || ^16 || >=18" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/@csstools/postcss-text-decoration-shorthand": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/@csstools/postcss-text-decoration-shorthand/-/postcss-text-decoration-shorthand-3.0.7.tgz", - "integrity": "sha512-+cptcsM5r45jntU6VjotnkC9GteFR7BQBfZ5oW7inLCxj7AfLGAzMbZ60hKTP13AULVZBdxky0P8um0IBfLHVA==", "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } + "license": "MIT", + "optional": true, + "os": [ + "android" ], - "license": "MIT-0", - "dependencies": { - "@csstools/color-helpers": "^4.2.1", - "postcss-value-parser": "^4.2.0" - }, + "peer": true, "engines": { - "node": "^14 || ^16 || >=18" + "node": ">= 10.0.0" }, - "peerDependencies": { - "postcss": "^8.4" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/@csstools/postcss-trigonometric-functions": { - "version": "3.0.10", - "resolved": "https://registry.npmjs.org/@csstools/postcss-trigonometric-functions/-/postcss-trigonometric-functions-3.0.10.tgz", - "integrity": "sha512-G9G8moTc2wiad61nY5HfvxLiM/myX0aYK4s1x8MQlPH29WDPxHQM7ghGgvv2qf2xH+rrXhztOmjGHJj4jsEqXw==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } + "node_modules/@parcel/watcher-darwin-arm64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.1.tgz", + "integrity": "sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw==", + "cpu": [ + "arm64" ], - "license": "MIT-0", - "dependencies": { - "@csstools/css-calc": "^1.2.4", - "@csstools/css-parser-algorithms": "^2.7.1", - "@csstools/css-tokenizer": "^2.4.1" - }, - "engines": { - "node": "^14 || ^16 || >=18" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/@csstools/postcss-unset-value": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@csstools/postcss-unset-value/-/postcss-unset-value-3.0.1.tgz", - "integrity": "sha512-dbDnZ2ja2U8mbPP0Hvmt2RMEGBiF1H7oY6HYSpjteXJGihYwgxgTr6KRbbJ/V6c+4wd51M+9980qG4gKVn5ttg==", "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } + "license": "MIT", + "optional": true, + "os": [ + "darwin" ], - "license": "MIT-0", + "peer": true, "engines": { - "node": "^14 || ^16 || >=18" + "node": ">= 10.0.0" }, - "peerDependencies": { - "postcss": "^8.4" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/@csstools/selector-resolve-nested": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@csstools/selector-resolve-nested/-/selector-resolve-nested-1.1.0.tgz", - "integrity": "sha512-uWvSaeRcHyeNenKg8tp17EVDRkpflmdyvbE0DHo6D/GdBb6PDnCYYU6gRpXhtICMGMcahQmj2zGxwFM/WC8hCg==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } + "node_modules/@parcel/watcher-darwin-x64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.1.tgz", + "integrity": "sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg==", + "cpu": [ + "x64" ], - "license": "MIT-0", - "engines": { - "node": "^14 || ^16 || >=18" - }, - "peerDependencies": { - "postcss-selector-parser": "^6.0.13" - } - }, - "node_modules/@csstools/selector-specificity": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/@csstools/selector-specificity/-/selector-specificity-3.1.1.tgz", - "integrity": "sha512-a7cxGcJ2wIlMFLlh8z2ONm+715QkPHiyJcxwQlKOz/03GPw1COpfhcmC9wm4xlZfp//jWHNNMwzjtqHXVWU9KA==", "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } + "license": "MIT", + "optional": true, + "os": [ + "darwin" ], - "license": "MIT-0", + "peer": true, "engines": { - "node": "^14 || ^16 || >=18" + "node": ">= 10.0.0" }, - "peerDependencies": { - "postcss-selector-parser": "^6.0.13" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/@csstools/utilities": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@csstools/utilities/-/utilities-1.0.0.tgz", - "integrity": "sha512-tAgvZQe/t2mlvpNosA4+CkMiZ2azISW5WPAcdSalZlEjQvUfghHxfQcrCiK/7/CrfAWVxyM88kGFYO82heIGDg==", + "node_modules/@parcel/watcher-freebsd-x64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.1.tgz", + "integrity": "sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ==", + "cpu": [ + "x64" + ], "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } + "license": "MIT", + "optional": true, + "os": [ + "freebsd" ], - "license": "MIT-0", + "peer": true, "engines": { - "node": "^14 || ^16 || >=18" + "node": ">= 10.0.0" }, - "peerDependencies": { - "postcss": "^8.4" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/@emotion/hash": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.8.0.tgz", - "integrity": "sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow==", - "dev": true, - "license": "MIT" - }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz", - "integrity": "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==", + "node_modules/@parcel/watcher-linux-arm-glibc": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.1.tgz", + "integrity": "sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA==", "cpu": [ - "ppc64" + "arm" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "aix" + "linux" ], + "peer": true, "engines": { - "node": ">=18" + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/@esbuild/android-arm": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.2.tgz", - "integrity": "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==", + "node_modules/@parcel/watcher-linux-arm-musl": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.1.tgz", + "integrity": "sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==", "cpu": [ "arm" ], @@ -1191,16 +815,21 @@ "license": "MIT", "optional": true, "os": [ - "android" + "linux" ], + "peer": true, "engines": { - "node": ">=18" + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/@esbuild/android-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.2.tgz", - "integrity": "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==", + "node_modules/@parcel/watcher-linux-arm64-glibc": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.1.tgz", + "integrity": "sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==", "cpu": [ "arm64" ], @@ -1208,50 +837,65 @@ "license": "MIT", "optional": true, "os": [ - "android" + "linux" ], + "peer": true, "engines": { - "node": ">=18" + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/@esbuild/android-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.2.tgz", - "integrity": "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==", + "node_modules/@parcel/watcher-linux-arm64-musl": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.1.tgz", + "integrity": "sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==", "cpu": [ - "x64" + "arm64" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "android" + "linux" ], + "peer": true, "engines": { - "node": ">=18" + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.2.tgz", - "integrity": "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==", + "node_modules/@parcel/watcher-linux-x64-glibc": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.1.tgz", + "integrity": "sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==", "cpu": [ - "arm64" + "x64" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "darwin" + "linux" ], + "peer": true, "engines": { - "node": ">=18" + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.2.tgz", - "integrity": "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==", + "node_modules/@parcel/watcher-linux-x64-musl": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.1.tgz", + "integrity": "sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==", "cpu": [ "x64" ], @@ -1259,16 +903,21 @@ "license": "MIT", "optional": true, "os": [ - "darwin" + "linux" ], + "peer": true, "engines": { - "node": ">=18" + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.2.tgz", - "integrity": "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==", + "node_modules/@parcel/watcher-win32-arm64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.1.tgz", + "integrity": "sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==", "cpu": [ "arm64" ], @@ -1276,186 +925,199 @@ "license": "MIT", "optional": true, "os": [ - "freebsd" + "win32" ], + "peer": true, "engines": { - "node": ">=18" + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.2.tgz", - "integrity": "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==", + "node_modules/@parcel/watcher-win32-ia32": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.1.tgz", + "integrity": "sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ==", "cpu": [ - "x64" + "ia32" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "freebsd" + "win32" ], + "peer": true, "engines": { - "node": ">=18" + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/@esbuild/linux-arm": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.2.tgz", - "integrity": "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==", + "node_modules/@parcel/watcher-win32-x64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.1.tgz", + "integrity": "sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA==", "cpu": [ - "arm" + "x64" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "linux" + "win32" ], + "peer": true, "engines": { - "node": ">=18" + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.2.tgz", - "integrity": "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==", + "node_modules/@parcel/watcher/node_modules/detect-libc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", + "integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "peer": true, + "bin": { + "detect-libc": "bin/detect-libc.js" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.53.tgz", + "integrity": "sha512-vENRlFU4YbrwVqNDZ7fLvy+JR1CRkyr01jhSiDpE1u6py3OMzQfztQU2jxykW3ALNxO4kSlqIDeYyD0Y9RcQeQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.53.5.tgz", + "integrity": "sha512-iDGS/h7D8t7tvZ1t6+WPK04KD0MwzLZrG0se1hzBjSi5fyxlsiggoJHwh18PCFNn7tG43OWb6pdZ6Y+rMlmyNQ==", "cpu": [ - "arm64" + "arm" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } + "android" + ] }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.2.tgz", - "integrity": "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==", + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.53.5.tgz", + "integrity": "sha512-wrSAViWvZHBMMlWk6EJhvg8/rjxzyEhEdgfMMjREHEq11EtJ6IP6yfcCH57YAEca2Oe3FNCE9DSTgU70EIGmVw==", "cpu": [ - "ia32" + "arm64" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } + "android" + ] }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.14.54", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.14.54.tgz", - "integrity": "sha512-bZBrLAIX1kpWelV0XemxBZllyRmM6vgFQQG2GdNb+r3Fkp0FOh1NJSvekXDs7jq70k4euu1cryLMfU+mTXlEpw==", + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.53.5.tgz", + "integrity": "sha512-S87zZPBmRO6u1YXQLwpveZm4JfPpAa6oHBX7/ghSiGH3rz/KDgAu1rKdGutV+WUI6tKDMbaBJomhnT30Y2t4VQ==", "cpu": [ - "loong64" + "arm64" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } + "darwin" + ] }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.2.tgz", - "integrity": "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==", + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.53.5.tgz", + "integrity": "sha512-YTbnsAaHo6VrAczISxgpTva8EkfQus0VPEVJCEaboHtZRIb6h6j0BNxRBOwnDciFTZLDPW5r+ZBmhL/+YpTZgA==", "cpu": [ - "mips64el" + "x64" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } + "darwin" + ] }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.2.tgz", - "integrity": "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==", + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.53.5.tgz", + "integrity": "sha512-1T8eY2J8rKJWzaznV7zedfdhD1BqVs1iqILhmHDq/bqCUZsrMt+j8VCTHhP0vdfbHK3e1IQ7VYx3jlKqwlf+vw==", "cpu": [ - "ppc64" + "arm64" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } + "freebsd" + ] }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.2.tgz", - "integrity": "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==", + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.53.5.tgz", + "integrity": "sha512-sHTiuXyBJApxRn+VFMaw1U+Qsz4kcNlxQ742snICYPrY+DDL8/ZbaC4DVIB7vgZmp3jiDaKA0WpBdP0aqPJoBQ==", "cpu": [ - "riscv64" + "x64" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } + "freebsd" + ] }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.2.tgz", - "integrity": "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==", + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.53.5.tgz", + "integrity": "sha512-dV3T9MyAf0w8zPVLVBptVlzaXxka6xg1f16VAQmjg+4KMSTWDvhimI/Y6mp8oHwNrmnmVl9XxJ/w/mO4uIQONA==", "cpu": [ - "s390x" + "arm" ], "dev": true, "license": "MIT", "optional": true, "os": [ "linux" - ], - "engines": { - "node": ">=18" - } + ] }, - "node_modules/@esbuild/linux-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.2.tgz", - "integrity": "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==", + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.53.5.tgz", + "integrity": "sha512-wIGYC1x/hyjP+KAu9+ewDI+fi5XSNiUi9Bvg6KGAh2TsNMA3tSEs+Sh6jJ/r4BV/bx/CyWu2ue9kDnIdRyafcQ==", "cpu": [ - "x64" + "arm" ], "dev": true, "license": "MIT", "optional": true, "os": [ "linux" - ], - "engines": { - "node": ">=18" - } + ] }, - "node_modules/@esbuild/netbsd-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.2.tgz", - "integrity": "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==", + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.53.5.tgz", + "integrity": "sha512-Y+qVA0D9d0y2FRNiG9oM3Hut/DgODZbU9I8pLLPwAsU0tUKZ49cyV1tzmB/qRbSzGvY8lpgGkJuMyuhH7Ma+Vg==", "cpu": [ "arm64" ], @@ -1463,84 +1125,111 @@ "license": "MIT", "optional": true, "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } + "linux" + ] }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.2.tgz", - "integrity": "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==", + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.53.5.tgz", + "integrity": "sha512-juaC4bEgJsyFVfqhtGLz8mbopaWD+WeSOYr5E16y+1of6KQjc0BpwZLuxkClqY1i8sco+MdyoXPNiCkQou09+g==", "cpu": [ - "x64" + "arm64" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } + "linux" + ] }, - "node_modules/@esbuild/openbsd-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.2.tgz", - "integrity": "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==", + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.53.5.tgz", + "integrity": "sha512-rIEC0hZ17A42iXtHX+EPJVL/CakHo+tT7W0pbzdAGuWOt2jxDFh7A/lRhsNHBcqL4T36+UiAgwO8pbmn3dE8wA==", "cpu": [ - "arm64" + "loong64" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "openbsd" + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.53.5.tgz", + "integrity": "sha512-T7l409NhUE552RcAOcmJHj3xyZ2h7vMWzcwQI0hvn5tqHh3oSoclf9WgTl+0QqffWFG8MEVZZP1/OBglKZx52Q==", + "cpu": [ + "ppc64" ], - "engines": { - "node": ">=18" - } + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.2.tgz", - "integrity": "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==", + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.53.5.tgz", + "integrity": "sha512-7OK5/GhxbnrMcxIFoYfhV/TkknarkYC1hqUw1wU2xUN3TVRLNT5FmBv4KkheSG2xZ6IEbRAhTooTV2+R5Tk0lQ==", "cpu": [ - "x64" + "riscv64" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "openbsd" + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.53.5.tgz", + "integrity": "sha512-GwuDBE/PsXaTa76lO5eLJTyr2k8QkPipAyOrs4V/KJufHCZBJ495VCGJol35grx9xryk4V+2zd3Ri+3v7NPh+w==", + "cpu": [ + "riscv64" ], - "engines": { - "node": ">=18" - } + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/@esbuild/openharmony-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.2.tgz", - "integrity": "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==", + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.53.5.tgz", + "integrity": "sha512-IAE1Ziyr1qNfnmiQLHBURAD+eh/zH1pIeJjeShleII7Vj8kyEm2PF77o+lf3WTHDpNJcu4IXJxNO0Zluro8bOw==", "cpu": [ - "arm64" + "s390x" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "openharmony" + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.53.5.tgz", + "integrity": "sha512-Pg6E+oP7GvZ4XwgRJBuSXZjcqpIW3yCBhK4BcsANvb47qMvAbCjR6E+1a/U2WXz1JJxp9/4Dno3/iSJLcm5auw==", + "cpu": [ + "x64" ], - "engines": { - "node": ">=18" - } + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.2.tgz", - "integrity": "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==", + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.53.5.tgz", + "integrity": "sha512-txGtluxDKTxaMDzUduGP0wdfng24y1rygUMnmlUJ88fzCCULCLn7oE5kb2+tRB+MWq1QDZT6ObT5RrR8HFRKqg==", "cpu": [ "x64" ], @@ -1548,16 +1237,27 @@ "license": "MIT", "optional": true, "os": [ - "sunos" + "linux" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.53.5.tgz", + "integrity": "sha512-3DFiLPnTxiOQV993fMc+KO8zXHTcIjgaInrqlG8zDp1TlhYl6WgrOHuJkJQ6M8zHEcntSJsUp1XFZSY8C1DYbg==", + "cpu": [ + "arm64" ], - "engines": { - "node": ">=18" - } + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.2.tgz", - "integrity": "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==", + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.53.5.tgz", + "integrity": "sha512-nggc/wPpNTgjGg75hu+Q/3i32R00Lq1B6N1DO7MCU340MRKL3WZJMjA9U4K4gzy3dkZPXm9E1Nc81FItBVGRlA==", "cpu": [ "arm64" ], @@ -1566,15 +1266,12 @@ "optional": true, "os": [ "win32" - ], - "engines": { - "node": ">=18" - } + ] }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.2.tgz", - "integrity": "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==", + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.53.5.tgz", + "integrity": "sha512-U/54pTbdQpPLBdEzCT6NBCFAfSZMvmjr0twhnD9f4EIvlm9wy3jjQ38yQj1AGznrNO65EWQMgm/QUjuIVrYF9w==", "cpu": [ "ia32" ], @@ -1583,15 +1280,12 @@ "optional": true, "os": [ "win32" - ], - "engines": { - "node": ">=18" - } + ] }, - "node_modules/@esbuild/win32-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.2.tgz", - "integrity": "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==", + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.53.5.tgz", + "integrity": "sha512-2NqKgZSuLH9SXBBV2dWNRCZmocgSOx8OJSdpRaEcRlIfX8YrKxUT6z0F1NpvDVhOsl190UFTRh2F2WDWWCYp3A==", "cpu": [ "x64" ], @@ -1600,302 +1294,158 @@ "optional": true, "os": [ "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.53.5.tgz", + "integrity": "sha512-JRpZUhCfhZ4keB5v0fe02gQJy05GqboPOaxvjugW04RLSYYoB/9t2lx2u/tMs/Na/1NXfY8QYjgRljRpN+MjTQ==", + "cpu": [ + "x64" ], - "engines": { - "node": ">=18" - } + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] }, - "node_modules/@gameap/debug": { - "version": "0.2.17", - "resolved": "https://registry.npmjs.org/@gameap/debug/-/debug-0.2.17.tgz", - "integrity": "sha512-yleZ/Spo8gAW0SztLTaB81RJj+HjS0jjr+4Bk9xCmsn75ryStwYdXKk+gMrLrnSYn4i9QtYQgvBXw8W+mm4NGg==", + "node_modules/@socket.io/component-emitter": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", + "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", "dev": true, - "dependencies": { - "@gameap/frontend": "4.1.0-dev8", - "@gameap/plugin-sdk": "^0.2.0", - "@originjs/vite-plugin-commonjs": "^1.0.3", - "@tailwindcss/postcss": "^4.1.0", - "@vitejs/plugin-vue": "^6.0.1", - "msw": "^2.7.0", - "postcss-preset-env": "^9.5.0", - "sass": "^1.32.0", - "tailwindcss": "^4.1.0", - "vite": "^7.1.0" - }, - "bin": { - "gameap-debug": "bin/cli.js" - }, - "peerDependencies": { - "axios": "^1.12.0", - "naive-ui": "^2.43.0", - "pinia": "^3.0.0", - "vue": "^3.5.0", - "vue-router": "^4.6.0" - } + "license": "MIT" }, - "node_modules/@gameap/frontend": { - "version": "4.1.0-dev8", - "resolved": "https://registry.npmjs.org/@gameap/frontend/-/frontend-4.1.0-dev8.tgz", - "integrity": "sha512-+m+UbeJmO8ttnazOIVXxsghcv93cjz5fDO3sp53WC47mlzsWCsJ/u4Ah1eE0Pgh4b7hUtcTIt1NzOYTS0qJo+w==", + "node_modules/@tailwindcss/node": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.18.tgz", + "integrity": "sha512-DoR7U1P7iYhw16qJ49fgXUlry1t4CpXeErJHnQ44JgTSKMaZUdf17cfn5mHchfJ4KRBZRFA/Coo+MUF5+gOaCQ==", "dev": true, + "license": "MIT", "dependencies": { - "@gameap/plugin-sdk": "^0.2.0" - }, - "peerDependencies": { - "axios": "^1.12.0", - "naive-ui": "^2.43.0", - "pinia": "^3.0.0", - "vue": "^3.5.0", - "vue-router": "^4.6.0" + "@jridgewell/remapping": "^2.3.4", + "enhanced-resolve": "^5.18.3", + "jiti": "^2.6.1", + "lightningcss": "1.30.2", + "magic-string": "^0.30.21", + "source-map-js": "^1.2.1", + "tailwindcss": "4.1.18" } }, - "node_modules/@gameap/plugin-sdk": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/@gameap/plugin-sdk/-/plugin-sdk-0.2.0.tgz", - "integrity": "sha512-QyIkSS6hBhYvWiGin2PhIb/EDiqlLzljZVXbQUg/oLjUVoGeAOFuQ8c7pdAk6BRCYLJQYCVeUQYaRwvPN3KsUA==", + "node_modules/@tailwindcss/oxide": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.18.tgz", + "integrity": "sha512-EgCR5tTS5bUSKQgzeMClT6iCY3ToqE1y+ZB0AKldj809QXk1Y+3jB0upOYZrn9aGIzPtUsP7sX4QQ4XtjBB95A==", + "dev": true, "license": "MIT", - "peerDependencies": { - "vue": "^3.5.0" + "engines": { + "node": ">= 10" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.1.18", + "@tailwindcss/oxide-darwin-arm64": "4.1.18", + "@tailwindcss/oxide-darwin-x64": "4.1.18", + "@tailwindcss/oxide-freebsd-x64": "4.1.18", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.18", + "@tailwindcss/oxide-linux-arm64-gnu": "4.1.18", + "@tailwindcss/oxide-linux-arm64-musl": "4.1.18", + "@tailwindcss/oxide-linux-x64-gnu": "4.1.18", + "@tailwindcss/oxide-linux-x64-musl": "4.1.18", + "@tailwindcss/oxide-wasm32-wasi": "4.1.18", + "@tailwindcss/oxide-win32-arm64-msvc": "4.1.18", + "@tailwindcss/oxide-win32-x64-msvc": "4.1.18" } }, - "node_modules/@inquirer/ansi": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@inquirer/ansi/-/ansi-1.0.2.tgz", - "integrity": "sha512-S8qNSZiYzFd0wAcyG5AXCvUHC5Sr7xpZ9wZ2py9XR88jUz8wooStVx5M6dRzczbBWjic9NP7+rY0Xi7qqK/aMQ==", + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.18.tgz", + "integrity": "sha512-dJHz7+Ugr9U/diKJA0W6N/6/cjI+ZTAoxPf9Iz9BFRF2GzEX8IvXxFIi/dZBloVJX/MZGvRuFA9rqwdiIEZQ0Q==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", + "optional": true, + "os": [ + "android" + ], "engines": { - "node": ">=18" + "node": ">= 10" } }, - "node_modules/@inquirer/confirm": { - "version": "5.1.21", - "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-5.1.21.tgz", - "integrity": "sha512-KR8edRkIsUayMXV+o3Gv+q4jlhENF9nMYUZs9PA2HzrXeHI8M5uDag70U7RJn9yyiMZSbtF5/UexBtAVtZGSbQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@inquirer/core": "^10.3.2", - "@inquirer/type": "^3.0.10" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } - } - }, - "node_modules/@inquirer/core": { - "version": "10.3.2", - "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.3.2.tgz", - "integrity": "sha512-43RTuEbfP8MbKzedNqBrlhhNKVwoK//vUFNW3Q3vZ88BLcrs4kYpGg+B2mm5p2K/HfygoCxuKwJJiv8PbGmE0A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@inquirer/ansi": "^1.0.2", - "@inquirer/figures": "^1.0.15", - "@inquirer/type": "^3.0.10", - "cli-width": "^4.1.0", - "mute-stream": "^2.0.0", - "signal-exit": "^4.1.0", - "wrap-ansi": "^6.2.0", - "yoctocolors-cjs": "^2.1.3" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } - } - }, - "node_modules/@inquirer/figures": { - "version": "1.0.15", - "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.15.tgz", - "integrity": "sha512-t2IEY+unGHOzAaVM5Xx6DEWKeXlDDcNPeDyUpsRc6CUhBfU3VQOEl+Vssh7VNp1dR8MdUJBWhuObjXCsVpjN5g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/@inquirer/type": { - "version": "3.0.10", - "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.10.tgz", - "integrity": "sha512-BvziSRxfz5Ov8ch0z/n3oijRSEcEsHnhggm4xFZe93DHcUCTlutlq9Ox4SVENAfcRD22UQq7T/atg9Wr3k09eA==", + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.18.tgz", + "integrity": "sha512-Gc2q4Qhs660bhjyBSKgq6BYvwDz4G+BuyJ5H1xfhmDR3D8HnHCmT/BSkvSL0vQLy/nkMLY20PQ2OoYMO15Jd0A==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } - } - }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.13", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", - "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0", - "@jridgewell/trace-mapping": "^0.3.24" - } - }, - "node_modules/@jridgewell/remapping": { - "version": "2.3.5", - "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", - "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" + "node": ">= 10" } }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.18.tgz", + "integrity": "sha512-FL5oxr2xQsFrc3X9o1fjHKBYBMD1QZNyc1Xzw/h5Qu4XnEBi3dZn96HcHm41c/euGV+GRiXFfh2hUCyKi/e+yw==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", - "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", - "license": "MIT" - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.31", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", - "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" + "node": ">= 10" } }, - "node_modules/@juggle/resize-observer": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/@juggle/resize-observer/-/resize-observer-3.4.0.tgz", - "integrity": "sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA==", - "dev": true, - "license": "Apache-2.0" - }, - "node_modules/@mswjs/interceptors": { - "version": "0.40.0", - "resolved": "https://registry.npmjs.org/@mswjs/interceptors/-/interceptors-0.40.0.tgz", - "integrity": "sha512-EFd6cVbHsgLa6wa4RljGj6Wk75qoHxUSyc5asLyyPSyuhIcdS2Q3Phw6ImS1q+CkALthJRShiYfKANcQMuMqsQ==", + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.18.tgz", + "integrity": "sha512-Fj+RHgu5bDodmV1dM9yAxlfJwkkWvLiRjbhuO2LEtwtlYlBgiAT4x/j5wQr1tC3SANAgD+0YcmWVrj8R9trVMA==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@open-draft/deferred-promise": "^2.2.0", - "@open-draft/logger": "^0.3.0", - "@open-draft/until": "^2.0.0", - "is-node-process": "^1.2.0", - "outvariant": "^1.4.3", - "strict-event-emitter": "^0.5.1" - }, + "optional": true, + "os": [ + "freebsd" + ], "engines": { - "node": ">=18" - } - }, - "node_modules/@open-draft/deferred-promise": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@open-draft/deferred-promise/-/deferred-promise-2.2.0.tgz", - "integrity": "sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@open-draft/logger": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@open-draft/logger/-/logger-0.3.0.tgz", - "integrity": "sha512-X2g45fzhxH238HKO4xbSr7+wBS8Fvw6ixhTDuvLd5mqh6bJJCFAPwU9mPDxbcrRtfxv4u5IHCEH77BmxvXmmxQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-node-process": "^1.2.0", - "outvariant": "^1.4.0" - } - }, - "node_modules/@open-draft/until": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@open-draft/until/-/until-2.1.0.tgz", - "integrity": "sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@originjs/vite-plugin-commonjs": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@originjs/vite-plugin-commonjs/-/vite-plugin-commonjs-1.0.3.tgz", - "integrity": "sha512-KuEXeGPptM2lyxdIEJ4R11+5ztipHoE7hy8ClZt3PYaOVQ/pyngd2alaSrPnwyFeOW1UagRBaQ752aA1dTMdOQ==", - "dev": true, - "license": "MulanPSL2", - "dependencies": { - "esbuild": "^0.14.14" + "node": ">= 10" } }, - "node_modules/@parcel/watcher": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.1.tgz", - "integrity": "sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==", + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.18.tgz", + "integrity": "sha512-Fp+Wzk/Ws4dZn+LV2Nqx3IilnhH51YZoRaYHQsVq3RQvEl+71VGKFpkfHrLM/Li+kt5c0DJe/bHXK1eHgDmdiA==", + "cpu": [ + "arm" + ], "dev": true, - "hasInstallScript": true, "license": "MIT", "optional": true, - "dependencies": { - "detect-libc": "^1.0.3", - "is-glob": "^4.0.3", - "micromatch": "^4.0.5", - "node-addon-api": "^7.0.0" - }, + "os": [ + "linux" + ], "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - }, - "optionalDependencies": { - "@parcel/watcher-android-arm64": "2.5.1", - "@parcel/watcher-darwin-arm64": "2.5.1", - "@parcel/watcher-darwin-x64": "2.5.1", - "@parcel/watcher-freebsd-x64": "2.5.1", - "@parcel/watcher-linux-arm-glibc": "2.5.1", - "@parcel/watcher-linux-arm-musl": "2.5.1", - "@parcel/watcher-linux-arm64-glibc": "2.5.1", - "@parcel/watcher-linux-arm64-musl": "2.5.1", - "@parcel/watcher-linux-x64-glibc": "2.5.1", - "@parcel/watcher-linux-x64-musl": "2.5.1", - "@parcel/watcher-win32-arm64": "2.5.1", - "@parcel/watcher-win32-ia32": "2.5.1", - "@parcel/watcher-win32-x64": "2.5.1" + "node": ">= 10" } }, - "node_modules/@parcel/watcher-android-arm64": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.1.tgz", - "integrity": "sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==", + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.18.tgz", + "integrity": "sha512-S0n3jboLysNbh55Vrt7pk9wgpyTTPD0fdQeh7wQfMqLPM/Hrxi+dVsLsPrycQjGKEQk85Kgbx+6+QnYNiHalnw==", "cpu": [ "arm64" ], @@ -1903,20 +1453,16 @@ "license": "MIT", "optional": true, "os": [ - "android" + "linux" ], "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "node": ">= 10" } }, - "node_modules/@parcel/watcher-darwin-arm64": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.1.tgz", - "integrity": "sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw==", + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.18.tgz", + "integrity": "sha512-1px92582HkPQlaaCkdRcio71p8bc8i/ap5807tPRDK/uw953cauQBT8c5tVGkOwrHMfc2Yh6UuxaH4vtTjGvHg==", "cpu": [ "arm64" ], @@ -1924,20 +1470,16 @@ "license": "MIT", "optional": true, "os": [ - "darwin" + "linux" ], "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "node": ">= 10" } }, - "node_modules/@parcel/watcher-darwin-x64": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.1.tgz", - "integrity": "sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg==", + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.18.tgz", + "integrity": "sha512-v3gyT0ivkfBLoZGF9LyHmts0Isc8jHZyVcbzio6Wpzifg/+5ZJpDiRiUhDLkcr7f/r38SWNe7ucxmGW3j3Kb/g==", "cpu": [ "x64" ], @@ -1945,20 +1487,16 @@ "license": "MIT", "optional": true, "os": [ - "darwin" + "linux" ], "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "node": ">= 10" } }, - "node_modules/@parcel/watcher-freebsd-x64": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.1.tgz", - "integrity": "sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ==", + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.18.tgz", + "integrity": "sha512-bhJ2y2OQNlcRwwgOAGMY0xTFStt4/wyU6pvI6LSuZpRgKQwxTec0/3Scu91O8ir7qCR3AuepQKLU/kX99FouqQ==", "cpu": [ "x64" ], @@ -1966,2029 +1504,525 @@ "license": "MIT", "optional": true, "os": [ - "freebsd" + "linux" ], "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "node": ">= 10" } }, - "node_modules/@parcel/watcher-linux-arm-glibc": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.1.tgz", - "integrity": "sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA==", - "cpu": [ - "arm" + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.18.tgz", + "integrity": "sha512-LffYTvPjODiP6PT16oNeUQJzNVyJl1cjIebq/rWWBF+3eDst5JGEFSc5cWxyRCJ0Mxl+KyIkqRxk1XPEs9x8TA==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" ], "dev": true, "license": "MIT", "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10.0.0" + "dependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1", + "@emnapi/wasi-threads": "^1.1.0", + "@napi-rs/wasm-runtime": "^1.1.0", + "@tybys/wasm-util": "^0.10.1", + "tslib": "^2.4.0" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "engines": { + "node": ">=14.0.0" } }, - "node_modules/@parcel/watcher-linux-arm-musl": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.1.tgz", - "integrity": "sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==", + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.18.tgz", + "integrity": "sha512-HjSA7mr9HmC8fu6bdsZvZ+dhjyGCLdotjVOgLA2vEqxEBZaQo9YTX4kwgEvPCpRh8o4uWc4J/wEoFzhEmjvPbA==", "cpu": [ - "arm" + "arm64" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "linux" + "win32" ], "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "node": ">= 10" } }, - "node_modules/@parcel/watcher-linux-arm64-glibc": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.1.tgz", - "integrity": "sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==", + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.18.tgz", + "integrity": "sha512-bJWbyYpUlqamC8dpR7pfjA0I7vdF6t5VpUGMWRkXVE3AXgIZjYUYAK7II1GNaxR8J1SSrSrppRar8G++JekE3Q==", "cpu": [ - "arm64" + "x64" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "linux" + "win32" ], "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "node": ">= 10" } }, - "node_modules/@parcel/watcher-linux-arm64-musl": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.1.tgz", - "integrity": "sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==", - "cpu": [ - "arm64" - ], + "node_modules/@tailwindcss/postcss": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.1.18.tgz", + "integrity": "sha512-Ce0GFnzAOuPyfV5SxjXGn0CubwGcuDB0zcdaPuCSzAa/2vII24JTkH+I6jcbXLb1ctjZMZZI6OjDaLPJQL1S0g==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "@tailwindcss/node": "4.1.18", + "@tailwindcss/oxide": "4.1.18", + "postcss": "^8.4.41", + "tailwindcss": "4.1.18" + } + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/cors": { + "version": "2.8.19", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz", + "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/lodash": { + "version": "4.17.25", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.25.tgz", + "integrity": "sha512-+K1NIO8I+F9/wNulfVvu23QYd0Pe9/OCqRrim4NoYIf1VoEDL90Ve4ClzpyqBLc7NpGGWRvYNCKZ1BE/Jpf8dQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/lodash-es": { + "version": "4.17.12", + "resolved": "https://registry.npmjs.org/@types/lodash-es/-/lodash-es-4.17.12.tgz", + "integrity": "sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/lodash": "*" + } + }, + "node_modules/@types/node": { + "version": "25.0.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.3.tgz", + "integrity": "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/@vitejs/plugin-vue": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-6.0.3.tgz", + "integrity": "sha512-TlGPkLFLVOY3T7fZrwdvKpjprR3s4fxRln0ORDo1VQ7HHyxJwTlrjKU3kpVWTlaAjIEuCTokmjkZnr8Tpc925w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rolldown/pluginutils": "1.0.0-beta.53" + }, "engines": { - "node": ">= 10.0.0" + "node": "^20.19.0 || >=22.12.0" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "peerDependencies": { + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0", + "vue": "^3.2.25" } }, - "node_modules/@parcel/watcher-linux-x64-glibc": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.1.tgz", - "integrity": "sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==", - "cpu": [ - "x64" - ], + "node_modules/@vitest/expect": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.7.tgz", + "integrity": "sha512-E8eBXaKibuvH2pSZErOjdVb5vF4PbKYcrnluBTYxEk1l/VhhwZg1kZQsdtjq+CsF5CFydf2Rdkz7jDHKSisi3w==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10.0.0" + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/spy": "3.2.7", + "@vitest/utils": "3.2.7", + "chai": "^5.2.0", + "tinyrainbow": "^2.0.0" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "url": "https://opencollective.com/vitest" } }, - "node_modules/@parcel/watcher-linux-x64-musl": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.1.tgz", - "integrity": "sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==", - "cpu": [ - "x64" - ], + "node_modules/@vitest/mocker": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.7.tgz", + "integrity": "sha512-Trr0hYO9CM3Wj6ksWHRhK9IZpIY6wTMO5u/MqXurMxT57sWBaOPEtP3Oq60ihZuh5JsiagKfz95OcxdEP6dBrA==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10.0.0" + "dependencies": { + "@vitest/spy": "3.2.7", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.17" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } } }, - "node_modules/@parcel/watcher-win32-arm64": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.1.tgz", - "integrity": "sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==", - "cpu": [ - "arm64" - ], + "node_modules/@vitest/mocker/node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10.0.0" + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/@vitest/pretty-format": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.7.tgz", + "integrity": "sha512-KUHlwqVu0sRlhCdyPdQ/wBoTfRahjUky1MubOmYw9fWfIZy1gNoHpuaaQBPAaMaVYdQYHJLurzj8ECCj5OwTqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^2.0.0" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "url": "https://opencollective.com/vitest" } }, - "node_modules/@parcel/watcher-win32-ia32": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.1.tgz", - "integrity": "sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ==", - "cpu": [ - "ia32" - ], + "node_modules/@vitest/runner": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.7.tgz", + "integrity": "sha512-sB9y4ovltoQP+WaUPwmSxO9WIg9Ig694Di5PalVPsYHklAdE027mehpWF2SQSVq+k6sFgaivbTjTJwZLSHbedA==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10.0.0" + "dependencies": { + "@vitest/utils": "3.2.7", + "pathe": "^2.0.3", + "strip-literal": "^3.0.0" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "url": "https://opencollective.com/vitest" } }, - "node_modules/@parcel/watcher-win32-x64": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.1.tgz", - "integrity": "sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA==", - "cpu": [ - "x64" - ], + "node_modules/@vitest/snapshot": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.7.tgz", + "integrity": "sha512-7C+MwShwtBSI5Buwoyg3s/iY1eHL9PKAf+O1wVh/TdnjXUtkoL/9YQtre90i4MtNXM6edP1wJ2zOBpfCyhIS7g==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10.0.0" + "dependencies": { + "@vitest/pretty-format": "3.2.7", + "magic-string": "^0.30.17", + "pathe": "^2.0.3" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "url": "https://opencollective.com/vitest" } }, - "node_modules/@parcel/watcher/node_modules/detect-libc": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", - "integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==", + "node_modules/@vitest/spy": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.7.tgz", + "integrity": "sha512-Q2eQGI6d2L/hBtZ0qNuKcAGid68XK6cv1xsoaIma6PaJhHPoqcEJhYpXZ/5myCMqkNgtP6UKuBhbc0nHKnrkuQ==", "dev": true, - "license": "Apache-2.0", - "optional": true, - "bin": { - "detect-libc": "bin/detect-libc.js" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/@rolldown/pluginutils": { - "version": "1.0.0-beta.53", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.53.tgz", - "integrity": "sha512-vENRlFU4YbrwVqNDZ7fLvy+JR1CRkyr01jhSiDpE1u6py3OMzQfztQU2jxykW3ALNxO4kSlqIDeYyD0Y9RcQeQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.53.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.53.5.tgz", - "integrity": "sha512-iDGS/h7D8t7tvZ1t6+WPK04KD0MwzLZrG0se1hzBjSi5fyxlsiggoJHwh18PCFNn7tG43OWb6pdZ6Y+rMlmyNQ==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-android-arm64": { - "version": "4.53.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.53.5.tgz", - "integrity": "sha512-wrSAViWvZHBMMlWk6EJhvg8/rjxzyEhEdgfMMjREHEq11EtJ6IP6yfcCH57YAEca2Oe3FNCE9DSTgU70EIGmVw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.53.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.53.5.tgz", - "integrity": "sha512-S87zZPBmRO6u1YXQLwpveZm4JfPpAa6oHBX7/ghSiGH3rz/KDgAu1rKdGutV+WUI6tKDMbaBJomhnT30Y2t4VQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.53.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.53.5.tgz", - "integrity": "sha512-YTbnsAaHo6VrAczISxgpTva8EkfQus0VPEVJCEaboHtZRIb6h6j0BNxRBOwnDciFTZLDPW5r+ZBmhL/+YpTZgA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.53.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.53.5.tgz", - "integrity": "sha512-1T8eY2J8rKJWzaznV7zedfdhD1BqVs1iqILhmHDq/bqCUZsrMt+j8VCTHhP0vdfbHK3e1IQ7VYx3jlKqwlf+vw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.53.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.53.5.tgz", - "integrity": "sha512-sHTiuXyBJApxRn+VFMaw1U+Qsz4kcNlxQ742snICYPrY+DDL8/ZbaC4DVIB7vgZmp3jiDaKA0WpBdP0aqPJoBQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.53.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.53.5.tgz", - "integrity": "sha512-dV3T9MyAf0w8zPVLVBptVlzaXxka6xg1f16VAQmjg+4KMSTWDvhimI/Y6mp8oHwNrmnmVl9XxJ/w/mO4uIQONA==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.53.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.53.5.tgz", - "integrity": "sha512-wIGYC1x/hyjP+KAu9+ewDI+fi5XSNiUi9Bvg6KGAh2TsNMA3tSEs+Sh6jJ/r4BV/bx/CyWu2ue9kDnIdRyafcQ==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.53.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.53.5.tgz", - "integrity": "sha512-Y+qVA0D9d0y2FRNiG9oM3Hut/DgODZbU9I8pLLPwAsU0tUKZ49cyV1tzmB/qRbSzGvY8lpgGkJuMyuhH7Ma+Vg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.53.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.53.5.tgz", - "integrity": "sha512-juaC4bEgJsyFVfqhtGLz8mbopaWD+WeSOYr5E16y+1of6KQjc0BpwZLuxkClqY1i8sco+MdyoXPNiCkQou09+g==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.53.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.53.5.tgz", - "integrity": "sha512-rIEC0hZ17A42iXtHX+EPJVL/CakHo+tT7W0pbzdAGuWOt2jxDFh7A/lRhsNHBcqL4T36+UiAgwO8pbmn3dE8wA==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.53.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.53.5.tgz", - "integrity": "sha512-T7l409NhUE552RcAOcmJHj3xyZ2h7vMWzcwQI0hvn5tqHh3oSoclf9WgTl+0QqffWFG8MEVZZP1/OBglKZx52Q==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.53.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.53.5.tgz", - "integrity": "sha512-7OK5/GhxbnrMcxIFoYfhV/TkknarkYC1hqUw1wU2xUN3TVRLNT5FmBv4KkheSG2xZ6IEbRAhTooTV2+R5Tk0lQ==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.53.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.53.5.tgz", - "integrity": "sha512-GwuDBE/PsXaTa76lO5eLJTyr2k8QkPipAyOrs4V/KJufHCZBJ495VCGJol35grx9xryk4V+2zd3Ri+3v7NPh+w==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.53.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.53.5.tgz", - "integrity": "sha512-IAE1Ziyr1qNfnmiQLHBURAD+eh/zH1pIeJjeShleII7Vj8kyEm2PF77o+lf3WTHDpNJcu4IXJxNO0Zluro8bOw==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.53.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.53.5.tgz", - "integrity": "sha512-Pg6E+oP7GvZ4XwgRJBuSXZjcqpIW3yCBhK4BcsANvb47qMvAbCjR6E+1a/U2WXz1JJxp9/4Dno3/iSJLcm5auw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.53.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.53.5.tgz", - "integrity": "sha512-txGtluxDKTxaMDzUduGP0wdfng24y1rygUMnmlUJ88fzCCULCLn7oE5kb2+tRB+MWq1QDZT6ObT5RrR8HFRKqg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.53.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.53.5.tgz", - "integrity": "sha512-3DFiLPnTxiOQV993fMc+KO8zXHTcIjgaInrqlG8zDp1TlhYl6WgrOHuJkJQ6M8zHEcntSJsUp1XFZSY8C1DYbg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ] - }, - "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.53.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.53.5.tgz", - "integrity": "sha512-nggc/wPpNTgjGg75hu+Q/3i32R00Lq1B6N1DO7MCU340MRKL3WZJMjA9U4K4gzy3dkZPXm9E1Nc81FItBVGRlA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.53.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.53.5.tgz", - "integrity": "sha512-U/54pTbdQpPLBdEzCT6NBCFAfSZMvmjr0twhnD9f4EIvlm9wy3jjQ38yQj1AGznrNO65EWQMgm/QUjuIVrYF9w==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.53.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.53.5.tgz", - "integrity": "sha512-2NqKgZSuLH9SXBBV2dWNRCZmocgSOx8OJSdpRaEcRlIfX8YrKxUT6z0F1NpvDVhOsl190UFTRh2F2WDWWCYp3A==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.53.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.53.5.tgz", - "integrity": "sha512-JRpZUhCfhZ4keB5v0fe02gQJy05GqboPOaxvjugW04RLSYYoB/9t2lx2u/tMs/Na/1NXfY8QYjgRljRpN+MjTQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@socket.io/component-emitter": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", - "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@tailwindcss/node": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.18.tgz", - "integrity": "sha512-DoR7U1P7iYhw16qJ49fgXUlry1t4CpXeErJHnQ44JgTSKMaZUdf17cfn5mHchfJ4KRBZRFA/Coo+MUF5+gOaCQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/remapping": "^2.3.4", - "enhanced-resolve": "^5.18.3", - "jiti": "^2.6.1", - "lightningcss": "1.30.2", - "magic-string": "^0.30.21", - "source-map-js": "^1.2.1", - "tailwindcss": "4.1.18" - } - }, - "node_modules/@tailwindcss/oxide": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.18.tgz", - "integrity": "sha512-EgCR5tTS5bUSKQgzeMClT6iCY3ToqE1y+ZB0AKldj809QXk1Y+3jB0upOYZrn9aGIzPtUsP7sX4QQ4XtjBB95A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 10" - }, - "optionalDependencies": { - "@tailwindcss/oxide-android-arm64": "4.1.18", - "@tailwindcss/oxide-darwin-arm64": "4.1.18", - "@tailwindcss/oxide-darwin-x64": "4.1.18", - "@tailwindcss/oxide-freebsd-x64": "4.1.18", - "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.18", - "@tailwindcss/oxide-linux-arm64-gnu": "4.1.18", - "@tailwindcss/oxide-linux-arm64-musl": "4.1.18", - "@tailwindcss/oxide-linux-x64-gnu": "4.1.18", - "@tailwindcss/oxide-linux-x64-musl": "4.1.18", - "@tailwindcss/oxide-wasm32-wasi": "4.1.18", - "@tailwindcss/oxide-win32-arm64-msvc": "4.1.18", - "@tailwindcss/oxide-win32-x64-msvc": "4.1.18" - } - }, - "node_modules/@tailwindcss/oxide-android-arm64": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.18.tgz", - "integrity": "sha512-dJHz7+Ugr9U/diKJA0W6N/6/cjI+ZTAoxPf9Iz9BFRF2GzEX8IvXxFIi/dZBloVJX/MZGvRuFA9rqwdiIEZQ0Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-darwin-arm64": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.18.tgz", - "integrity": "sha512-Gc2q4Qhs660bhjyBSKgq6BYvwDz4G+BuyJ5H1xfhmDR3D8HnHCmT/BSkvSL0vQLy/nkMLY20PQ2OoYMO15Jd0A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-darwin-x64": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.18.tgz", - "integrity": "sha512-FL5oxr2xQsFrc3X9o1fjHKBYBMD1QZNyc1Xzw/h5Qu4XnEBi3dZn96HcHm41c/euGV+GRiXFfh2hUCyKi/e+yw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-freebsd-x64": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.18.tgz", - "integrity": "sha512-Fj+RHgu5bDodmV1dM9yAxlfJwkkWvLiRjbhuO2LEtwtlYlBgiAT4x/j5wQr1tC3SANAgD+0YcmWVrj8R9trVMA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.18.tgz", - "integrity": "sha512-Fp+Wzk/Ws4dZn+LV2Nqx3IilnhH51YZoRaYHQsVq3RQvEl+71VGKFpkfHrLM/Li+kt5c0DJe/bHXK1eHgDmdiA==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.18.tgz", - "integrity": "sha512-S0n3jboLysNbh55Vrt7pk9wgpyTTPD0fdQeh7wQfMqLPM/Hrxi+dVsLsPrycQjGKEQk85Kgbx+6+QnYNiHalnw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-linux-arm64-musl": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.18.tgz", - "integrity": "sha512-1px92582HkPQlaaCkdRcio71p8bc8i/ap5807tPRDK/uw953cauQBT8c5tVGkOwrHMfc2Yh6UuxaH4vtTjGvHg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-linux-x64-gnu": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.18.tgz", - "integrity": "sha512-v3gyT0ivkfBLoZGF9LyHmts0Isc8jHZyVcbzio6Wpzifg/+5ZJpDiRiUhDLkcr7f/r38SWNe7ucxmGW3j3Kb/g==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-linux-x64-musl": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.18.tgz", - "integrity": "sha512-bhJ2y2OQNlcRwwgOAGMY0xTFStt4/wyU6pvI6LSuZpRgKQwxTec0/3Scu91O8ir7qCR3AuepQKLU/kX99FouqQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.18.tgz", - "integrity": "sha512-LffYTvPjODiP6PT16oNeUQJzNVyJl1cjIebq/rWWBF+3eDst5JGEFSc5cWxyRCJ0Mxl+KyIkqRxk1XPEs9x8TA==", - "bundleDependencies": [ - "@napi-rs/wasm-runtime", - "@emnapi/core", - "@emnapi/runtime", - "@tybys/wasm-util", - "@emnapi/wasi-threads", - "tslib" - ], - "cpu": [ - "wasm32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/core": "^1.7.1", - "@emnapi/runtime": "^1.7.1", - "@emnapi/wasi-threads": "^1.1.0", - "@napi-rs/wasm-runtime": "^1.1.0", - "@tybys/wasm-util": "^0.10.1", - "tslib": "^2.4.0" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.18.tgz", - "integrity": "sha512-HjSA7mr9HmC8fu6bdsZvZ+dhjyGCLdotjVOgLA2vEqxEBZaQo9YTX4kwgEvPCpRh8o4uWc4J/wEoFzhEmjvPbA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-win32-x64-msvc": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.18.tgz", - "integrity": "sha512-bJWbyYpUlqamC8dpR7pfjA0I7vdF6t5VpUGMWRkXVE3AXgIZjYUYAK7II1GNaxR8J1SSrSrppRar8G++JekE3Q==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/postcss": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.1.18.tgz", - "integrity": "sha512-Ce0GFnzAOuPyfV5SxjXGn0CubwGcuDB0zcdaPuCSzAa/2vII24JTkH+I6jcbXLb1ctjZMZZI6OjDaLPJQL1S0g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@alloc/quick-lru": "^5.2.0", - "@tailwindcss/node": "4.1.18", - "@tailwindcss/oxide": "4.1.18", - "postcss": "^8.4.41", - "tailwindcss": "4.1.18" - } - }, - "node_modules/@types/cors": { - "version": "2.8.19", - "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz", - "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/estree": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", - "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/katex": { - "version": "0.16.7", - "resolved": "https://registry.npmjs.org/@types/katex/-/katex-0.16.7.tgz", - "integrity": "sha512-HMwFiRujE5PjrgwHQ25+bsLJgowjGjm5Z8FVSf0N6PwgJrwxH0QxzHYDcKsTfV3wva0vzrpqMTJS2jXPr5BMEQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-FOvQ0YPD5NOfPgMzJihoT+Za5pdkDJWcbpuj1DjaKZIr/gxodQjY/uWEFlTNqW2ugXHUiL8lRQgw63dzKHZdeQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/lodash-es": { - "version": "4.17.12", - "resolved": "https://registry.npmjs.org/@types/lodash-es/-/lodash-es-4.17.12.tgz", - "integrity": "sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/lodash": "*" - } - }, - "node_modules/@types/node": { - "version": "25.0.3", - "resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.3.tgz", - "integrity": "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "undici-types": "~7.16.0" - } - }, - "node_modules/@types/statuses": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/@types/statuses/-/statuses-2.0.6.tgz", - "integrity": "sha512-xMAgYwceFhRA2zY+XbEA7mxYbA093wdiW8Vu6gZPGWy9cmOyU9XesH1tNcEWsKFd5Vzrqx5T3D38PWx1FIIXkA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@vitejs/plugin-vue": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-6.0.3.tgz", - "integrity": "sha512-TlGPkLFLVOY3T7fZrwdvKpjprR3s4fxRln0ORDo1VQ7HHyxJwTlrjKU3kpVWTlaAjIEuCTokmjkZnr8Tpc925w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@rolldown/pluginutils": "1.0.0-beta.53" - }, - "engines": { - "node": "^20.19.0 || >=22.12.0" - }, - "peerDependencies": { - "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0", - "vue": "^3.2.25" - } - }, - "node_modules/@vue/compiler-core": { - "version": "3.5.26", - "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.26.tgz", - "integrity": "sha512-vXyI5GMfuoBCnv5ucIT7jhHKl55Y477yxP6fc4eUswjP8FG3FFVFd41eNDArR+Uk3QKn2Z85NavjaxLxOC19/w==", - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.28.5", - "@vue/shared": "3.5.26", - "entities": "^7.0.0", - "estree-walker": "^2.0.2", - "source-map-js": "^1.2.1" - } - }, - "node_modules/@vue/compiler-dom": { - "version": "3.5.26", - "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.26.tgz", - "integrity": "sha512-y1Tcd3eXs834QjswshSilCBnKGeQjQXB6PqFn/1nxcQw4pmG42G8lwz+FZPAZAby6gZeHSt/8LMPfZ4Rb+Bd/A==", - "license": "MIT", - "dependencies": { - "@vue/compiler-core": "3.5.26", - "@vue/shared": "3.5.26" - } - }, - "node_modules/@vue/compiler-sfc": { - "version": "3.5.26", - "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.26.tgz", - "integrity": "sha512-egp69qDTSEZcf4bGOSsprUr4xI73wfrY5oRs6GSgXFTiHrWj4Y3X5Ydtip9QMqiCMCPVwLglB9GBxXtTadJ3mA==", - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.28.5", - "@vue/compiler-core": "3.5.26", - "@vue/compiler-dom": "3.5.26", - "@vue/compiler-ssr": "3.5.26", - "@vue/shared": "3.5.26", - "estree-walker": "^2.0.2", - "magic-string": "^0.30.21", - "postcss": "^8.5.6", - "source-map-js": "^1.2.1" - } - }, - "node_modules/@vue/compiler-ssr": { - "version": "3.5.26", - "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.26.tgz", - "integrity": "sha512-lZT9/Y0nSIRUPVvapFJEVDbEXruZh2IYHMk2zTtEgJSlP5gVOqeWXH54xDKAaFS4rTnDeDBQUYDtxKyoW9FwDw==", - "license": "MIT", - "dependencies": { - "@vue/compiler-dom": "3.5.26", - "@vue/shared": "3.5.26" - } - }, - "node_modules/@vue/devtools-api": { - "version": "7.7.9", - "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-7.7.9.tgz", - "integrity": "sha512-kIE8wvwlcZ6TJTbNeU2HQNtaxLx3a84aotTITUuL/4bzfPxzajGBOoqjMhwZJ8L9qFYDU/lAYMEEm11dnZOD6g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vue/devtools-kit": "^7.7.9" - } - }, - "node_modules/@vue/devtools-kit": { - "version": "7.7.9", - "resolved": "https://registry.npmjs.org/@vue/devtools-kit/-/devtools-kit-7.7.9.tgz", - "integrity": "sha512-PyQ6odHSgiDVd4hnTP+aDk2X4gl2HmLDfiyEnn3/oV+ckFDuswRs4IbBT7vacMuGdwY/XemxBoh302ctbsptuA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vue/devtools-shared": "^7.7.9", - "birpc": "^2.3.0", - "hookable": "^5.5.3", - "mitt": "^3.0.1", - "perfect-debounce": "^1.0.0", - "speakingurl": "^14.0.1", - "superjson": "^2.2.2" - } - }, - "node_modules/@vue/devtools-shared": { - "version": "7.7.9", - "resolved": "https://registry.npmjs.org/@vue/devtools-shared/-/devtools-shared-7.7.9.tgz", - "integrity": "sha512-iWAb0v2WYf0QWmxCGy0seZNDPdO3Sp5+u78ORnyeonS6MT4PC7VPrryX2BpMJrwlDeaZ6BD4vP4XKjK0SZqaeA==", - "dev": true, - "license": "MIT", - "dependencies": { - "rfdc": "^1.4.1" - } - }, - "node_modules/@vue/reactivity": { - "version": "3.5.26", - "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.26.tgz", - "integrity": "sha512-9EnYB1/DIiUYYnzlnUBgwU32NNvLp/nhxLXeWRhHUEeWNTn1ECxX8aGO7RTXeX6PPcxe3LLuNBFoJbV4QZ+CFQ==", - "license": "MIT", - "dependencies": { - "@vue/shared": "3.5.26" - } - }, - "node_modules/@vue/runtime-core": { - "version": "3.5.26", - "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.26.tgz", - "integrity": "sha512-xJWM9KH1kd201w5DvMDOwDHYhrdPTrAatn56oB/LRG4plEQeZRQLw0Bpwih9KYoqmzaxF0OKSn6swzYi84e1/Q==", - "license": "MIT", - "dependencies": { - "@vue/reactivity": "3.5.26", - "@vue/shared": "3.5.26" - } - }, - "node_modules/@vue/runtime-dom": { - "version": "3.5.26", - "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.26.tgz", - "integrity": "sha512-XLLd/+4sPC2ZkN/6+V4O4gjJu6kSDbHAChvsyWgm1oGbdSO3efvGYnm25yCjtFm/K7rrSDvSfPDgN1pHgS4VNQ==", - "license": "MIT", - "dependencies": { - "@vue/reactivity": "3.5.26", - "@vue/runtime-core": "3.5.26", - "@vue/shared": "3.5.26", - "csstype": "^3.2.3" - } - }, - "node_modules/@vue/server-renderer": { - "version": "3.5.26", - "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.26.tgz", - "integrity": "sha512-TYKLXmrwWKSodyVuO1WAubucd+1XlLg4set0YoV+Hu8Lo79mp/YMwWV5mC5FgtsDxX3qo1ONrxFaTP1OQgy1uA==", - "license": "MIT", - "dependencies": { - "@vue/compiler-ssr": "3.5.26", - "@vue/shared": "3.5.26" - }, - "peerDependencies": { - "vue": "3.5.26" - } - }, - "node_modules/@vue/shared": { - "version": "3.5.26", - "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.26.tgz", - "integrity": "sha512-7Z6/y3uFI5PRoKeorTOSXKcDj0MSasfNNltcslbFrPpcw6aXRUALq4IfJlaTRspiWIUOEZbrpM+iQGmCOiWe4A==", - "license": "MIT" - }, - "node_modules/accepts": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", - "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", - "dev": true, - "license": "MIT", - "dependencies": { - "mime-types": "~2.1.34", - "negotiator": "0.6.3" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/anymatch": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", - "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", - "dev": true, - "license": "ISC", - "dependencies": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/async": { - "version": "2.6.4", - "resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz", - "integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==", - "dev": true, - "license": "MIT", - "dependencies": { - "lodash": "^4.17.14" - } - }, - "node_modules/async-each-series": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/async-each-series/-/async-each-series-0.1.1.tgz", - "integrity": "sha512-p4jj6Fws4Iy2m0iCmI2am2ZNZCgbdgE+P8F/8csmn2vx7ixXrO2zGcuNsD46X5uZSVecmkEy/M06X2vG8KD6dQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/async-validator": { - "version": "4.2.5", - "resolved": "https://registry.npmjs.org/async-validator/-/async-validator-4.2.5.tgz", - "integrity": "sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg==", - "dev": true, - "license": "MIT" - }, - "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, - "license": "MIT" - }, - "node_modules/autoprefixer": { - "version": "10.4.23", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.23.tgz", - "integrity": "sha512-YYTXSFulfwytnjAPlw8QHncHJmlvFKtczb8InXaAx9Q0LbfDnfEYDE55omerIJKihhmU61Ft+cAOSzQVaBUmeA==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/autoprefixer" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "browserslist": "^4.28.1", - "caniuse-lite": "^1.0.30001760", - "fraction.js": "^5.3.4", - "picocolors": "^1.1.1", - "postcss-value-parser": "^4.2.0" - }, - "bin": { - "autoprefixer": "bin/autoprefixer" - }, - "engines": { - "node": "^10 || ^12 || >=14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, - "node_modules/axios": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz", - "integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "follow-redirects": "^1.15.6", - "form-data": "^4.0.4", - "proxy-from-env": "^1.1.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, - "license": "MIT" - }, - "node_modules/base64id": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", - "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^4.5.0 || >= 5.9" - } - }, - "node_modules/baseline-browser-mapping": { - "version": "2.9.10", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.10.tgz", - "integrity": "sha512-2VIKvDx8Z1a9rTB2eCkdPE5nSe28XnA+qivGnWHoB40hMMt/h1hSz0960Zqsn6ZyxWXUie0EBdElKv8may20AA==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "baseline-browser-mapping": "dist/cli.js" - } - }, - "node_modules/batch": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz", - "integrity": "sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw==", - "dev": true, - "license": "MIT" - }, - "node_modules/binary-extensions": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", - "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/birpc": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/birpc/-/birpc-2.9.0.tgz", - "integrity": "sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/antfu" - } - }, - "node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/braces": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "dev": true, - "license": "MIT", - "dependencies": { - "fill-range": "^7.1.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/browser-sync": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/browser-sync/-/browser-sync-3.0.4.tgz", - "integrity": "sha512-mcYOIy4BW6sWSEnTSBjQwWsnbx2btZX78ajTTjdNfyC/EqQVcIe0nQR6894RNAMtvlfAnLaH9L2ka97zpvgenA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "browser-sync-client": "^3.0.4", - "browser-sync-ui": "^3.0.4", - "bs-recipes": "1.3.4", - "chalk": "4.1.2", - "chokidar": "^3.5.1", - "connect": "3.6.6", - "connect-history-api-fallback": "^1", - "dev-ip": "^1.0.1", - "easy-extender": "^2.3.4", - "eazy-logger": "^4.1.0", - "etag": "^1.8.1", - "fresh": "^0.5.2", - "fs-extra": "3.0.1", - "http-proxy": "^1.18.1", - "immutable": "^3", - "micromatch": "^4.0.8", - "opn": "5.3.0", - "portscanner": "2.2.0", - "raw-body": "^2.3.2", - "resp-modifier": "6.0.2", - "rx": "4.1.0", - "send": "^0.19.0", - "serve-index": "^1.9.1", - "serve-static": "^1.16.2", - "server-destroy": "1.0.1", - "socket.io": "^4.4.1", - "ua-parser-js": "^1.0.33", - "yargs": "^17.3.1" - }, - "bin": { - "browser-sync": "dist/bin.js" - }, - "engines": { - "node": ">= 8.0.0" - } - }, - "node_modules/browser-sync-client": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/browser-sync-client/-/browser-sync-client-3.0.4.tgz", - "integrity": "sha512-+ew5ubXzGRKVjquBL3u6najS40TG7GxCdyBll0qSRc/n+JRV9gb/yDdRL1IAgRHqjnJTdqeBKKIQabjvjRSYRQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "etag": "1.8.1", - "fresh": "0.5.2", - "mitt": "^1.1.3" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/browser-sync-client/node_modules/mitt": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/mitt/-/mitt-1.2.0.tgz", - "integrity": "sha512-r6lj77KlwqLhIUku9UWYes7KJtsczvolZkzp8hbaDPPaE24OmWl5s539Mytlj22siEQKosZ26qCBgda2PKwoJw==", - "dev": true, - "license": "MIT" - }, - "node_modules/browser-sync-ui": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/browser-sync-ui/-/browser-sync-ui-3.0.4.tgz", - "integrity": "sha512-5Po3YARCZ/8yQHFzvrSjn8+hBUF7ZWac39SHsy8Tls+7tE62iq6pYWxpVU6aOOMAGD21RwFQhQeqmJPf70kHEQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "async-each-series": "0.1.1", - "chalk": "4.1.2", - "connect-history-api-fallback": "^1", - "immutable": "^3", - "server-destroy": "1.0.1", - "socket.io-client": "^4.4.1", - "stream-throttle": "^0.1.3" - } - }, - "node_modules/browser-sync-ui/node_modules/immutable": { - "version": "3.8.2", - "resolved": "https://registry.npmjs.org/immutable/-/immutable-3.8.2.tgz", - "integrity": "sha512-15gZoQ38eYjEjxkorfbcgBKBL6R7T459OuK+CpcWt7O3KF4uPCx2tD0uFETlUDIyo+1789crbMhTvQBSR5yBMg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/browser-sync/node_modules/chokidar": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", - "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", - "dev": true, - "license": "MIT", - "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" - }, - "engines": { - "node": ">= 8.10.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - } - }, - "node_modules/browser-sync/node_modules/immutable": { - "version": "3.8.2", - "resolved": "https://registry.npmjs.org/immutable/-/immutable-3.8.2.tgz", - "integrity": "sha512-15gZoQ38eYjEjxkorfbcgBKBL6R7T459OuK+CpcWt7O3KF4uPCx2tD0uFETlUDIyo+1789crbMhTvQBSR5yBMg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/browser-sync/node_modules/readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "dev": true, - "license": "MIT", - "dependencies": { - "picomatch": "^2.2.1" - }, - "engines": { - "node": ">=8.10.0" - } - }, - "node_modules/browserslist": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", - "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", - "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" - } - ], - "license": "MIT", - "peer": true, - "dependencies": { - "baseline-browser-mapping": "^2.9.0", - "caniuse-lite": "^1.0.30001759", - "electron-to-chromium": "^1.5.263", - "node-releases": "^2.0.27", - "update-browserslist-db": "^1.2.0" - }, - "bin": { - "browserslist": "cli.js" - }, - "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" - } - }, - "node_modules/bs-recipes": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/bs-recipes/-/bs-recipes-1.3.4.tgz", - "integrity": "sha512-BXvDkqhDNxXEjeGM8LFkSbR+jzmP/CYpCiVKYn+soB1dDldeU15EBNDkwVXndKuX35wnNUaPd0qSoQEAkmQtMw==", - "dev": true, - "license": "ISC" - }, - "node_modules/bytes": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/call-bind-apply-helpers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", - "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/caniuse-lite": { - "version": "1.0.30001760", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001760.tgz", - "integrity": "sha512-7AAMPcueWELt1p3mi13HR/LHH0TJLT11cnwDJEs3xA4+CK/PLKeO9Kl1oru24htkyUKtkGCvAx4ohB0Ttry8Dw==", - "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" - } - ], - "license": "CC-BY-4.0" - }, - "node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/chokidar": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", - "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", - "dev": true, - "license": "MIT", - "dependencies": { - "readdirp": "^4.0.1" - }, - "engines": { - "node": ">= 14.16.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/cli-width": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", - "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">= 12" - } - }, - "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, - "license": "ISC", - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/cliui/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, - "license": "MIT", - "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/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, - "license": "MIT", - "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, - "license": "MIT" - }, - "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, - "license": "MIT", - "dependencies": { - "delayed-stream": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/commander": { - "version": "2.20.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true, - "license": "MIT" - }, - "node_modules/concurrently": { - "version": "9.2.1", - "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-9.2.1.tgz", - "integrity": "sha512-fsfrO0MxV64Znoy8/l1vVIjjHa29SZyyqPgQBwhiDcaW8wJc2W3XWVOGx4M3oJBnv/zdUZIIp1gDeS98GzP8Ng==", - "dev": true, - "license": "MIT", - "dependencies": { - "chalk": "4.1.2", - "rxjs": "7.8.2", - "shell-quote": "1.8.3", - "supports-color": "8.1.1", - "tree-kill": "1.2.2", - "yargs": "17.7.2" - }, - "bin": { - "conc": "dist/bin/concurrently.js", - "concurrently": "dist/bin/concurrently.js" - }, - "engines": { - "node": ">=18" + "license": "MIT", + "dependencies": { + "tinyspy": "^4.0.3" }, "funding": { - "url": "https://github.com/open-cli-tools/concurrently?sponsor=1" + "url": "https://opencollective.com/vitest" } }, - "node_modules/concurrently/node_modules/supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "node_modules/@vitest/utils": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.7.tgz", + "integrity": "sha512-x6BDOd7dyo3PFLY3I9/HJ25X/6OurhGXk2/B9gOZNPF7XDVjeBK4k01lQE5uvDpbuheErh91qYuE1E2OEjK3Rw==", "dev": true, "license": "MIT", "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=10" + "@vitest/pretty-format": "3.2.7", + "loupe": "^3.1.4", + "tinyrainbow": "^2.0.0" }, "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" + "url": "https://opencollective.com/vitest" } }, - "node_modules/connect": { - "version": "3.6.6", - "resolved": "https://registry.npmjs.org/connect/-/connect-3.6.6.tgz", - "integrity": "sha512-OO7axMmPpu/2XuX1+2Yrg0ddju31B6xLZMWkJ5rYBu4YRmRVlOjvlY6kw2FJKiAzyxGwnrDUAG4s1Pf0sbBMCQ==", + "node_modules/@volar/language-core": { + "version": "2.4.15", + "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.4.15.tgz", + "integrity": "sha512-3VHw+QZU0ZG9IuQmzT68IyN4hZNd9GchGPhbD9+pa8CVv7rnoOZwo7T8weIbrRmihqy3ATpdfXFnqRrfPVK6CA==", "dev": true, "license": "MIT", "dependencies": { - "debug": "2.6.9", - "finalhandler": "1.1.0", - "parseurl": "~1.3.2", - "utils-merge": "1.0.1" - }, - "engines": { - "node": ">= 0.10.0" + "@volar/source-map": "2.4.15" } }, - "node_modules/connect-history-api-fallback": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-1.6.0.tgz", - "integrity": "sha512-e54B99q/OUoH64zYYRf3HBP5z24G38h5D3qXu23JGRoigpX5Ss4r9ZnDk3g0Z8uQC2x2lPaJ+UlWBc1ZWBWdLg==", + "node_modules/@volar/source-map": { + "version": "2.4.15", + "resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-2.4.15.tgz", + "integrity": "sha512-CPbMWlUN6hVZJYGcU/GSoHu4EnCHiLaXI9n8c9la6RaI9W5JHX+NqG+GSQcB0JdC2FIBLdZJwGsfKyBB71VlTg==", "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8" - } + "license": "MIT" }, - "node_modules/cookie": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", - "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "node_modules/@volar/typescript": { + "version": "2.4.15", + "resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-2.4.15.tgz", + "integrity": "sha512-2aZ8i0cqPGjXb4BhkMsPYDkkuc2ZQ6yOpqwAuNwUoncELqoy5fRgOQtLR9gB0g902iS0NAkvpIzs27geVyVdPg==", "dev": true, "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" + "dependencies": { + "@volar/language-core": "2.4.15", + "path-browserify": "^1.0.1", + "vscode-uri": "^3.0.8" } }, - "node_modules/copy-anything": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-4.0.5.tgz", - "integrity": "sha512-7Vv6asjS4gMOuILabD3l739tsaxFQmC+a7pLZm02zyvs8p977bL3zEgq3yDk5rn9B0PbYgIv++jmHcuUab4RhA==", + "node_modules/@vue/compiler-core": { + "version": "3.5.26", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.26.tgz", + "integrity": "sha512-vXyI5GMfuoBCnv5ucIT7jhHKl55Y477yxP6fc4eUswjP8FG3FFVFd41eNDArR+Uk3QKn2Z85NavjaxLxOC19/w==", "dev": true, "license": "MIT", "dependencies": { - "is-what": "^5.2.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/mesqueeb" + "@babel/parser": "^7.28.5", + "@vue/shared": "3.5.26", + "entities": "^7.0.0", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.1" } }, - "node_modules/cors": { - "version": "2.8.5", - "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", - "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "node_modules/@vue/compiler-dom": { + "version": "3.5.26", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.26.tgz", + "integrity": "sha512-y1Tcd3eXs834QjswshSilCBnKGeQjQXB6PqFn/1nxcQw4pmG42G8lwz+FZPAZAby6gZeHSt/8LMPfZ4Rb+Bd/A==", "dev": true, "license": "MIT", "dependencies": { - "object-assign": "^4", - "vary": "^1" - }, - "engines": { - "node": ">= 0.10" + "@vue/compiler-core": "3.5.26", + "@vue/shared": "3.5.26" } }, - "node_modules/css-blank-pseudo": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/css-blank-pseudo/-/css-blank-pseudo-6.0.2.tgz", - "integrity": "sha512-J/6m+lsqpKPqWHOifAFtKFeGLOzw3jR92rxQcwRUfA/eTuZzKfKlxOmYDx2+tqOPQAueNvBiY8WhAeHu5qNmTg==", + "node_modules/@vue/compiler-sfc": { + "version": "3.5.26", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.26.tgz", + "integrity": "sha512-egp69qDTSEZcf4bGOSsprUr4xI73wfrY5oRs6GSgXFTiHrWj4Y3X5Ydtip9QMqiCMCPVwLglB9GBxXtTadJ3mA==", "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT-0", + "license": "MIT", "dependencies": { - "postcss-selector-parser": "^6.0.13" - }, - "engines": { - "node": "^14 || ^16 || >=18" - }, - "peerDependencies": { - "postcss": "^8.4" + "@babel/parser": "^7.28.5", + "@vue/compiler-core": "3.5.26", + "@vue/compiler-dom": "3.5.26", + "@vue/compiler-ssr": "3.5.26", + "@vue/shared": "3.5.26", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.21", + "postcss": "^8.5.6", + "source-map-js": "^1.2.1" } }, - "node_modules/css-has-pseudo": { - "version": "6.0.5", - "resolved": "https://registry.npmjs.org/css-has-pseudo/-/css-has-pseudo-6.0.5.tgz", - "integrity": "sha512-ZTv6RlvJJZKp32jPYnAJVhowDCrRrHUTAxsYSuUPBEDJjzws6neMnzkRblxtgmv1RgcV5dhH2gn7E3wA9Wt6lw==", + "node_modules/@vue/compiler-ssr": { + "version": "3.5.26", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.26.tgz", + "integrity": "sha512-lZT9/Y0nSIRUPVvapFJEVDbEXruZh2IYHMk2zTtEgJSlP5gVOqeWXH54xDKAaFS4rTnDeDBQUYDtxKyoW9FwDw==", "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT-0", + "license": "MIT", "dependencies": { - "@csstools/selector-specificity": "^3.1.1", - "postcss-selector-parser": "^6.0.13", - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^14 || ^16 || >=18" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/css-prefers-color-scheme": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/css-prefers-color-scheme/-/css-prefers-color-scheme-9.0.1.tgz", - "integrity": "sha512-iFit06ochwCKPRiWagbTa1OAWCvWWVdEnIFd8BaRrgO8YrrNh4RAWUQTFcYX5tdFZgFl1DJ3iiULchZyEbnF4g==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT-0", - "engines": { - "node": "^14 || ^16 || >=18" - }, - "peerDependencies": { - "postcss": "^8.4" + "@vue/compiler-dom": "3.5.26", + "@vue/shared": "3.5.26" } }, - "node_modules/css-render": { - "version": "0.15.14", - "resolved": "https://registry.npmjs.org/css-render/-/css-render-0.15.14.tgz", - "integrity": "sha512-9nF4PdUle+5ta4W5SyZdLCCmFd37uVimSjg1evcTqKJCyvCEEj12WKzOSBNak6r4im4J4iYXKH1OWpUV5LBYFg==", + "node_modules/@vue/compiler-vue2": { + "version": "2.7.16", + "resolved": "https://registry.npmjs.org/@vue/compiler-vue2/-/compiler-vue2-2.7.16.tgz", + "integrity": "sha512-qYC3Psj9S/mfu9uVi5WvNZIzq+xnXMhOwbTFKKDD7b1lhpnn71jXSFdTQ+WsIEk0ONCd7VV2IMm7ONl6tbQ86A==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { - "@emotion/hash": "~0.8.0", - "csstype": "~3.0.5" + "de-indent": "^1.0.2", + "he": "^1.2.0" } }, - "node_modules/css-render/node_modules/csstype": { - "version": "3.0.11", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.11.tgz", - "integrity": "sha512-sa6P2wJ+CAbgyy4KFssIb/JNMLxFvKF1pCYCSXS8ZMuqZnMsrxqI2E5sPyoTpxoPU/gVZMzr2zjOfg8GIZOMsw==", - "dev": true, - "license": "MIT" - }, - "node_modules/cssdb": { - "version": "8.5.2", - "resolved": "https://registry.npmjs.org/cssdb/-/cssdb-8.5.2.tgz", - "integrity": "sha512-Pmoj9RmD8RIoIzA2EQWO4D4RMeDts0tgAH0VXdlNdxjuBGI3a9wMOIcUwaPNmD4r2qtIa06gqkIf7sECl+cBCg==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - }, - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - } - ], - "license": "MIT-0" - }, - "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==", + "node_modules/@vue/language-core": { + "version": "2.2.12", + "resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-2.2.12.tgz", + "integrity": "sha512-IsGljWbKGU1MZpBPN+BvPAdr55YPkj2nB/TBNGNC32Vy2qLG25DYu/NBN2vNtZqdRbTRjaoYrahLrToim2NanA==", "dev": true, "license": "MIT", - "bin": { - "cssesc": "bin/cssesc" + "dependencies": { + "@volar/language-core": "2.4.15", + "@vue/compiler-dom": "^3.5.0", + "@vue/compiler-vue2": "^2.7.16", + "@vue/shared": "^3.5.0", + "alien-signals": "^1.0.3", + "minimatch": "^9.0.3", + "muggle-string": "^0.4.1", + "path-browserify": "^1.0.1" }, - "engines": { - "node": ">=4" - } - }, - "node_modules/csstype": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", - "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "license": "MIT" - }, - "node_modules/date-fns": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz", - "integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==", - "dev": true, - "license": "MIT", - "peer": true, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/kossnocorp" - } - }, - "node_modules/date-fns-tz": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/date-fns-tz/-/date-fns-tz-3.2.0.tgz", - "integrity": "sha512-sg8HqoTEulcbbbVXeg84u5UnlsQa8GS5QXMqjjYIhS4abEVVKIUwe0/l/UhrZdKaL/W5eWZNlbTeEIiOXTcsBQ==", - "dev": true, - "license": "MIT", "peerDependencies": { - "date-fns": "^3.0.0 || ^4.0.0" - } - }, - "node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "2.0.0" - } - }, - "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, - "license": "MIT", - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/destroy": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", - "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, - "node_modules/detect-libc": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", - "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=8" - } - }, - "node_modules/dev-ip": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/dev-ip/-/dev-ip-1.0.1.tgz", - "integrity": "sha512-LmVkry/oDShEgSZPNgqCIp2/TlqtExeGmymru3uCELnfyjY11IzpAproLYs+1X88fXO6DBoYP3ul2Xo2yz2j6A==", - "dev": true, - "bin": { - "dev-ip": "lib/dev-ip.js" + "typescript": "*" }, - "engines": { - "node": ">= 0.8.0" + "peerDependenciesMeta": { + "typescript": { + "optional": true + } } }, - "node_modules/dunder-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", - "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "node_modules/@vue/language-core/node_modules/brace-expansion": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.4.tgz", + "integrity": "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==", "dev": true, "license": "MIT", "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "es-errors": "^1.3.0", - "gopd": "^1.2.0" - }, - "engines": { - "node": ">= 0.4" + "balanced-match": "^1.0.0" } }, - "node_modules/easy-extender": { - "version": "2.3.4", - "resolved": "https://registry.npmjs.org/easy-extender/-/easy-extender-2.3.4.tgz", - "integrity": "sha512-8cAwm6md1YTiPpOvDULYJL4ZS6WfM5/cTeVVh4JsvyYZAoqlRVUpHL9Gr5Fy7HA6xcSZicUia3DeAgO3Us8E+Q==", + "node_modules/@vue/language-core/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", "dev": true, + "license": "ISC", "dependencies": { - "lodash": "^4.17.10" + "brace-expansion": "^2.0.2" }, "engines": { - "node": ">= 4.0.0" + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/eazy-logger": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/eazy-logger/-/eazy-logger-4.1.0.tgz", - "integrity": "sha512-+mn7lRm+Zf1UT/YaH8WXtpU6PIV2iOjzP6jgKoiaq/VNrjYKp+OHZGe2znaLgDeFkw8cL9ffuaUm+nNnzcYyGw==", + "node_modules/@vue/reactivity": { + "version": "3.5.26", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.26.tgz", + "integrity": "sha512-9EnYB1/DIiUYYnzlnUBgwU32NNvLp/nhxLXeWRhHUEeWNTn1ECxX8aGO7RTXeX6PPcxe3LLuNBFoJbV4QZ+CFQ==", "dev": true, + "license": "MIT", "dependencies": { - "chalk": "4.1.2" - }, - "engines": { - "node": ">= 0.8.0" + "@vue/shared": "3.5.26" } }, - "node_modules/ee-first": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", - "dev": true, - "license": "MIT" - }, - "node_modules/electron-to-chromium": { - "version": "1.5.267", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.267.tgz", - "integrity": "sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==", + "node_modules/@vue/runtime-core": { + "version": "3.5.26", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.26.tgz", + "integrity": "sha512-xJWM9KH1kd201w5DvMDOwDHYhrdPTrAatn56oB/LRG4plEQeZRQLw0Bpwih9KYoqmzaxF0OKSn6swzYi84e1/Q==", "dev": true, - "license": "ISC" + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.26", + "@vue/shared": "3.5.26" + } }, - "node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "node_modules/@vue/runtime-dom": { + "version": "3.5.26", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.26.tgz", + "integrity": "sha512-XLLd/+4sPC2ZkN/6+V4O4gjJu6kSDbHAChvsyWgm1oGbdSO3efvGYnm25yCjtFm/K7rrSDvSfPDgN1pHgS4VNQ==", "dev": true, - "license": "MIT" + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.26", + "@vue/runtime-core": "3.5.26", + "@vue/shared": "3.5.26", + "csstype": "^3.2.3" + } }, - "node_modules/encodeurl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "node_modules/@vue/server-renderer": { + "version": "3.5.26", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.26.tgz", + "integrity": "sha512-TYKLXmrwWKSodyVuO1WAubucd+1XlLg4set0YoV+Hu8Lo79mp/YMwWV5mC5FgtsDxX3qo1ONrxFaTP1OQgy1uA==", "dev": true, "license": "MIT", - "engines": { - "node": ">= 0.8" + "dependencies": { + "@vue/compiler-ssr": "3.5.26", + "@vue/shared": "3.5.26" + }, + "peerDependencies": { + "vue": "3.5.26" } }, - "node_modules/engine.io": { - "version": "6.6.4", - "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.4.tgz", - "integrity": "sha512-ZCkIjSYNDyGn0R6ewHDtXgns/Zre/NT6Agvq1/WobF7JXgFff4SeDroKiCO3fNJreU9YG429Sc81o4w5ok/W5g==", + "node_modules/@vue/shared": { + "version": "3.5.26", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.26.tgz", + "integrity": "sha512-7Z6/y3uFI5PRoKeorTOSXKcDj0MSasfNNltcslbFrPpcw6aXRUALq4IfJlaTRspiWIUOEZbrpM+iQGmCOiWe4A==", + "dev": true, + "license": "MIT" + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", "dev": true, "license": "MIT", "dependencies": { - "@types/cors": "^2.8.12", - "@types/node": ">=10.0.0", - "accepts": "~1.3.4", - "base64id": "2.0.0", - "cookie": "~0.7.2", - "cors": "~2.8.5", - "debug": "~4.3.1", - "engine.io-parser": "~5.2.1", - "ws": "~8.17.1" + "mime-types": "~2.1.34", + "negotiator": "0.6.3" }, "engines": { - "node": ">=10.2.0" + "node": ">= 0.6" } }, - "node_modules/engine.io-client": { - "version": "6.6.3", - "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.3.tgz", - "integrity": "sha512-T0iLjnyNWahNyv/lcjS2y4oE358tVS/SYQNxYXGAJ9/GLgH4VCvOQ/mhTjqU88mLZCQgiG8RIegFHYCdVC+j5w==", + "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, "license": "MIT", "dependencies": { - "@socket.io/component-emitter": "~3.1.0", - "debug": "~4.3.1", - "engine.io-parser": "~5.2.1", - "ws": "~8.17.1", - "xmlhttprequest-ssl": "~2.1.1" + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" } }, - "node_modules/engine.io-client/node_modules/debug": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", - "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "node_modules/agent-base/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "dev": true, "license": "MIT", "dependencies": { @@ -4003,2432 +2037,2239 @@ } } }, - "node_modules/engine.io-client/node_modules/ms": { + "node_modules/agent-base/node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "dev": true, "license": "MIT" }, - "node_modules/engine.io-parser": { - "version": "5.2.3", - "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", - "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", + "node_modules/alien-signals": { + "version": "1.0.13", + "resolved": "https://registry.npmjs.org/alien-signals/-/alien-signals-1.0.13.tgz", + "integrity": "sha512-OGj9yyTnJEttvzhTUWuscOvtqxq5vrhF7vL9oS0xJ2mK0ItPYP1/y+vCFebfxoEyAz0++1AIwJ5CMr+Fk3nDmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true, "license": "MIT", "engines": { - "node": ">=10.0.0" + "node": ">=8" } }, - "node_modules/engine.io/node_modules/cookie": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", - "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, "engines": { - "node": ">= 0.6" + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/engine.io/node_modules/debug": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", - "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/async": { + "version": "2.6.4", + "resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz", + "integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "lodash": "^4.17.14" + } + }, + "node_modules/async-each-series": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/async-each-series/-/async-each-series-0.1.1.tgz", + "integrity": "sha512-p4jj6Fws4Iy2m0iCmI2am2ZNZCgbdgE+P8F/8csmn2vx7ixXrO2zGcuNsD46X5uZSVecmkEy/M06X2vG8KD6dQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/async-validator": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/async-validator/-/async-validator-4.2.5.tgz", + "integrity": "sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg==", + "dev": true, + "license": "MIT" + }, + "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, + "license": "MIT" + }, + "node_modules/autoprefixer": { + "version": "10.4.23", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.23.tgz", + "integrity": "sha512-YYTXSFulfwytnjAPlw8QHncHJmlvFKtczb8InXaAx9Q0LbfDnfEYDE55omerIJKihhmU61Ft+cAOSzQVaBUmeA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "browserslist": "^4.28.1", + "caniuse-lite": "^1.0.30001760", + "fraction.js": "^5.3.4", + "picocolors": "^1.1.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/axios": { + "version": "1.20.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.20.0.tgz", + "integrity": "sha512-r8aOh8j9cGKpgQAqpzrUHnSIc6a59Y3Xf/cv8sy1DrHCkZHzQGEuoq1tARk6qSyDdtQGSDgpb9kFlruzPvrgwg==", "dev": true, "license": "MIT", "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } + "follow-redirects": "^1.16.0", + "form-data": "^4.0.6", + "https-proxy-agent": "^5.0.1", + "proxy-from-env": "^2.1.0" } }, - "node_modules/engine.io/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "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, "license": "MIT" }, - "node_modules/enhanced-resolve": { - "version": "5.18.4", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.4.tgz", - "integrity": "sha512-LgQMM4WXU3QI+SYgEc2liRgznaD5ojbmY3sb8LxyguVkIg5FxdpTkvk72te2R38/TGKxH634oLxXRGY6d7AP+Q==", + "node_modules/base64id": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", + "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==", "dev": true, "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.4", - "tapable": "^2.2.0" - }, "engines": { - "node": ">=10.13.0" + "node": "^4.5.0 || >= 5.9" } }, - "node_modules/entities": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.0.tgz", - "integrity": "sha512-FDWG5cmEYf2Z00IkYRhbFrwIwvdFKH07uV8dvNy0omp/Qb1xcyCWp2UDtcwJF4QZZvk0sLudP6/hAu42TaqVhQ==", - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.12" - }, - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" + "node_modules/baseline-browser-mapping": { + "version": "2.9.10", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.10.tgz", + "integrity": "sha512-2VIKvDx8Z1a9rTB2eCkdPE5nSe28XnA+qivGnWHoB40hMMt/h1hSz0960Zqsn6ZyxWXUie0EBdElKv8may20AA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.js" } }, - "node_modules/es-define-property": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "node_modules/batch": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz", + "integrity": "sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw==", "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } + "license": "MIT" }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", "dev": true, "license": "MIT", "engines": { - "node": ">= 0.4" + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/es-object-atoms": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, "license": "MIT", "dependencies": { - "es-errors": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" } }, - "node_modules/es-set-tostringtag": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", - "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dev": true, "license": "MIT", "dependencies": { - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.6", - "has-tostringtag": "^1.0.2", - "hasown": "^2.0.2" + "fill-range": "^7.1.1" }, "engines": { - "node": ">= 0.4" + "node": ">=8" } }, - "node_modules/esbuild": { - "version": "0.14.54", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.14.54.tgz", - "integrity": "sha512-Cy9llcy8DvET5uznocPyqL3BFRrFXSVqbgpMJ9Wz8oVjZlh/zUSNbPRbov0VX7VxN2JH1Oa0uNxZ7eLRb62pJA==", + "node_modules/browser-sync": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/browser-sync/-/browser-sync-3.0.4.tgz", + "integrity": "sha512-mcYOIy4BW6sWSEnTSBjQwWsnbx2btZX78ajTTjdNfyC/EqQVcIe0nQR6894RNAMtvlfAnLaH9L2ka97zpvgenA==", "dev": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" + "license": "Apache-2.0", + "dependencies": { + "browser-sync-client": "^3.0.4", + "browser-sync-ui": "^3.0.4", + "bs-recipes": "1.3.4", + "chalk": "4.1.2", + "chokidar": "^3.5.1", + "connect": "3.6.6", + "connect-history-api-fallback": "^1", + "dev-ip": "^1.0.1", + "easy-extender": "^2.3.4", + "eazy-logger": "^4.1.0", + "etag": "^1.8.1", + "fresh": "^0.5.2", + "fs-extra": "3.0.1", + "http-proxy": "^1.18.1", + "immutable": "^3", + "micromatch": "^4.0.8", + "opn": "5.3.0", + "portscanner": "2.2.0", + "raw-body": "^2.3.2", + "resp-modifier": "6.0.2", + "rx": "4.1.0", + "send": "^0.19.0", + "serve-index": "^1.9.1", + "serve-static": "^1.16.2", + "server-destroy": "1.0.1", + "socket.io": "^4.4.1", + "ua-parser-js": "^1.0.33", + "yargs": "^17.3.1" }, - "engines": { - "node": ">=12" + "bin": { + "browser-sync": "dist/bin.js" }, - "optionalDependencies": { - "@esbuild/linux-loong64": "0.14.54", - "esbuild-android-64": "0.14.54", - "esbuild-android-arm64": "0.14.54", - "esbuild-darwin-64": "0.14.54", - "esbuild-darwin-arm64": "0.14.54", - "esbuild-freebsd-64": "0.14.54", - "esbuild-freebsd-arm64": "0.14.54", - "esbuild-linux-32": "0.14.54", - "esbuild-linux-64": "0.14.54", - "esbuild-linux-arm": "0.14.54", - "esbuild-linux-arm64": "0.14.54", - "esbuild-linux-mips64le": "0.14.54", - "esbuild-linux-ppc64le": "0.14.54", - "esbuild-linux-riscv64": "0.14.54", - "esbuild-linux-s390x": "0.14.54", - "esbuild-netbsd-64": "0.14.54", - "esbuild-openbsd-64": "0.14.54", - "esbuild-sunos-64": "0.14.54", - "esbuild-windows-32": "0.14.54", - "esbuild-windows-64": "0.14.54", - "esbuild-windows-arm64": "0.14.54" - } - }, - "node_modules/esbuild-android-64": { - "version": "0.14.54", - "resolved": "https://registry.npmjs.org/esbuild-android-64/-/esbuild-android-64-0.14.54.tgz", - "integrity": "sha512-Tz2++Aqqz0rJ7kYBfz+iqyE3QMycD4vk7LBRyWaAVFgFtQ/O8EJOnVmTOiDWYZ/uYzB4kvP+bqejYdVKzE5lAQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/esbuild-android-arm64": { - "version": "0.14.54", - "resolved": "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.14.54.tgz", - "integrity": "sha512-F9E+/QDi9sSkLaClO8SOV6etqPd+5DgJje1F9lOWoNncDdOBL2YF59IhsWATSt0TLZbYCf3pNlTHvVV5VfHdvg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/esbuild-darwin-64": { - "version": "0.14.54", - "resolved": "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.14.54.tgz", - "integrity": "sha512-jtdKWV3nBviOd5v4hOpkVmpxsBy90CGzebpbO9beiqUYVMBtSc0AL9zGftFuBon7PNDcdvNCEuQqw2x0wP9yug==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], "engines": { - "node": ">=12" + "node": ">= 8.0.0" } }, - "node_modules/esbuild-darwin-arm64": { - "version": "0.14.54", - "resolved": "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.14.54.tgz", - "integrity": "sha512-OPafJHD2oUPyvJMrsCvDGkRrVCar5aVyHfWGQzY1dWnzErjrDuSETxwA2HSsyg2jORLY8yBfzc1MIpUkXlctmw==", - "cpu": [ - "arm64" - ], + "node_modules/browser-sync-client": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/browser-sync-client/-/browser-sync-client-3.0.4.tgz", + "integrity": "sha512-+ew5ubXzGRKVjquBL3u6najS40TG7GxCdyBll0qSRc/n+JRV9gb/yDdRL1IAgRHqjnJTdqeBKKIQabjvjRSYRQ==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], + "license": "ISC", + "dependencies": { + "etag": "1.8.1", + "fresh": "0.5.2", + "mitt": "^1.1.3" + }, "engines": { - "node": ">=12" + "node": ">=8.0.0" } }, - "node_modules/esbuild-freebsd-64": { - "version": "0.14.54", - "resolved": "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.14.54.tgz", - "integrity": "sha512-OKwd4gmwHqOTp4mOGZKe/XUlbDJ4Q9TjX0hMPIDBUWWu/kwhBAudJdBoxnjNf9ocIB6GN6CPowYpR/hRCbSYAg==", - "cpu": [ - "x64" - ], + "node_modules/browser-sync-client/node_modules/mitt": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mitt/-/mitt-1.2.0.tgz", + "integrity": "sha512-r6lj77KlwqLhIUku9UWYes7KJtsczvolZkzp8hbaDPPaE24OmWl5s539Mytlj22siEQKosZ26qCBgda2PKwoJw==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } + "license": "MIT" }, - "node_modules/esbuild-freebsd-arm64": { - "version": "0.14.54", - "resolved": "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.14.54.tgz", - "integrity": "sha512-sFwueGr7OvIFiQT6WeG0jRLjkjdqWWSrfbVwZp8iMP+8UHEHRBvlaxL6IuKNDwAozNUmbb8nIMXa7oAOARGs1Q==", - "cpu": [ - "arm64" - ], + "node_modules/browser-sync-ui": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/browser-sync-ui/-/browser-sync-ui-3.0.4.tgz", + "integrity": "sha512-5Po3YARCZ/8yQHFzvrSjn8+hBUF7ZWac39SHsy8Tls+7tE62iq6pYWxpVU6aOOMAGD21RwFQhQeqmJPf70kHEQ==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" + "license": "Apache-2.0", + "dependencies": { + "async-each-series": "0.1.1", + "chalk": "4.1.2", + "connect-history-api-fallback": "^1", + "immutable": "^3", + "server-destroy": "1.0.1", + "socket.io-client": "^4.4.1", + "stream-throttle": "^0.1.3" } }, - "node_modules/esbuild-linux-32": { - "version": "0.14.54", - "resolved": "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.14.54.tgz", - "integrity": "sha512-1ZuY+JDI//WmklKlBgJnglpUL1owm2OX+8E1syCD6UAxcMM/XoWd76OHSjl/0MR0LisSAXDqgjT3uJqT67O3qw==", - "cpu": [ - "ia32" - ], + "node_modules/browser-sync-ui/node_modules/immutable": { + "version": "3.8.2", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-3.8.2.tgz", + "integrity": "sha512-15gZoQ38eYjEjxkorfbcgBKBL6R7T459OuK+CpcWt7O3KF4uPCx2tD0uFETlUDIyo+1789crbMhTvQBSR5yBMg==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], "engines": { - "node": ">=12" + "node": ">=0.10.0" } }, - "node_modules/esbuild-linux-64": { - "version": "0.14.54", - "resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.14.54.tgz", - "integrity": "sha512-EgjAgH5HwTbtNsTqQOXWApBaPVdDn7XcK+/PtJwZLT1UmpLoznPd8c5CxqsH2dQK3j05YsB3L17T8vE7cp4cCg==", - "cpu": [ - "x64" - ], + "node_modules/browser-sync/node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, "engines": { - "node": ">=12" + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" } }, - "node_modules/esbuild-linux-arm": { - "version": "0.14.54", - "resolved": "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.14.54.tgz", - "integrity": "sha512-qqz/SjemQhVMTnvcLGoLOdFpCYbz4v4fUo+TfsWG+1aOu70/80RV6bgNpR2JCrppV2moUQkww+6bWxXRL9YMGw==", - "cpu": [ - "arm" - ], + "node_modules/browser-sync/node_modules/immutable": { + "version": "3.8.2", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-3.8.2.tgz", + "integrity": "sha512-15gZoQ38eYjEjxkorfbcgBKBL6R7T459OuK+CpcWt7O3KF4uPCx2tD0uFETlUDIyo+1789crbMhTvQBSR5yBMg==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], "engines": { - "node": ">=12" + "node": ">=0.10.0" } }, - "node_modules/esbuild-linux-arm64": { - "version": "0.14.54", - "resolved": "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.14.54.tgz", - "integrity": "sha512-WL71L+0Rwv+Gv/HTmxTEmpv0UgmxYa5ftZILVi2QmZBgX3q7+tDeOQNqGtdXSdsL8TQi1vIaVFHUPDe0O0kdig==", - "cpu": [ - "arm64" - ], + "node_modules/browser-sync/node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "dependencies": { + "picomatch": "^2.2.1" + }, "engines": { - "node": ">=12" + "node": ">=8.10.0" } }, - "node_modules/esbuild-linux-mips64le": { - "version": "0.14.54", - "resolved": "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.14.54.tgz", - "integrity": "sha512-qTHGQB8D1etd0u1+sB6p0ikLKRVuCWhYQhAHRPkO+OF3I/iSlTKNNS0Lh2Oc0g0UFGguaFZZiPJdJey3AGpAlw==", - "cpu": [ - "mips64el" - ], + "node_modules/browserslist": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" + "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" + } ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" + }, + "bin": { + "browserslist": "cli.js" + }, "engines": { - "node": ">=12" + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, - "node_modules/esbuild-linux-ppc64le": { - "version": "0.14.54", - "resolved": "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.14.54.tgz", - "integrity": "sha512-j3OMlzHiqwZBDPRCDFKcx595XVfOfOnv68Ax3U4UKZ3MTYQB5Yz3X1mn5GnodEVYzhtZgxEBidLWeIs8FDSfrQ==", - "cpu": [ - "ppc64" - ], + "node_modules/bs-recipes": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/bs-recipes/-/bs-recipes-1.3.4.tgz", + "integrity": "sha512-BXvDkqhDNxXEjeGM8LFkSbR+jzmP/CYpCiVKYn+soB1dDldeU15EBNDkwVXndKuX35wnNUaPd0qSoQEAkmQtMw==", + "dev": true, + "license": "ISC" + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], "engines": { - "node": ">=12" + "node": ">= 0.8" } }, - "node_modules/esbuild-linux-riscv64": { - "version": "0.14.54", - "resolved": "https://registry.npmjs.org/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.14.54.tgz", - "integrity": "sha512-y7Vt7Wl9dkOGZjxQZnDAqqn+XOqFD7IMWiewY5SPlNlzMX39ocPQlOaoxvT4FllA5viyV26/QzHtvTjVNOxHZg==", - "cpu": [ - "riscv64" - ], + "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, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], "engines": { - "node": ">=12" + "node": ">=8" } }, - "node_modules/esbuild-linux-s390x": { - "version": "0.14.54", - "resolved": "https://registry.npmjs.org/esbuild-linux-s390x/-/esbuild-linux-s390x-0.14.54.tgz", - "integrity": "sha512-zaHpW9dziAsi7lRcyV4r8dhfG1qBidQWUXweUjnw+lliChJqQr+6XD71K41oEIC3Mx1KStovEmlzm+MkGZHnHA==", - "cpu": [ - "s390x" - ], + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, "engines": { - "node": ">=12" + "node": ">= 0.4" } }, - "node_modules/esbuild-netbsd-64": { - "version": "0.14.54", - "resolved": "https://registry.npmjs.org/esbuild-netbsd-64/-/esbuild-netbsd-64-0.14.54.tgz", - "integrity": "sha512-PR01lmIMnfJTgeU9VJTDY9ZerDWVFIUzAtJuDHwwceppW7cQWjBBqP48NdeRtoP04/AtO9a7w3viI+PIDr6d+w==", - "cpu": [ - "x64" + "node_modules/caniuse-lite": { + "version": "1.0.30001760", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001760.tgz", + "integrity": "sha512-7AAMPcueWELt1p3mi13HR/LHH0TJLT11cnwDJEs3xA4+CK/PLKeO9Kl1oru24htkyUKtkGCvAx4ohB0Ttry8Dw==", + "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" + } ], + "license": "CC-BY-4.0" + }, + "node_modules/chai": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, "engines": { - "node": ">=12" + "node": ">=18" } }, - "node_modules/esbuild-openbsd-64": { - "version": "0.14.54", - "resolved": "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.14.54.tgz", - "integrity": "sha512-Qyk7ikT2o7Wu76UsvvDS5q0amJvmRzDyVlL0qf5VLsLchjCa1+IAvd8kTBgUxD7VBUUVgItLkk609ZHUc1oCaw==", - "cpu": [ - "x64" - ], + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, "engines": { - "node": ">=12" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/esbuild-sunos-64": { - "version": "0.14.54", - "resolved": "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.14.54.tgz", - "integrity": "sha512-28GZ24KmMSeKi5ueWzMcco6EBHStL3B6ubM7M51RmPwXQGLe0teBGJocmWhgwccA1GeFXqxzILIxXpHbl9Q/Kw==", - "cpu": [ - "x64" - ], + "node_modules/check-error": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", + "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], "engines": { - "node": ">=12" + "node": ">= 16" } }, - "node_modules/esbuild-windows-32": { - "version": "0.14.54", - "resolved": "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.14.54.tgz", - "integrity": "sha512-T+rdZW19ql9MjS7pixmZYVObd9G7kcaZo+sETqNH4RCkuuYSuv9AGHUVnPoP9hhuE1WM1ZimHz1CIBHBboLU7w==", - "cpu": [ - "ia32" - ], + "node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", "dev": true, "license": "MIT", "optional": true, - "os": [ - "win32" - ], + "peer": true, + "dependencies": { + "readdirp": "^4.0.1" + }, "engines": { - "node": ">=12" + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" } }, - "node_modules/esbuild-windows-64": { - "version": "0.14.54", - "resolved": "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.14.54.tgz", - "integrity": "sha512-AoHTRBUuYwXtZhjXZbA1pGfTo8cJo3vZIcWGLiUcTNgHpJJMC1rVA44ZereBHMJtotyN71S8Qw0npiCIkW96cQ==", - "cpu": [ - "x64" - ], + "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, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, "engines": { "node": ">=12" } }, - "node_modules/esbuild-windows-arm64": { - "version": "0.14.54", - "resolved": "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.14.54.tgz", - "integrity": "sha512-M0kuUvXhot1zOISQGXwWn6YtS+Y/1RT9WrVIOywZnJHo3jCDyewAc79aKNQWFCQm+xNHVTq9h8dZKvygoXQQRg==", - "cpu": [ - "arm64" - ], + "node_modules/cliui/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, "license": "MIT", - "optional": true, - "os": [ - "win32" - ], + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, "engines": { - "node": ">=12" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, - "node_modules/escalade": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", - "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "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, "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, "engines": { - "node": ">=6" + "node": ">=7.0.0" } }, - "node_modules/escape-html": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "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, "license": "MIT" }, - "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==", - "license": "MIT" - }, - "node_modules/etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "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, "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, "engines": { - "node": ">= 0.6" + "node": ">= 0.8" } }, - "node_modules/eventemitter3": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", - "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", "dev": true, "license": "MIT" }, - "node_modules/evtd": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/evtd/-/evtd-0.2.4.tgz", - "integrity": "sha512-qaeGN5bx63s/AXgQo8gj6fBkxge+OoLddLniox5qtLAEY5HSnuSlISXVPxnSae1dWblvTh4/HoMIB+mbMsvZzw==", + "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, "license": "MIT" }, - "node_modules/fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "node_modules/concurrently": { + "version": "9.2.1", + "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-9.2.1.tgz", + "integrity": "sha512-fsfrO0MxV64Znoy8/l1vVIjjHa29SZyyqPgQBwhiDcaW8wJc2W3XWVOGx4M3oJBnv/zdUZIIp1gDeS98GzP8Ng==", "dev": true, "license": "MIT", "dependencies": { - "to-regex-range": "^5.0.1" + "chalk": "4.1.2", + "rxjs": "7.8.2", + "shell-quote": "1.8.3", + "supports-color": "8.1.1", + "tree-kill": "1.2.2", + "yargs": "17.7.2" + }, + "bin": { + "conc": "dist/bin/concurrently.js", + "concurrently": "dist/bin/concurrently.js" }, "engines": { - "node": ">=8" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/open-cli-tools/concurrently?sponsor=1" } }, - "node_modules/finalhandler": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.0.tgz", - "integrity": "sha512-ejnvM9ZXYzp6PUPUyQBMBf0Co5VX2gr5H2VQe2Ui2jWXNlxv+PYZo8wpAymJNJdLsG1R4p+M4aynF8KuoUEwRw==", + "node_modules/concurrently/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", "dev": true, "license": "MIT", "dependencies": { - "debug": "2.6.9", - "encodeurl": "~1.0.1", - "escape-html": "~1.0.3", - "on-finished": "~2.3.0", - "parseurl": "~1.3.2", - "statuses": "~1.3.1", - "unpipe": "~1.0.0" + "has-flag": "^4.0.0" }, "engines": { - "node": ">= 0.8" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" } }, - "node_modules/finalhandler/node_modules/statuses": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.3.1.tgz", - "integrity": "sha512-wuTCPGlJONk/a1kqZ4fQM2+908lC7fa7nPYpTC1EhnvqLX/IICbeP1OZGDtA374trpSq68YubKUMo8oRhN46yg==", + "node_modules/connect": { + "version": "3.6.6", + "resolved": "https://registry.npmjs.org/connect/-/connect-3.6.6.tgz", + "integrity": "sha512-OO7axMmPpu/2XuX1+2Yrg0ddju31B6xLZMWkJ5rYBu4YRmRVlOjvlY6kw2FJKiAzyxGwnrDUAG4s1Pf0sbBMCQ==", "dev": true, "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "finalhandler": "1.1.0", + "parseurl": "~1.3.2", + "utils-merge": "1.0.1" + }, "engines": { - "node": ">= 0.6" + "node": ">= 0.10.0" } }, - "node_modules/follow-redirects": { - "version": "1.15.11", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", - "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "node_modules/connect-history-api-fallback": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-1.6.0.tgz", + "integrity": "sha512-e54B99q/OUoH64zYYRf3HBP5z24G38h5D3qXu23JGRoigpX5Ss4r9ZnDk3g0Z8uQC2x2lPaJ+UlWBc1ZWBWdLg==", "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/RubenVerborgh" - } - ], "license": "MIT", "engines": { - "node": ">=4.0" - }, - "peerDependenciesMeta": { - "debug": { - "optional": true - } + "node": ">=0.8" } }, - "node_modules/form-data": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", - "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", "dev": true, "license": "MIT", "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "es-set-tostringtag": "^2.1.0", - "hasown": "^2.0.2", - "mime-types": "^2.1.12" + "object-assign": "^4", + "vary": "^1" }, "engines": { - "node": ">= 6" + "node": ">= 0.10" } }, - "node_modules/fraction.js": { - "version": "5.3.4", - "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", - "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==", + "node_modules/css-render": { + "version": "0.15.14", + "resolved": "https://registry.npmjs.org/css-render/-/css-render-0.15.14.tgz", + "integrity": "sha512-9nF4PdUle+5ta4W5SyZdLCCmFd37uVimSjg1evcTqKJCyvCEEj12WKzOSBNak6r4im4J4iYXKH1OWpUV5LBYFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@emotion/hash": "~0.8.0", + "csstype": "~3.0.5" + } + }, + "node_modules/css-render/node_modules/csstype": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.11.tgz", + "integrity": "sha512-sa6P2wJ+CAbgyy4KFssIb/JNMLxFvKF1pCYCSXS8ZMuqZnMsrxqI2E5sPyoTpxoPU/gVZMzr2zjOfg8GIZOMsw==", + "dev": true, + "license": "MIT" + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/date-fns": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.4.0.tgz", + "integrity": "sha512-+1UMbeh68lH1SegH83CGWwpb6OHHbpSgr3+s5Eww5M4CAgswBpoWS0AjTOfEJ33HiYKz1hdj/KTFprzXHmq/6w==", "dev": true, "license": "MIT", - "engines": { - "node": "*" - }, "funding": { "type": "github", - "url": "https://github.com/sponsors/rawify" + "url": "https://github.com/sponsors/kossnocorp" } }, - "node_modules/fresh": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", - "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "node_modules/date-fns-tz": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/date-fns-tz/-/date-fns-tz-3.2.0.tgz", + "integrity": "sha512-sg8HqoTEulcbbbVXeg84u5UnlsQa8GS5QXMqjjYIhS4abEVVKIUwe0/l/UhrZdKaL/W5eWZNlbTeEIiOXTcsBQ==", "dev": true, "license": "MIT", - "engines": { - "node": ">= 0.6" + "peerDependencies": { + "date-fns": "^3.0.0 || ^4.0.0" } }, - "node_modules/fs-extra": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-3.0.1.tgz", - "integrity": "sha512-V3Z3WZWVUYd8hoCL5xfXJCaHWYzmtwW5XWYSlLgERi8PWd8bx1kUHUk8L1BT57e49oKnDDD180mjfrHc1yA9rg==", + "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, + "license": "MIT" + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "dev": true, "license": "MIT", "dependencies": { - "graceful-fs": "^4.1.2", - "jsonfile": "^3.0.0", - "universalify": "^0.1.0" + "ms": "2.0.0" } }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", "dev": true, - "hasInstallScript": true, "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + "node": ">=6" } }, - "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==", + "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, "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" } }, - "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==", + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", "dev": true, - "license": "ISC", + "license": "Apache-2.0", "engines": { - "node": "6.* || 8.* || >= 10.*" + "node": ">=8" } }, - "node_modules/get-intrinsic": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", - "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "node_modules/dev-ip": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dev-ip/-/dev-ip-1.0.1.tgz", + "integrity": "sha512-LmVkry/oDShEgSZPNgqCIp2/TlqtExeGmymru3uCELnfyjY11IzpAproLYs+1X88fXO6DBoYP3ul2Xo2yz2j6A==", "dev": true, - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "function-bind": "^1.1.2", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "math-intrinsics": "^1.1.0" + "bin": { + "dev-ip": "lib/dev-ip.js" }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">= 0.8.0" } }, - "node_modules/get-proto": { + "node_modules/dunder-proto": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", - "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", "dev": true, "license": "MIT", "dependencies": { - "dunder-proto": "^1.0.1", - "es-object-atoms": "^1.0.0" + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" }, "engines": { "node": ">= 0.4" } }, - "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==", + "node_modules/easy-extender": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/easy-extender/-/easy-extender-2.3.4.tgz", + "integrity": "sha512-8cAwm6md1YTiPpOvDULYJL4ZS6WfM5/cTeVVh4JsvyYZAoqlRVUpHL9Gr5Fy7HA6xcSZicUia3DeAgO3Us8E+Q==", "dev": true, - "license": "ISC", "dependencies": { - "is-glob": "^4.0.1" + "lodash": "^4.17.10" }, "engines": { - "node": ">= 6" + "node": ">= 4.0.0" } }, - "node_modules/gopd": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "node_modules/eazy-logger": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/eazy-logger/-/eazy-logger-4.1.0.tgz", + "integrity": "sha512-+mn7lRm+Zf1UT/YaH8WXtpU6PIV2iOjzP6jgKoiaq/VNrjYKp+OHZGe2znaLgDeFkw8cL9ffuaUm+nNnzcYyGw==", "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" + "dependencies": { + "chalk": "4.1.2" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "engines": { + "node": ">= 0.8.0" } }, - "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/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", "dev": true, - "license": "ISC" + "license": "MIT" }, - "node_modules/graphql": { - "version": "16.12.0", - "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.12.0.tgz", - "integrity": "sha512-DKKrynuQRne0PNpEbzuEdHlYOMksHSUI8Zc9Unei5gTsMNA2/vMpoMz/yKba50pejK56qj98qM0SjYxAKi13gQ==", + "node_modules/electron-to-chromium": { + "version": "1.5.267", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.267.tgz", + "integrity": "sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==", "dev": true, - "license": "MIT", - "engines": { - "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0" - } + "license": "ISC" }, - "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==", + "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, - "license": "MIT", - "engines": { - "node": ">=8" - } + "license": "MIT" }, - "node_modules/has-symbols": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", "dev": true, "license": "MIT", "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">= 0.8" } }, - "node_modules/has-tostringtag": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", - "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "node_modules/engine.io": { + "version": "6.6.4", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.4.tgz", + "integrity": "sha512-ZCkIjSYNDyGn0R6ewHDtXgns/Zre/NT6Agvq1/WobF7JXgFff4SeDroKiCO3fNJreU9YG429Sc81o4w5ok/W5g==", "dev": true, "license": "MIT", "dependencies": { - "has-symbols": "^1.0.3" + "@types/cors": "^2.8.12", + "@types/node": ">=10.0.0", + "accepts": "~1.3.4", + "base64id": "2.0.0", + "cookie": "~0.7.2", + "cors": "~2.8.5", + "debug": "~4.3.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.17.1" }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=10.2.0" } }, - "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "node_modules/engine.io-client": { + "version": "6.6.3", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.3.tgz", + "integrity": "sha512-T0iLjnyNWahNyv/lcjS2y4oE358tVS/SYQNxYXGAJ9/GLgH4VCvOQ/mhTjqU88mLZCQgiG8RIegFHYCdVC+j5w==", "dev": true, "license": "MIT", "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.17.1", + "xmlhttprequest-ssl": "~2.1.1" } }, - "node_modules/headers-polyfill": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/headers-polyfill/-/headers-polyfill-4.0.3.tgz", - "integrity": "sha512-IScLbePpkvO846sIwOtOTDjutRMWdXdJmXdMvk6gCBHxFO8d+QKOQedyZSxFTTFYRSmlgSTDtXqqq4pcenBXLQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/highlight.js": { - "version": "11.11.1", - "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.11.1.tgz", - "integrity": "sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==", + "node_modules/engine.io-client/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", "dev": true, - "license": "BSD-3-Clause", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, "engines": { - "node": ">=12.0.0" + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } } }, - "node_modules/hookable": { - "version": "5.5.3", - "resolved": "https://registry.npmjs.org/hookable/-/hookable-5.5.3.tgz", - "integrity": "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==", + "node_modules/engine.io-client/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "dev": true, "license": "MIT" }, - "node_modules/http-errors": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", - "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "node_modules/engine.io-parser": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", + "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", "dev": true, "license": "MIT", - "dependencies": { - "depd": "~2.0.0", - "inherits": "~2.0.4", - "setprototypeof": "~1.2.0", - "statuses": "~2.0.2", - "toidentifier": "~1.0.1" - }, "engines": { - "node": ">= 0.8" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" + "node": ">=10.0.0" } }, - "node_modules/http-proxy": { - "version": "1.18.1", - "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", - "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==", + "node_modules/engine.io/node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", "dev": true, "license": "MIT", - "dependencies": { - "eventemitter3": "^4.0.0", - "follow-redirects": "^1.0.0", - "requires-port": "^1.0.0" - }, "engines": { - "node": ">=8.0.0" + "node": ">= 0.6" } }, - "node_modules/iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "node_modules/engine.io/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", "dev": true, "license": "MIT", "dependencies": { - "safer-buffer": ">= 2.1.2 < 3" + "ms": "^2.1.3" }, "engines": { - "node": ">=0.10.0" + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } } }, - "node_modules/immutable": { - "version": "5.1.4", - "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.4.tgz", - "integrity": "sha512-p6u1bG3YSnINT5RQmx/yRZBpenIl30kVxkTLDyHLIMk0gict704Q9n+thfDI7lTRm9vXdDYutVzXhzcThxTnXA==", + "node_modules/engine.io/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "dev": true, "license": "MIT" }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/is-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "node_modules/enhanced-resolve": { + "version": "5.18.4", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.4.tgz", + "integrity": "sha512-LgQMM4WXU3QI+SYgEc2liRgznaD5ojbmY3sb8LxyguVkIg5FxdpTkvk72te2R38/TGKxH634oLxXRGY6d7AP+Q==", "dev": true, "license": "MIT", "dependencies": { - "binary-extensions": "^2.0.0" + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" }, "engines": { - "node": ">=8" + "node": ">=10.13.0" } }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "node_modules/entities": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.0.tgz", + "integrity": "sha512-FDWG5cmEYf2Z00IkYRhbFrwIwvdFKH07uV8dvNy0omp/Qb1xcyCWp2UDtcwJF4QZZvk0sLudP6/hAu42TaqVhQ==", "dev": true, - "license": "MIT", + "license": "BSD-2-Clause", "engines": { - "node": ">=0.10.0" + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" } }, - "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==", + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", "dev": true, "license": "MIT", "engines": { - "node": ">=8" + "node": ">= 0.4" } }, - "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==", + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", "dev": true, "license": "MIT", - "dependencies": { - "is-extglob": "^2.1.1" - }, "engines": { - "node": ">=0.10.0" + "node": ">= 0.4" } }, - "node_modules/is-node-process": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/is-node-process/-/is-node-process-1.2.0.tgz", - "integrity": "sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw==", + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", "dev": true, "license": "MIT" }, - "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==", + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", "dev": true, "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, "engines": { - "node": ">=0.12.0" + "node": ">= 0.4" } }, - "node_modules/is-number-like": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/is-number-like/-/is-number-like-1.0.8.tgz", - "integrity": "sha512-6rZi3ezCyFcn5L71ywzz2bS5b2Igl1En3eTlZlvKjpz1n3IZLAYMbKYAIQgFmEu0GENg92ziU/faEOA/aixjbA==", + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", "dev": true, - "license": "ISC", + "license": "MIT", "dependencies": { - "lodash.isfinite": "^3.3.2" + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" } }, - "node_modules/is-what": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/is-what/-/is-what-5.5.0.tgz", - "integrity": "sha512-oG7cgbmg5kLYae2N5IVd3jm2s+vldjxJzK1pcu9LfpGuQ93MQSzo0okvRna+7y5ifrD+20FE8FvjusyGaz14fw==", + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", "dev": true, "license": "MIT", "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/mesqueeb" + "node": ">=6" } }, - "node_modules/is-wsl": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-1.1.0.tgz", - "integrity": "sha512-gfygJYZ2gLTDlmbWMI0CE2MwnFzSN/2SZfkMlItC4K/JBlsWVDB0bO6XhqcY13YXE7iMcAJnzTCJjPiTeJJ0Mw==", + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "dev": true, + "license": "MIT" + }, + "node_modules/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, + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", "dev": true, "license": "MIT", "engines": { - "node": ">=4" + "node": ">= 0.6" } }, - "node_modules/jiti": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", - "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", + "node_modules/eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", "dev": true, - "license": "MIT", - "bin": { - "jiti": "lib/jiti-cli.mjs" + "license": "MIT" + }, + "node_modules/evtd": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/evtd/-/evtd-0.2.4.tgz", + "integrity": "sha512-qaeGN5bx63s/AXgQo8gj6fBkxge+OoLddLniox5qtLAEY5HSnuSlISXVPxnSae1dWblvTh4/HoMIB+mbMsvZzw==", + "dev": true, + "license": "MIT" + }, + "node_modules/expect-type": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", + "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" } }, - "node_modules/jsonfile": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-3.0.1.tgz", - "integrity": "sha512-oBko6ZHlubVB5mRFkur5vgYR1UyqX+S6Y/oCfLhqNdcc2fYFlDpIoNc7AfKS1KOGcnNAkvsr0grLck9ANM815w==", + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dev": true, "license": "MIT", - "optionalDependencies": { - "graceful-fs": "^4.1.6" + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" } }, - "node_modules/lightningcss": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.2.tgz", - "integrity": "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==", + "node_modules/finalhandler": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.0.tgz", + "integrity": "sha512-ejnvM9ZXYzp6PUPUyQBMBf0Co5VX2gr5H2VQe2Ui2jWXNlxv+PYZo8wpAymJNJdLsG1R4p+M4aynF8KuoUEwRw==", "dev": true, - "license": "MPL-2.0", + "license": "MIT", "dependencies": { - "detect-libc": "^2.0.3" + "debug": "2.6.9", + "encodeurl": "~1.0.1", + "escape-html": "~1.0.3", + "on-finished": "~2.3.0", + "parseurl": "~1.3.2", + "statuses": "~1.3.1", + "unpipe": "~1.0.0" }, "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - }, - "optionalDependencies": { - "lightningcss-android-arm64": "1.30.2", - "lightningcss-darwin-arm64": "1.30.2", - "lightningcss-darwin-x64": "1.30.2", - "lightningcss-freebsd-x64": "1.30.2", - "lightningcss-linux-arm-gnueabihf": "1.30.2", - "lightningcss-linux-arm64-gnu": "1.30.2", - "lightningcss-linux-arm64-musl": "1.30.2", - "lightningcss-linux-x64-gnu": "1.30.2", - "lightningcss-linux-x64-musl": "1.30.2", - "lightningcss-win32-arm64-msvc": "1.30.2", - "lightningcss-win32-x64-msvc": "1.30.2" + "node": ">= 0.8" } }, - "node_modules/lightningcss-android-arm64": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.30.2.tgz", - "integrity": "sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==", - "cpu": [ - "arm64" - ], + "node_modules/finalhandler/node_modules/statuses": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.3.1.tgz", + "integrity": "sha512-wuTCPGlJONk/a1kqZ4fQM2+908lC7fa7nPYpTC1EhnvqLX/IICbeP1OZGDtA374trpSq68YubKUMo8oRhN46yg==", "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "android" - ], + "license": "MIT", "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "node": ">= 0.6" } }, - "node_modules/lightningcss-darwin-arm64": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.2.tgz", - "integrity": "sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA==", - "cpu": [ - "arm64" - ], + "node_modules/follow-redirects": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "darwin" + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } ], + "license": "MIT", "engines": { - "node": ">= 12.0.0" + "node": ">=4.0" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "peerDependenciesMeta": { + "debug": { + "optional": true + } } }, - "node_modules/lightningcss-darwin-x64": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.2.tgz", - "integrity": "sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ==", - "cpu": [ - "x64" - ], + "node_modules/form-data": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", + "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 12.0.0" + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.4", + "mime-types": "^2.1.35" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "engines": { + "node": ">= 6" } }, - "node_modules/lightningcss-freebsd-x64": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.2.tgz", - "integrity": "sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA==", - "cpu": [ - "x64" - ], + "node_modules/fraction.js": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", + "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==", "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "freebsd" - ], + "license": "MIT", "engines": { - "node": ">= 12.0.0" + "node": "*" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "type": "github", + "url": "https://github.com/sponsors/rawify" } }, - "node_modules/lightningcss-linux-arm-gnueabihf": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.2.tgz", - "integrity": "sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA==", - "cpu": [ - "arm" - ], + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], + "license": "MIT", "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "node": ">= 0.6" } }, - "node_modules/lightningcss-linux-arm64-gnu": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.2.tgz", - "integrity": "sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A==", - "cpu": [ - "arm64" - ], + "node_modules/fs-extra": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-3.0.1.tgz", + "integrity": "sha512-V3Z3WZWVUYd8hoCL5xfXJCaHWYzmtwW5XWYSlLgERi8PWd8bx1kUHUk8L1BT57e49oKnDDD180mjfrHc1yA9rg==", "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.1.2", + "jsonfile": "^3.0.0", + "universalify": "^0.1.0" } }, - "node_modules/lightningcss-linux-arm64-musl": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.2.tgz", - "integrity": "sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==", - "cpu": [ - "arm64" - ], + "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, - "license": "MPL-2.0", + "hasInstallScript": true, + "license": "MIT", "optional": true, "os": [ - "linux" + "darwin" ], "engines": { - "node": ">= 12.0.0" - }, + "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==", + "dev": true, + "license": "MIT", "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/lightningcss-linux-x64-gnu": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.2.tgz", - "integrity": "sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==", - "cpu": [ - "x64" - ], + "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, - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], + "license": "ISC", "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "node": "6.* || 8.* || >= 10.*" } }, - "node_modules/lightningcss-linux-x64-musl": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.2.tgz", - "integrity": "sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==", - "cpu": [ - "x64" - ], + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, "engines": { - "node": ">= 12.0.0" + "node": ">= 0.4" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/lightningcss-win32-arm64-msvc": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.2.tgz", - "integrity": "sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==", - "cpu": [ - "arm64" - ], + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "win32" - ], + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, "engines": { - "node": ">= 12.0.0" + "node": ">= 0.4" + } + }, + "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, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "engines": { + "node": ">= 6" } }, - "node_modules/lightningcss-win32-x64-msvc": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.2.tgz", - "integrity": "sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw==", - "cpu": [ - "x64" - ], + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "win32" - ], + "license": "MIT", "engines": { - "node": ">= 12.0.0" + "node": ">= 0.4" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/limiter": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/limiter/-/limiter-1.1.5.tgz", - "integrity": "sha512-FWWMIEOxz3GwUI4Ts/IvgVy6LPvoMPgjMdQ185nN6psJyBJ4yOpzqm695/h5umdLJg2vW3GR5iG11MAkR2AzJA==", - "dev": true - }, - "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "dev": true, - "license": "MIT" - }, - "node_modules/lodash-es": { - "version": "4.17.22", - "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.22.tgz", - "integrity": "sha512-XEawp1t0gxSi9x01glktRZ5HDy0HXqrM0x5pXQM98EaI0NxO6jVM7omDOxsuEo5UIASAnm2bRp1Jt/e0a2XU8Q==", + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", "dev": true, - "license": "MIT" + "license": "ISC" }, - "node_modules/lodash.isfinite": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/lodash.isfinite/-/lodash.isfinite-3.3.2.tgz", - "integrity": "sha512-7FGG40uhC8Mm633uKW1r58aElFlBlxCrg9JfSi3P6aYiWmfiWF0PgMd86ZUsxE5GwWPdHoS2+48bwTh2VPkIQA==", + "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, - "license": "MIT" - }, - "node_modules/magic-string": { - "version": "0.30.21", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", - "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.5" + "engines": { + "node": ">=8" } }, - "node_modules/math-intrinsics": { + "node_modules/has-symbols": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", - "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/micromatch": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", - "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", "dev": true, "license": "MIT", "dependencies": { - "braces": "^3.0.3", - "picomatch": "^2.3.1" + "has-symbols": "^1.0.3" }, "engines": { - "node": ">=8.6" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/mime": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", "dev": true, "license": "MIT", - "bin": { - "mime": "cli.js" + "dependencies": { + "function-bind": "^1.1.2" }, "engines": { - "node": ">=4" + "node": ">= 0.4" } }, - "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==", + "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, "license": "MIT", + "bin": { + "he": "bin/he" + } + }, + "node_modules/highlight.js": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.12.0.tgz", + "integrity": "sha512-nbfWpyRMcMrPMmDwJB+dhX/eiaPKtc2RB+0QZskqJ3WjRA/FDS0e9hZrx8EC/lbEv8gXy98FcDbNa/dspAaJMg==", + "dev": true, + "license": "BSD-3-Clause", "engines": { - "node": ">= 0.6" + "node": ">=12.0.0" } }, - "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==", + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", "dev": true, "license": "MIT", "dependencies": { - "mime-db": "1.52.0" + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" }, "engines": { - "node": ">= 0.6" + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, - "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "node_modules/http-proxy": { + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", + "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==", "dev": true, - "license": "ISC", + "license": "MIT", "dependencies": { - "brace-expansion": "^1.1.7" + "eventemitter3": "^4.0.0", + "follow-redirects": "^1.0.0", + "requires-port": "^1.0.0" }, "engines": { - "node": "*" + "node": ">=8.0.0" } }, - "node_modules/mitt": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", - "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", - "dev": true, - "license": "MIT" - }, - "node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "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, - "license": "MIT" + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } }, - "node_modules/msw": { - "version": "2.12.4", - "resolved": "https://registry.npmjs.org/msw/-/msw-2.12.4.tgz", - "integrity": "sha512-rHNiVfTyKhzc0EjoXUBVGteNKBevdjOlVC6GlIRXpy+/3LHEIGRovnB5WPjcvmNODVQ1TNFnoa7wsGbd0V3epg==", + "node_modules/https-proxy-agent/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "dev": true, - "hasInstallScript": true, "license": "MIT", "dependencies": { - "@inquirer/confirm": "^5.0.0", - "@mswjs/interceptors": "^0.40.0", - "@open-draft/deferred-promise": "^2.2.0", - "@types/statuses": "^2.0.6", - "cookie": "^1.0.2", - "graphql": "^16.12.0", - "headers-polyfill": "^4.0.2", - "is-node-process": "^1.2.0", - "outvariant": "^1.4.3", - "path-to-regexp": "^6.3.0", - "picocolors": "^1.1.1", - "rettime": "^0.7.0", - "statuses": "^2.0.2", - "strict-event-emitter": "^0.5.1", - "tough-cookie": "^6.0.0", - "type-fest": "^5.2.0", - "until-async": "^3.0.2", - "yargs": "^17.7.2" - }, - "bin": { - "msw": "cli/index.js" + "ms": "^2.1.3" }, "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/mswjs" - }, - "peerDependencies": { - "typescript": ">= 4.8.x" + "node": ">=6.0" }, "peerDependenciesMeta": { - "typescript": { + "supports-color": { "optional": true } } }, - "node_modules/mute-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-2.0.0.tgz", - "integrity": "sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==", + "node_modules/https-proxy-agent/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "dev": true, - "license": "ISC", + "license": "MIT" + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": ">=0.10.0" } }, - "node_modules/naive-ui": { - "version": "2.43.2", - "resolved": "https://registry.npmjs.org/naive-ui/-/naive-ui-2.43.2.tgz", - "integrity": "sha512-YlLMnGrwGTOc+zMj90sG3ubaH5/7czsgLgGcjTLA981IUaz8r6t4WIujNt8r9PNr+dqv6XNEr0vxkARgPPjfBQ==", + "node_modules/immutable": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.4.tgz", + "integrity": "sha512-p6u1bG3YSnINT5RQmx/yRZBpenIl30kVxkTLDyHLIMk0gict704Q9n+thfDI7lTRm9vXdDYutVzXhzcThxTnXA==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { - "@css-render/plugin-bem": "^0.15.14", - "@css-render/vue3-ssr": "^0.15.14", - "@types/katex": "^0.16.2", - "@types/lodash": "^4.17.20", - "@types/lodash-es": "^4.17.12", - "async-validator": "^4.2.5", - "css-render": "^0.15.14", - "csstype": "^3.1.3", - "date-fns": "^4.1.0", - "date-fns-tz": "^3.2.0", - "evtd": "^0.2.4", - "highlight.js": "^11.8.0", - "lodash": "^4.17.21", - "lodash-es": "^4.17.21", - "seemly": "^0.3.10", - "treemate": "^0.3.11", - "vdirs": "^0.1.8", - "vooks": "^0.2.12", - "vueuc": "^0.4.65" + "binary-extensions": "^2.0.0" }, - "peerDependencies": { - "vue": "^3.0.0" + "engines": { + "node": ">=8" } }, - "node_modules/nanoid": { - "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], + "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, "license": "MIT", - "bin": { - "nanoid": "bin/nanoid.cjs" - }, "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + "node": ">=0.10.0" } }, - "node_modules/negotiator": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", - "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "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, "license": "MIT", "engines": { - "node": ">= 0.6" + "node": ">=8" } }, - "node_modules/node-addon-api": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", - "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "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, "license": "MIT", - "optional": true + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } }, - "node_modules/node-releases": { - "version": "2.0.27", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", - "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "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, - "license": "MIT" + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } }, - "node_modules/normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "node_modules/is-number-like": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/is-number-like/-/is-number-like-1.0.8.tgz", + "integrity": "sha512-6rZi3ezCyFcn5L71ywzz2bS5b2Igl1En3eTlZlvKjpz1n3IZLAYMbKYAIQgFmEu0GENg92ziU/faEOA/aixjbA==", + "dev": true, + "license": "ISC", + "dependencies": { + "lodash.isfinite": "^3.3.2" + } + }, + "node_modules/is-wsl": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-1.1.0.tgz", + "integrity": "sha512-gfygJYZ2gLTDlmbWMI0CE2MwnFzSN/2SZfkMlItC4K/JBlsWVDB0bO6XhqcY13YXE7iMcAJnzTCJjPiTeJJ0Mw==", "dev": true, "license": "MIT", "engines": { - "node": ">=0.10.0" + "node": ">=4" } }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "node_modules/jiti": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", + "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", "dev": true, "license": "MIT", - "engines": { - "node": ">=0.10.0" + "bin": { + "jiti": "lib/jiti-cli.mjs" } }, - "node_modules/on-finished": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", - "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==", + "node_modules/js-tokens": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsonfile": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-3.0.1.tgz", + "integrity": "sha512-oBko6ZHlubVB5mRFkur5vgYR1UyqX+S6Y/oCfLhqNdcc2fYFlDpIoNc7AfKS1KOGcnNAkvsr0grLck9ANM815w==", "dev": true, "license": "MIT", - "dependencies": { - "ee-first": "1.1.1" - }, - "engines": { - "node": ">= 0.8" + "optionalDependencies": { + "graceful-fs": "^4.1.6" } }, - "node_modules/opn": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/opn/-/opn-5.3.0.tgz", - "integrity": "sha512-bYJHo/LOmoTd+pfiYhfZDnf9zekVJrY+cnS2a5F2x+w5ppvTqObojTP7WiFG+kVZs9Inw+qQ/lw7TroWwhdd2g==", + "node_modules/lightningcss": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.2.tgz", + "integrity": "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==", "dev": true, - "license": "MIT", + "license": "MPL-2.0", "dependencies": { - "is-wsl": "^1.1.0" + "detect-libc": "^2.0.3" }, "engines": { - "node": ">=4" + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.30.2", + "lightningcss-darwin-arm64": "1.30.2", + "lightningcss-darwin-x64": "1.30.2", + "lightningcss-freebsd-x64": "1.30.2", + "lightningcss-linux-arm-gnueabihf": "1.30.2", + "lightningcss-linux-arm64-gnu": "1.30.2", + "lightningcss-linux-arm64-musl": "1.30.2", + "lightningcss-linux-x64-gnu": "1.30.2", + "lightningcss-linux-x64-musl": "1.30.2", + "lightningcss-win32-arm64-msvc": "1.30.2", + "lightningcss-win32-x64-msvc": "1.30.2" } }, - "node_modules/outvariant": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/outvariant/-/outvariant-1.4.3.tgz", - "integrity": "sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA==", - "dev": true, - "license": "MIT" - }, - "node_modules/parseurl": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "node_modules/lightningcss-android-arm64": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.30.2.tgz", + "integrity": "sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "MIT", + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], "engines": { - "node": ">= 0.8" + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/path-to-regexp": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz", - "integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==", + "node_modules/lightningcss-darwin-arm64": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.2.tgz", + "integrity": "sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "MIT" + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } }, - "node_modules/perfect-debounce": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz", - "integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==", + "node_modules/lightningcss-darwin-x64": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.2.tgz", + "integrity": "sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ==", + "cpu": [ + "x64" + ], "dev": true, - "license": "MIT" - }, - "node_modules/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "license": "ISC" + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } }, - "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "node_modules/lightningcss-freebsd-x64": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.2.tgz", + "integrity": "sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA==", + "cpu": [ + "x64" + ], "dev": true, - "license": "MIT", + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], "engines": { - "node": ">=8.6" + "node": ">= 12.0.0" }, "funding": { - "url": "https://github.com/sponsors/jonschlinkert" + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/pinia": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/pinia/-/pinia-3.0.4.tgz", - "integrity": "sha512-l7pqLUFTI/+ESXn6k3nu30ZIzW5E2WZF/LaHJEpoq6ElcLD+wduZoB2kBN19du6K/4FDpPMazY2wJr+IndBtQw==", + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.2.tgz", + "integrity": "sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA==", + "cpu": [ + "arm" + ], "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@vue/devtools-api": "^7.7.7" + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" }, "funding": { - "url": "https://github.com/sponsors/posva" - }, - "peerDependencies": { - "typescript": ">=4.5.0", - "vue": "^3.5.11" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/portscanner": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/portscanner/-/portscanner-2.2.0.tgz", - "integrity": "sha512-IFroCz/59Lqa2uBvzK3bKDbDDIEaAY8XJ1jFxcLWTqosrsc32//P4VuSB2vZXoHiHqOmx8B5L5hnKOxL/7FlPw==", + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.2.tgz", + "integrity": "sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "MIT", - "dependencies": { - "async": "^2.6.0", - "is-number-like": "^1.0.3" - }, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=0.4", - "npm": ">=1.0.0" + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/postcss": { - "version": "8.5.6", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", - "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", - "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" - } + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.2.tgz", + "integrity": "sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==", + "cpu": [ + "arm64" ], - "license": "MIT", - "peer": true, - "dependencies": { - "nanoid": "^3.3.11", - "picocolors": "^1.1.1", - "source-map-js": "^1.2.1" + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" }, - "engines": { - "node": "^10 || ^12 || >=14" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/postcss-attribute-case-insensitive": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/postcss-attribute-case-insensitive/-/postcss-attribute-case-insensitive-6.0.3.tgz", - "integrity": "sha512-KHkmCILThWBRtg+Jn1owTnHPnFit4OkqS+eKiGEOPIGke54DCeYGJ6r0Fx/HjfE9M9kznApCLcU0DvnPchazMQ==", + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.2.tgz", + "integrity": "sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==", + "cpu": [ + "x64" + ], "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" ], - "license": "MIT", - "dependencies": { - "postcss-selector-parser": "^6.0.13" - }, "engines": { - "node": "^14 || ^16 || >=18" + "node": ">= 12.0.0" }, - "peerDependencies": { - "postcss": "^8.4" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/postcss-clamp": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/postcss-clamp/-/postcss-clamp-4.1.0.tgz", - "integrity": "sha512-ry4b1Llo/9zz+PKC+030KUnPITTJAHeOwjfAyyB60eT0AorGLdzp52s31OsPRHRf8NchkgFoG2y6fCfn1IV1Ow==", + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.2.tgz", + "integrity": "sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==", + "cpu": [ + "x64" + ], "dev": true, - "license": "MIT", - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=7.6.0" + "node": ">= 12.0.0" }, - "peerDependencies": { - "postcss": "^8.4.6" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/postcss-color-functional-notation": { - "version": "6.0.14", - "resolved": "https://registry.npmjs.org/postcss-color-functional-notation/-/postcss-color-functional-notation-6.0.14.tgz", - "integrity": "sha512-dNUX+UH4dAozZ8uMHZ3CtCNYw8fyFAmqqdcyxMr7PEdM9jLXV19YscoYO0F25KqZYhmtWKQ+4tKrIZQrwzwg7A==", + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.2.tgz", + "integrity": "sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==", + "cpu": [ + "arm64" + ], "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" ], - "license": "MIT-0", - "dependencies": { - "@csstools/css-color-parser": "^2.0.4", - "@csstools/css-parser-algorithms": "^2.7.1", - "@csstools/css-tokenizer": "^2.4.1", - "@csstools/postcss-progressive-custom-properties": "^3.3.0", - "@csstools/utilities": "^1.0.0" - }, "engines": { - "node": "^14 || ^16 || >=18" + "node": ">= 12.0.0" }, - "peerDependencies": { - "postcss": "^8.4" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/postcss-color-hex-alpha": { - "version": "9.0.4", - "resolved": "https://registry.npmjs.org/postcss-color-hex-alpha/-/postcss-color-hex-alpha-9.0.4.tgz", - "integrity": "sha512-XQZm4q4fNFqVCYMGPiBjcqDhuG7Ey2xrl99AnDJMyr5eDASsAGalndVgHZF8i97VFNy1GQeZc4q2ydagGmhelQ==", + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.2.tgz", + "integrity": "sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw==", + "cpu": [ + "x64" + ], "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" ], - "license": "MIT", - "dependencies": { - "@csstools/utilities": "^1.0.0", - "postcss-value-parser": "^4.2.0" - }, "engines": { - "node": "^14 || ^16 || >=18" + "node": ">= 12.0.0" }, - "peerDependencies": { - "postcss": "^8.4" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/postcss-color-rebeccapurple": { - "version": "9.0.3", - "resolved": "https://registry.npmjs.org/postcss-color-rebeccapurple/-/postcss-color-rebeccapurple-9.0.3.tgz", - "integrity": "sha512-ruBqzEFDYHrcVq3FnW3XHgwRqVMrtEPLBtD7K2YmsLKVc2jbkxzzNEctJKsPCpDZ+LeMHLKRDoSShVefGc+CkQ==", + "node_modules/limiter": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/limiter/-/limiter-1.1.5.tgz", + "integrity": "sha512-FWWMIEOxz3GwUI4Ts/IvgVy6LPvoMPgjMdQ185nN6psJyBJ4yOpzqm695/h5umdLJg2vW3GR5iG11MAkR2AzJA==", + "dev": true + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT-0", + "license": "MIT" + }, + "node_modules/lodash-es": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.18.1.tgz", + "integrity": "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.isfinite": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/lodash.isfinite/-/lodash.isfinite-3.3.2.tgz", + "integrity": "sha512-7FGG40uhC8Mm633uKW1r58aElFlBlxCrg9JfSi3P6aYiWmfiWF0PgMd86ZUsxE5GwWPdHoS2+48bwTh2VPkIQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", "dependencies": { - "@csstools/utilities": "^1.0.0", - "postcss-value-parser": "^4.2.0" - }, + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", "engines": { - "node": "^14 || ^16 || >=18" - }, - "peerDependencies": { - "postcss": "^8.4" + "node": ">= 0.4" } }, - "node_modules/postcss-custom-media": { - "version": "10.0.8", - "resolved": "https://registry.npmjs.org/postcss-custom-media/-/postcss-custom-media-10.0.8.tgz", - "integrity": "sha512-V1KgPcmvlGdxTel4/CyQtBJEFhMVpEmRGFrnVtgfGIHj5PJX9vO36eFBxKBeJn+aCDTed70cc+98Mz3J/uVdGQ==", + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], "license": "MIT", "dependencies": { - "@csstools/cascade-layer-name-parser": "^1.0.13", - "@csstools/css-parser-algorithms": "^2.7.1", - "@csstools/css-tokenizer": "^2.4.1", - "@csstools/media-query-list-parser": "^2.1.13" + "braces": "^3.0.3", + "picomatch": "^2.3.1" }, "engines": { - "node": "^14 || ^16 || >=18" - }, - "peerDependencies": { - "postcss": "^8.4" + "node": ">=8.6" } }, - "node_modules/postcss-custom-properties": { - "version": "13.3.12", - "resolved": "https://registry.npmjs.org/postcss-custom-properties/-/postcss-custom-properties-13.3.12.tgz", - "integrity": "sha512-oPn/OVqONB2ZLNqN185LDyaVByELAA/u3l2CS2TS16x2j2XsmV4kd8U49+TMxmUsEU9d8fB/I10E6U7kB0L1BA==", + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], "license": "MIT", - "dependencies": { - "@csstools/cascade-layer-name-parser": "^1.0.13", - "@csstools/css-parser-algorithms": "^2.7.1", - "@csstools/css-tokenizer": "^2.4.1", - "@csstools/utilities": "^1.0.0", - "postcss-value-parser": "^4.2.0" + "bin": { + "mime": "cli.js" }, "engines": { - "node": "^14 || ^16 || >=18" - }, - "peerDependencies": { - "postcss": "^8.4" + "node": ">=4" } }, - "node_modules/postcss-custom-selectors": { - "version": "7.1.12", - "resolved": "https://registry.npmjs.org/postcss-custom-selectors/-/postcss-custom-selectors-7.1.12.tgz", - "integrity": "sha512-ctIoprBMJwByYMGjXG0F7IT2iMF2hnamQ+aWZETyBM0aAlyaYdVZTeUkk8RB+9h9wP+NdN3f01lfvKl2ZSqC0g==", + "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, + "license": "MIT", + "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, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], "license": "MIT", "dependencies": { - "@csstools/cascade-layer-name-parser": "^1.0.13", - "@csstools/css-parser-algorithms": "^2.7.1", - "@csstools/css-tokenizer": "^2.4.1", - "postcss-selector-parser": "^6.1.0" + "mime-db": "1.52.0" }, "engines": { - "node": "^14 || ^16 || >=18" - }, - "peerDependencies": { - "postcss": "^8.4" + "node": ">= 0.6" } }, - "node_modules/postcss-dir-pseudo-class": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/postcss-dir-pseudo-class/-/postcss-dir-pseudo-class-8.0.1.tgz", - "integrity": "sha512-uULohfWBBVoFiZXgsQA24JV6FdKIidQ+ZqxOouhWwdE+qJlALbkS5ScB43ZTjPK+xUZZhlaO/NjfCt5h4IKUfw==", + "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, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT-0", + "license": "ISC", "dependencies": { - "postcss-selector-parser": "^6.0.13" + "brace-expansion": "^1.1.7" }, "engines": { - "node": "^14 || ^16 || >=18" - }, - "peerDependencies": { - "postcss": "^8.4" + "node": "*" } }, - "node_modules/postcss-double-position-gradients": { - "version": "5.0.7", - "resolved": "https://registry.npmjs.org/postcss-double-position-gradients/-/postcss-double-position-gradients-5.0.7.tgz", - "integrity": "sha512-1xEhjV9u1s4l3iP5lRt1zvMjI/ya8492o9l/ivcxHhkO3nOz16moC4JpMxDUGrOs4R3hX+KWT7gKoV842cwRgg==", + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT-0", + "license": "MIT" + }, + "node_modules/muggle-string": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/muggle-string/-/muggle-string-0.4.1.tgz", + "integrity": "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/naive-ui": { + "version": "2.45.3", + "resolved": "https://registry.npmjs.org/naive-ui/-/naive-ui-2.45.3.tgz", + "integrity": "sha512-D83gJ+6zUdkQ5DuUbmlfyaM1t8pV5WMPa6KmL9mByqQuiL12qBdA0n/iEIth4dQw1x/duNGpOS2anpt5FKO1kw==", + "dev": true, + "license": "MIT", "dependencies": { - "@csstools/postcss-progressive-custom-properties": "^3.3.0", - "@csstools/utilities": "^1.0.0", - "postcss-value-parser": "^4.2.0" + "@css-render/plugin-bem": "^0.15.14", + "@css-render/vue3-ssr": "^0.15.14", + "@types/lodash": "^4.17.20", + "@types/lodash-es": "^4.17.12", + "async-validator": "^4.2.5", + "css-render": "^0.15.14", + "csstype": "^3.1.3", + "date-fns": "^4.1.0", + "date-fns-tz": "^3.2.0", + "evtd": "^0.2.4", + "highlight.js": "^11.8.0", + "lodash": "^4.17.21", + "lodash-es": "^4.17.21", + "seemly": "^0.3.10", + "treemate": "^0.3.11", + "vdirs": "^0.1.8", + "vooks": "^0.2.12", + "vueuc": "^0.4.66" }, "engines": { - "node": "^14 || ^16 || >=18" + "node": ">=20" }, "peerDependencies": { - "postcss": "^8.4" + "vue": "^3.0.0" } }, - "node_modules/postcss-focus-visible": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/postcss-focus-visible/-/postcss-focus-visible-9.0.1.tgz", - "integrity": "sha512-N2VQ5uPz3Z9ZcqI5tmeholn4d+1H14fKXszpjogZIrFbhaq0zNAtq8sAnw6VLiqGbL8YBzsnu7K9bBkTqaRimQ==", + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", "dev": true, "funding": [ { "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" + "url": "https://github.com/sponsors/ai" } ], - "license": "MIT-0", - "dependencies": { - "postcss-selector-parser": "^6.0.13" + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" }, "engines": { - "node": "^14 || ^16 || >=18" - }, - "peerDependencies": { - "postcss": "^8.4" + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, - "node_modules/postcss-focus-within": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/postcss-focus-within/-/postcss-focus-within-8.0.1.tgz", - "integrity": "sha512-NFU3xcY/xwNaapVb+1uJ4n23XImoC86JNwkY/uduytSl2s9Ekc2EpzmRR63+ExitnW3Mab3Fba/wRPCT5oDILA==", + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT-0", - "dependencies": { - "postcss-selector-parser": "^6.0.13" - }, + "license": "MIT", "engines": { - "node": "^14 || ^16 || >=18" - }, - "peerDependencies": { - "postcss": "^8.4" + "node": ">= 0.6" } }, - "node_modules/postcss-font-variant": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/postcss-font-variant/-/postcss-font-variant-5.0.0.tgz", - "integrity": "sha512-1fmkBaCALD72CK2a9i468mA/+tr9/1cBxRRMXOUaZqO43oWPR5imcyPjXwuv7PXbCid4ndlP5zWhidQVVa3hmA==", + "node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", "dev": true, "license": "MIT", - "peerDependencies": { - "postcss": "^8.1.0" - } + "optional": true, + "peer": true }, - "node_modules/postcss-gap-properties": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/postcss-gap-properties/-/postcss-gap-properties-5.0.1.tgz", - "integrity": "sha512-k2z9Cnngc24c0KF4MtMuDdToROYqGMMUQGcE6V0odwjHyOHtaDBlLeRBV70y9/vF7KIbShrTRZ70JjsI1BZyWw==", + "node_modules/node-releases": { + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT-0", - "engines": { - "node": "^14 || ^16 || >=18" - }, - "peerDependencies": { - "postcss": "^8.4" - } + "license": "MIT" }, - "node_modules/postcss-image-set-function": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/postcss-image-set-function/-/postcss-image-set-function-6.0.3.tgz", - "integrity": "sha512-i2bXrBYzfbRzFnm+pVuxVePSTCRiNmlfssGI4H0tJQvDue+yywXwUxe68VyzXs7cGtMaH6MCLY6IbCShrSroCw==", + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT-0", - "dependencies": { - "@csstools/utilities": "^1.0.0", - "postcss-value-parser": "^4.2.0" - }, + "license": "MIT", "engines": { - "node": "^14 || ^16 || >=18" - }, - "peerDependencies": { - "postcss": "^8.4" + "node": ">=0.10.0" } }, - "node_modules/postcss-lab-function": { - "version": "6.0.19", - "resolved": "https://registry.npmjs.org/postcss-lab-function/-/postcss-lab-function-6.0.19.tgz", - "integrity": "sha512-vwln/mgvFrotJuGV8GFhpAOu9iGf3pvTBr6dLPDmUcqVD5OsQpEFyQMAFTxSxWXGEzBj6ld4pZ/9GDfEpXvo0g==", + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT-0", - "dependencies": { - "@csstools/css-color-parser": "^2.0.4", - "@csstools/css-parser-algorithms": "^2.7.1", - "@csstools/css-tokenizer": "^2.4.1", - "@csstools/postcss-progressive-custom-properties": "^3.3.0", - "@csstools/utilities": "^1.0.0" - }, + "license": "MIT", "engines": { - "node": "^14 || ^16 || >=18" - }, - "peerDependencies": { - "postcss": "^8.4" + "node": ">=0.10.0" } }, - "node_modules/postcss-logical": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/postcss-logical/-/postcss-logical-7.0.1.tgz", - "integrity": "sha512-8GwUQZE0ri0K0HJHkDv87XOLC8DE0msc+HoWLeKdtjDZEwpZ5xuK3QdV6FhmHSQW40LPkg43QzvATRAI3LsRkg==", + "node_modules/on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==", "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT-0", + "license": "MIT", "dependencies": { - "postcss-value-parser": "^4.2.0" + "ee-first": "1.1.1" }, "engines": { - "node": "^14 || ^16 || >=18" - }, - "peerDependencies": { - "postcss": "^8.4" + "node": ">= 0.8" } }, - "node_modules/postcss-nesting": { - "version": "12.1.5", - "resolved": "https://registry.npmjs.org/postcss-nesting/-/postcss-nesting-12.1.5.tgz", - "integrity": "sha512-N1NgI1PDCiAGWPTYrwqm8wpjv0bgDmkYHH72pNsqTCv9CObxjxftdYu6AKtGN+pnJa7FQjMm3v4sp8QJbFsYdQ==", + "node_modules/opn": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/opn/-/opn-5.3.0.tgz", + "integrity": "sha512-bYJHo/LOmoTd+pfiYhfZDnf9zekVJrY+cnS2a5F2x+w5ppvTqObojTP7WiFG+kVZs9Inw+qQ/lw7TroWwhdd2g==", "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT-0", + "license": "MIT", "dependencies": { - "@csstools/selector-resolve-nested": "^1.1.0", - "@csstools/selector-specificity": "^3.1.1", - "postcss-selector-parser": "^6.1.0" + "is-wsl": "^1.1.0" }, "engines": { - "node": "^14 || ^16 || >=18" - }, - "peerDependencies": { - "postcss": "^8.4" + "node": ">=4" } }, - "node_modules/postcss-opacity-percentage": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/postcss-opacity-percentage/-/postcss-opacity-percentage-2.0.0.tgz", - "integrity": "sha512-lyDrCOtntq5Y1JZpBFzIWm2wG9kbEdujpNt4NLannF+J9c8CgFIzPa80YQfdza+Y+yFfzbYj/rfoOsYsooUWTQ==", + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", "dev": true, - "funding": [ - { - "type": "kofi", - "url": "https://ko-fi.com/mrcgrtz" - }, - { - "type": "liberapay", - "url": "https://liberapay.com/mrcgrtz" - } - ], "license": "MIT", "engines": { - "node": "^14 || ^16 || >=18" - }, - "peerDependencies": { - "postcss": "^8.2" + "node": ">= 0.8" } }, - "node_modules/postcss-overflow-shorthand": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/postcss-overflow-shorthand/-/postcss-overflow-shorthand-5.0.1.tgz", - "integrity": "sha512-XzjBYKLd1t6vHsaokMV9URBt2EwC9a7nDhpQpjoPk2HRTSQfokPfyAS/Q7AOrzUu6q+vp/GnrDBGuj/FCaRqrQ==", + "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, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT-0", - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^14 || ^16 || >=18" - }, - "peerDependencies": { - "postcss": "^8.4" - } + "license": "MIT" }, - "node_modules/postcss-page-break": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/postcss-page-break/-/postcss-page-break-3.0.4.tgz", - "integrity": "sha512-1JGu8oCjVXLa9q9rFTo4MbeeA5FMe00/9C7lN4va606Rdb+HkxXtXsmEDrIraQ11fGz/WvKWa8gMuCKkrXpTsQ==", + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", "dev": true, - "license": "MIT", - "peerDependencies": { - "postcss": "^8" - } + "license": "MIT" }, - "node_modules/postcss-place": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/postcss-place/-/postcss-place-9.0.1.tgz", - "integrity": "sha512-JfL+paQOgRQRMoYFc2f73pGuG/Aw3tt4vYMR6UA3cWVMxivviPTnMFnFTczUJOA4K2Zga6xgQVE+PcLs64WC8Q==", + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT-0", - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, + "license": "MIT", "engines": { - "node": "^14 || ^16 || >=18" - }, - "peerDependencies": { - "postcss": "^8.4" + "node": ">= 14.16" } }, - "node_modules/postcss-preset-env": { - "version": "9.6.0", - "resolved": "https://registry.npmjs.org/postcss-preset-env/-/postcss-preset-env-9.6.0.tgz", - "integrity": "sha512-Lxfk4RYjUdwPCYkc321QMdgtdCP34AeI94z+/8kVmqnTIlD4bMRQeGcMZgwz8BxHrzQiFXYIR5d7k/9JMs2MEA==", + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT-0", - "dependencies": { - "@csstools/postcss-cascade-layers": "^4.0.6", - "@csstools/postcss-color-function": "^3.0.19", - "@csstools/postcss-color-mix-function": "^2.0.19", - "@csstools/postcss-content-alt-text": "^1.0.0", - "@csstools/postcss-exponential-functions": "^1.0.9", - "@csstools/postcss-font-format-keywords": "^3.0.2", - "@csstools/postcss-gamut-mapping": "^1.0.11", - "@csstools/postcss-gradients-interpolation-method": "^4.0.20", - "@csstools/postcss-hwb-function": "^3.0.18", - "@csstools/postcss-ic-unit": "^3.0.7", - "@csstools/postcss-initial": "^1.0.1", - "@csstools/postcss-is-pseudo-class": "^4.0.8", - "@csstools/postcss-light-dark-function": "^1.0.8", - "@csstools/postcss-logical-float-and-clear": "^2.0.1", - "@csstools/postcss-logical-overflow": "^1.0.1", - "@csstools/postcss-logical-overscroll-behavior": "^1.0.1", - "@csstools/postcss-logical-resize": "^2.0.1", - "@csstools/postcss-logical-viewport-units": "^2.0.11", - "@csstools/postcss-media-minmax": "^1.1.8", - "@csstools/postcss-media-queries-aspect-ratio-number-values": "^2.0.11", - "@csstools/postcss-nested-calc": "^3.0.2", - "@csstools/postcss-normalize-display-values": "^3.0.2", - "@csstools/postcss-oklab-function": "^3.0.19", - "@csstools/postcss-progressive-custom-properties": "^3.3.0", - "@csstools/postcss-relative-color-syntax": "^2.0.19", - "@csstools/postcss-scope-pseudo-class": "^3.0.1", - "@csstools/postcss-stepped-value-functions": "^3.0.10", - "@csstools/postcss-text-decoration-shorthand": "^3.0.7", - "@csstools/postcss-trigonometric-functions": "^3.0.10", - "@csstools/postcss-unset-value": "^3.0.1", - "autoprefixer": "^10.4.19", - "browserslist": "^4.23.1", - "css-blank-pseudo": "^6.0.2", - "css-has-pseudo": "^6.0.5", - "css-prefers-color-scheme": "^9.0.1", - "cssdb": "^8.1.0", - "postcss-attribute-case-insensitive": "^6.0.3", - "postcss-clamp": "^4.1.0", - "postcss-color-functional-notation": "^6.0.14", - "postcss-color-hex-alpha": "^9.0.4", - "postcss-color-rebeccapurple": "^9.0.3", - "postcss-custom-media": "^10.0.8", - "postcss-custom-properties": "^13.3.12", - "postcss-custom-selectors": "^7.1.12", - "postcss-dir-pseudo-class": "^8.0.1", - "postcss-double-position-gradients": "^5.0.7", - "postcss-focus-visible": "^9.0.1", - "postcss-focus-within": "^8.0.1", - "postcss-font-variant": "^5.0.0", - "postcss-gap-properties": "^5.0.1", - "postcss-image-set-function": "^6.0.3", - "postcss-lab-function": "^6.0.19", - "postcss-logical": "^7.0.1", - "postcss-nesting": "^12.1.5", - "postcss-opacity-percentage": "^2.0.0", - "postcss-overflow-shorthand": "^5.0.1", - "postcss-page-break": "^3.0.4", - "postcss-place": "^9.0.1", - "postcss-pseudo-class-any-link": "^9.0.2", - "postcss-replace-overflow-wrap": "^4.0.0", - "postcss-selector-not": "^7.0.2" - }, - "engines": { - "node": "^14 || ^16 || >=18" - }, - "peerDependencies": { - "postcss": "^8.4" - } + "license": "ISC" }, - "node_modules/postcss-pseudo-class-any-link": { - "version": "9.0.2", - "resolved": "https://registry.npmjs.org/postcss-pseudo-class-any-link/-/postcss-pseudo-class-any-link-9.0.2.tgz", - "integrity": "sha512-HFSsxIqQ9nA27ahyfH37cRWGk3SYyQLpk0LiWw/UGMV4VKT5YG2ONee4Pz/oFesnK0dn2AjcyequDbIjKJgB0g==", + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT-0", - "dependencies": { - "postcss-selector-parser": "^6.0.13" - }, + "license": "MIT", "engines": { - "node": "^14 || ^16 || >=18" + "node": ">=8.6" }, - "peerDependencies": { - "postcss": "^8.4" + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/postcss-replace-overflow-wrap": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/postcss-replace-overflow-wrap/-/postcss-replace-overflow-wrap-4.0.0.tgz", - "integrity": "sha512-KmF7SBPphT4gPPcKZc7aDkweHiKEEO8cla/GjcBK+ckKxiZslIu3C4GCRW3DNfL0o7yW7kMQu9xlZ1kXRXLXtw==", + "node_modules/portscanner": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/portscanner/-/portscanner-2.2.0.tgz", + "integrity": "sha512-IFroCz/59Lqa2uBvzK3bKDbDDIEaAY8XJ1jFxcLWTqosrsc32//P4VuSB2vZXoHiHqOmx8B5L5hnKOxL/7FlPw==", "dev": true, "license": "MIT", - "peerDependencies": { - "postcss": "^8.0.3" + "dependencies": { + "async": "^2.6.0", + "is-number-like": "^1.0.3" + }, + "engines": { + "node": ">=0.4", + "npm": ">=1.0.0" } }, - "node_modules/postcss-selector-not": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/postcss-selector-not/-/postcss-selector-not-7.0.2.tgz", - "integrity": "sha512-/SSxf/90Obye49VZIfc0ls4H0P6i6V1iHv0pzZH8SdgvZOPFkF37ef1r5cyWcMflJSFJ5bfuoluTnFnBBFiuSA==", + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", "dev": true, "funding": [ { - "type": "github", - "url": "https://github.com/sponsors/csstools" + "type": "opencollective", + "url": "https://opencollective.com/postcss/" }, { - "type": "opencollective", - "url": "https://opencollective.com/csstools" + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" } ], "license": "MIT", "dependencies": { - "postcss-selector-parser": "^6.0.13" - }, - "engines": { - "node": "^14 || ^16 || >=18" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/postcss-selector-parser": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", - "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" }, "engines": { - "node": ">=4" + "node": "^10 || ^12 || >=14" } }, "node_modules/postcss-value-parser": { @@ -6439,11 +4280,14 @@ "license": "MIT" }, "node_modules/proxy-from-env": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", + "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", "dev": true, - "license": "MIT" + "license": "MIT", + "engines": { + "node": ">=10" + } }, "node_modules/range-parser": { "version": "1.2.1", @@ -6477,6 +4321,8 @@ "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "engines": { "node": ">= 14.18.0" }, @@ -6515,20 +4361,6 @@ "node": ">= 0.8.0" } }, - "node_modules/rettime": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/rettime/-/rettime-0.7.0.tgz", - "integrity": "sha512-LPRKoHnLKd/r3dVxcwO7vhCW+orkOGj9ViueosEBK6ie89CijnfRlhaDhHq/3Hxu4CkWQtxwlBG0mzTQY6uQjw==", - "dev": true, - "license": "MIT" - }, - "node_modules/rfdc": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", - "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", - "dev": true, - "license": "MIT" - }, "node_modules/rollup": { "version": "4.53.5", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.53.5.tgz", @@ -6601,6 +4433,8 @@ "integrity": "sha512-KR0igP1z4avUJetEuIeOdDlwaUDvkH8wSx7FdSjyYBS3dpyX3TzHfAMO0G1Q4/3cdjcmi3r7idh+KCmKqS+KeQ==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "chokidar": "^4.0.0", "immutable": "^5.0.2", @@ -6800,18 +4634,12 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", "dev": true, - "license": "ISC", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } + "license": "ISC" }, "node_modules/socket.io": { "version": "4.8.1", @@ -6977,20 +4805,18 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" } }, - "node_modules/speakingurl": { - "version": "14.0.1", - "resolved": "https://registry.npmjs.org/speakingurl/-/speakingurl-14.0.1.tgz", - "integrity": "sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==", + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } + "license": "MIT" }, "node_modules/statuses": { "version": "2.0.2", @@ -7002,6 +4828,13 @@ "node": ">= 0.8" } }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, "node_modules/stream-throttle": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/stream-throttle/-/stream-throttle-0.1.3.tgz", @@ -7019,13 +4852,6 @@ "node": ">= 0.10.0" } }, - "node_modules/strict-event-emitter": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/strict-event-emitter/-/strict-event-emitter-0.5.1.tgz", - "integrity": "sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ==", - "dev": true, - "license": "MIT" - }, "node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", @@ -7054,17 +4880,17 @@ "node": ">=8" } }, - "node_modules/superjson": { - "version": "2.2.6", - "resolved": "https://registry.npmjs.org/superjson/-/superjson-2.2.6.tgz", - "integrity": "sha512-H+ue8Zo4vJmV2nRjpx86P35lzwDT3nItnIsocgumgr0hHMQ+ZGq5vrERg9kJBo5AWGmxZDhzDo+WVIJqkB0cGA==", + "node_modules/strip-literal": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz", + "integrity": "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==", "dev": true, "license": "MIT", "dependencies": { - "copy-anything": "^4" + "js-tokens": "^9.0.1" }, - "engines": { - "node": ">=16" + "funding": { + "url": "https://github.com/sponsors/antfu" } }, "node_modules/supports-color": { @@ -7080,19 +4906,6 @@ "node": ">=8" } }, - "node_modules/tagged-tag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/tagged-tag/-/tagged-tag-1.0.0.tgz", - "integrity": "sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/tailwindcss": { "version": "4.1.18", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.18.tgz", @@ -7114,6 +4927,20 @@ "url": "https://opencollective.com/webpack" } }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, "node_modules/tinyglobby": { "version": "0.2.15", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", @@ -7155,7 +4982,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -7163,25 +4989,35 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/tldts": { - "version": "7.0.19", - "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.19.tgz", - "integrity": "sha512-8PWx8tvC4jDB39BQw1m4x8y5MH1BcQ5xHeL2n7UVFulMPH/3Q0uiamahFJ3lXA0zO2SUyRXuVVbWSDmstlt9YA==", + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", "dev": true, "license": "MIT", - "dependencies": { - "tldts-core": "^7.0.19" - }, - "bin": { - "tldts": "bin/cli.js" + "engines": { + "node": "^18.0.0 || >=20.0.0" } }, - "node_modules/tldts-core": { - "version": "7.0.19", - "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.19.tgz", - "integrity": "sha512-lJX2dEWx0SGH4O6p+7FPwYmJ/bu1JbcGJ8RLaG9b7liIgZ85itUVEPbMtWRVrde/0fnDPEPHW10ZsKW3kVsE9A==", + "node_modules/tinyrainbow": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", + "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", "dev": true, - "license": "MIT" + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.4.tgz", + "integrity": "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } }, "node_modules/to-regex-range": { "version": "5.0.1", @@ -7206,19 +5042,6 @@ "node": ">=0.6" } }, - "node_modules/tough-cookie": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.0.tgz", - "integrity": "sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "tldts": "^7.0.5" - }, - "engines": { - "node": ">=16" - } - }, "node_modules/tree-kill": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", @@ -7243,29 +5066,12 @@ "dev": true, "license": "0BSD" }, - "node_modules/type-fest": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-5.3.1.tgz", - "integrity": "sha512-VCn+LMHbd4t6sF3wfU/+HKT63C9OoyrSIf4b+vtWHpt2U7/4InZG467YDNMFMR70DdHjAdpPWmw2lzRdg0Xqqg==", - "dev": true, - "license": "(MIT OR CC0-1.0)", - "dependencies": { - "tagged-tag": "^1.0.0" - }, - "engines": { - "node": ">=20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/typescript": { "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "devOptional": true, + "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -7328,16 +5134,6 @@ "node": ">= 0.8" } }, - "node_modules/until-async": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/until-async/-/until-async-3.0.2.tgz", - "integrity": "sha512-IiSk4HlzAMqTUseHHe3VhIGyuFmN90zMTpD3Z3y8jeQbzLIq500MVM7Jq2vUAnTKAFPJrqwkzr6PoTcPhGcOiw==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/kettanaito" - } - }, "node_modules/update-browserslist-db": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", @@ -7369,13 +5165,6 @@ "browserslist": ">= 4.21.0" } }, - "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, - "license": "MIT" - }, "node_modules/utils-merge": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", @@ -7415,7 +5204,6 @@ "integrity": "sha512-dZwN5L1VlUBewiP6H9s2+B3e3Jg96D0vzN+Ry73sOefebhYr9f94wwkMNN/9ouoU8pV1BqA1d1zGk8928cx0rg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", @@ -7485,6 +5273,54 @@ } } }, + "node_modules/vite-node": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz", + "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.4.1", + "es-module-lexer": "^1.7.0", + "pathe": "^2.0.3", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vite-node/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/vite-node/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, "node_modules/vite/node_modules/@esbuild/linux-loong64": { "version": "0.27.2", "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.2.tgz", @@ -7568,7 +5404,117 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/vitest": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.7.tgz", + "integrity": "sha512-KrxIJ62Fd89gfysR4WotlgZABiz2dqFPgqGzX7s+CwsqLFomRH7777ZcrOD6+WVAh7khPQP41A+BKbpcJFrdEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/expect": "3.2.7", + "@vitest/mocker": "3.2.7", + "@vitest/pretty-format": "^3.2.7", + "@vitest/runner": "3.2.7", + "@vitest/snapshot": "3.2.7", + "@vitest/spy": "3.2.7", + "@vitest/utils": "3.2.7", + "chai": "^5.2.0", + "debug": "^4.4.1", + "expect-type": "^1.2.1", + "magic-string": "^0.30.17", + "pathe": "^2.0.3", + "picomatch": "^4.0.2", + "std-env": "^3.9.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.14", + "tinypool": "^1.1.1", + "tinyrainbow": "^2.0.0", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", + "vite-node": "3.2.4", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/debug": "^4.1.12", + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "@vitest/browser": "3.2.7", + "@vitest/ui": "3.2.7", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/debug": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/vitest/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/vitest/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/vitest/node_modules/picomatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "dev": true, + "license": "MIT", "engines": { "node": ">=12" }, @@ -7589,12 +5535,19 @@ "vue": "^3.0.0" } }, + "node_modules/vscode-uri": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.2.0.tgz", + "integrity": "sha512-m2gXo3bn0G1kT9InzMf07fTbqMbGtyckj3bH5ktLO+1Ssv+yiATZ4dhwaQv9UZWxJh6E9IFGnQyjgWVDWVBDrg==", + "dev": true, + "license": "MIT" + }, "node_modules/vue": { "version": "3.5.26", "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.26.tgz", "integrity": "sha512-SJ/NTccVyAoNUJmkM9KUqPcYlY+u8OVL1X5EW9RIs3ch5H2uERxyyIUI4MRxVCSOiEcupX9xNGde1tL9ZKpimA==", + "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@vue/compiler-dom": "3.5.26", "@vue/compiler-sfc": "3.5.26", @@ -7611,33 +5564,27 @@ } } }, - "node_modules/vue-router": { - "version": "4.6.4", - "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.6.4.tgz", - "integrity": "sha512-Hz9q5sa33Yhduglwz6g9skT8OBPii+4bFn88w6J+J4MfEo4KRRpmiNG/hHHkdbRFlLBOqxN8y8gf2Fb0MTUgVg==", + "node_modules/vue-tsc": { + "version": "2.2.12", + "resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-2.2.12.tgz", + "integrity": "sha512-P7OP77b2h/Pmk+lZdJ0YWs+5tJ6J2+uOQPo7tlBnY44QqQSPYvS0qVT4wqDJgwrZaLe47etJLLQRFia71GYITw==", "dev": true, "license": "MIT", "dependencies": { - "@vue/devtools-api": "^6.6.4" + "@volar/typescript": "2.4.15", + "@vue/language-core": "2.2.12" }, - "funding": { - "url": "https://github.com/sponsors/posva" + "bin": { + "vue-tsc": "bin/vue-tsc.js" }, "peerDependencies": { - "vue": "^3.5.0" + "typescript": ">=5.0.0" } }, - "node_modules/vue-router/node_modules/@vue/devtools-api": { - "version": "6.6.4", - "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.6.4.tgz", - "integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==", - "dev": true, - "license": "MIT" - }, "node_modules/vueuc": { - "version": "0.4.65", - "resolved": "https://registry.npmjs.org/vueuc/-/vueuc-0.4.65.tgz", - "integrity": "sha512-lXuMl+8gsBmruudfxnMF9HW4be8rFziylXFu1VHVNbLVhRTXXV4njvpRuJapD/8q+oFEMSfQMH16E/85VoWRyQ==", + "version": "0.4.66", + "resolved": "https://registry.npmjs.org/vueuc/-/vueuc-0.4.66.tgz", + "integrity": "sha512-hHrZl+0AsrUvqYPBXSXQ2QaSrO3razjeUseLAhJWlOS/Hdfw/Y2MU05fOTP1qn1A3121gMxsHLaEz2V6CkWdTg==", "dev": true, "license": "MIT", "dependencies": { @@ -7653,16 +5600,18 @@ "vue": "^3.0.11" } }, - "node_modules/wrap-ansi": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", - "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", "dev": true, "license": "MIT", "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" }, "engines": { "node": ">=8" @@ -7737,19 +5686,6 @@ "engines": { "node": ">=12" } - }, - "node_modules/yoctocolors-cjs": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/yoctocolors-cjs/-/yoctocolors-cjs-2.1.3.tgz", - "integrity": "sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } } } } diff --git a/frontend/package.json b/frontend/package.json index b16930e..a1d6da1 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -7,24 +7,31 @@ "scripts": { "build": "vite build", "dev": "vite build --watch", - "debug": "PLUGIN_PATH=./dist npx @gameap/debug", - "dev:hmr": "concurrently -k -n build,debug,sync -c blue,green,yellow \"npm:dev\" \"npm:debug\" \"npx browser-sync start --proxy localhost:5174 --port 5175 --files dist/plugin.js,dist/*.css --no-open --no-notify\"" + "debug": "PLUGINS_PATH=./dist npx @gameap/debug", + "dev:hmr": "concurrently -k -n build,debug,sync -c blue,green,yellow \"npm:dev\" \"npm:debug\" \"npx browser-sync start --proxy localhost:5174 --port 5175 --files dist/plugin.js,dist/*.css --no-open --no-notify\"", + "typecheck": "vue-tsc --noEmit", + "test": "vitest run", + "test:watch": "vitest" }, "dependencies": { - "@gameap/plugin-sdk": "^0.2.0" + "@gameap/plugin-sdk": "file:../../gameap-api/web/plugin-sdk" }, "devDependencies": { - "@gameap/debug": "^0.2.17", + "@gameap/debug": "file:../../gameap-api/web/frontend/packages/gameap-debug", "@tailwindcss/postcss": "^4.1.18", "@vitejs/plugin-vue": "^6.0.1", "autoprefixer": "^10.4.23", + "axios": "^1.19.0", "browser-sync": "^3.0.4", "concurrently": "^9.2.1", + "naive-ui": "^2.44.1", "postcss": "^8.5.6", "tailwindcss": "^4.1.18", "typescript": "^5.3.0", "vite": "^7.1.0", - "vue": "^3.5.22" + "vitest": "^3.0.0", + "vue": "^3.5.22", + "vue-tsc": "^2.0.0" }, "peerDependencies": { "vue": "^3.5.0" diff --git a/frontend/src/__tests__/chunkCache.test.ts b/frontend/src/__tests__/chunkCache.test.ts new file mode 100644 index 0000000..2edb634 --- /dev/null +++ b/frontend/src/__tests__/chunkCache.test.ts @@ -0,0 +1,66 @@ +import { describe, expect, it } from 'vitest'; +import { ChunkCache, capacityFor, chunkIndexOf, chunkStart } from '../lib/chunkCache'; +import { CHUNK_SIZE, LRU_MAX_CHUNKS, LRU_MIN_CHUNKS } from '../lib/constants'; + +function chunk(fill: number): Uint8Array { + return new Uint8Array(4).fill(fill); +} + +describe('chunk addressing', () => { + it('divides rather than shifts, so a file past 4 GiB still works', () => { + const past4GiB = 5 * 1024 ** 3 + 12345; + const shiftWidth = Math.log2(CHUNK_SIZE); + expect(chunkIndexOf(past4GiB)).toBe(Math.floor(past4GiB / CHUNK_SIZE)); + // What a shift would have answered: the offset wraps at 32 bits. + expect(past4GiB >>> shiftWidth).not.toBe(chunkIndexOf(past4GiB)); + expect(chunkStart(chunkIndexOf(past4GiB))).toBeLessThanOrEqual(past4GiB); + }); + + it('sizes the cache between its bounds', () => { + expect(capacityFor(1)).toBe(LRU_MIN_CHUNKS); + expect(capacityFor(CHUNK_SIZE * 20)).toBe(20); + expect(capacityFor(CHUNK_SIZE * 1000)).toBe(LRU_MAX_CHUNKS); + }); +}); + +describe('ChunkCache', () => { + it('evicts the least recently used', () => { + const cache = new ChunkCache(2); + cache.set(0, chunk(0)); + cache.set(1, chunk(1)); + cache.set(2, chunk(2)); + expect(cache.has(0)).toBe(false); + expect(cache.has(1)).toBe(true); + expect(cache.has(2)).toBe(true); + expect(cache.size).toBe(2); + }); + + it('reading with get keeps a chunk alive, peek does not', () => { + const cache = new ChunkCache(2); + cache.set(0, chunk(0)); + cache.set(1, chunk(1)); + + cache.get(0); + cache.set(2, chunk(2)); + expect(cache.has(0)).toBe(true); + expect(cache.has(1)).toBe(false); + + cache.peek(0); + cache.set(3, chunk(3)); + expect(cache.has(0)).toBe(false); + }); + + it('patches a byte of a cached chunk only', () => { + const cache = new ChunkCache(4); + cache.set(1, new Uint8Array(CHUNK_SIZE)); + expect(cache.patch(CHUNK_SIZE + 5, 0xab)).toBe(true); + expect(cache.peek(1)?.[5]).toBe(0xab); + expect(cache.patch(CHUNK_SIZE * 3, 0xab)).toBe(false); + }); + + it('does not patch past the end of a short last chunk', () => { + const cache = new ChunkCache(4); + cache.set(0, new Uint8Array(10)); + expect(cache.patch(50, 1)).toBe(false); + }); +}); diff --git a/frontend/src/__tests__/chunkLoader.test.ts b/frontend/src/__tests__/chunkLoader.test.ts new file mode 100644 index 0000000..6538344 --- /dev/null +++ b/frontend/src/__tests__/chunkLoader.test.ts @@ -0,0 +1,208 @@ +import { describe, expect, it, vi } from 'vitest'; +import { FileChangedError, RangeNotSatisfiableError } from '../api/files'; +import { ChunkCache } from '../lib/chunkCache'; +import { ChunkLoader, splitRuns } from '../lib/chunkLoader'; +import { CHUNK_SIZE, MAX_RUN_CHUNKS } from '../lib/constants'; +import type { StaleInfo } from '../lib/document'; +import { FakeReader, flush } from './helpers'; + +const SIZE = CHUNK_SIZE * 40; + +function build(reader: FakeReader, size = SIZE) { + const cache = new ChunkCache(64); + const stale: StaleInfo[] = []; + /** What `inflightCount` was at each notification the view would see. */ + const seenInflight: number[] = []; + const loader = new ChunkLoader( + reader, + cache, + size, + { + onChange: () => seenInflight.push(loader.inflightCount), + onStale: (info) => stale.push(info), + }, + { delays: [1, 1, 1], sleep: async () => {} }, + ); + return { cache, loader, stale, seenInflight }; +} + +describe('splitRuns', () => { + it('groups consecutive indices and caps a run', () => { + expect(splitRuns([1, 2, 3, 7, 8], 4)).toEqual([ + { first: 1, last: 3 }, + { first: 7, last: 8 }, + ]); + expect(splitRuns([0, 1, 2, 3, 4, 5], 2)).toEqual([ + { first: 0, last: 1 }, + { first: 2, last: 3 }, + { first: 4, last: 5 }, + ]); + }); +}); + +describe('ChunkLoader', () => { + it('coalesces consecutive chunks into one request', async () => { + const reader = new FakeReader(SIZE); + const { loader, cache } = build(reader); + loader.ensureRange(0, CHUNK_SIZE * 2); + await flush(20); + expect(reader.calls.length).toBe(1); + expect(reader.calls[0].start).toBe(0); + expect(cache.has(0)).toBe(true); + expect(cache.has(1)).toBe(true); + }); + + it('never asks for one window twice at a time', async () => { + const reader = new FakeReader(SIZE); + reader.manual = true; + const { loader } = build(reader); + loader.ensureRange(0, 16); + await flush(); + loader.ensureRange(0, 16); + await flush(); + expect(reader.calls.length).toBe(1); + reader.release(); + await flush(20); + }); + + it('keeps at most two requests in flight', async () => { + const reader = new FakeReader(SIZE); + reader.manual = true; + const { loader } = build(reader); + loader.ensureRange(0, CHUNK_SIZE * 20); + await flush(20); + expect(reader.calls.length).toBe(2); + reader.release(); + await flush(40); + expect(reader.calls.length).toBeGreaterThan(2); + }); + + it('drops what the viewport no longer wants', async () => { + const reader = new FakeReader(SIZE); + reader.manual = true; + const { loader } = build(reader); + loader.ensureRange(0, CHUNK_SIZE * 12); + await flush(20); + const started = reader.calls.length; + + // The viewport jumps to the far end before the queue is drained. + loader.ensureRange(CHUNK_SIZE * 30, CHUNK_SIZE); + reader.release(); + await flush(40); + + const late = reader.calls.slice(started); + expect(late.length).toBeGreaterThan(0); + expect(late.every((call) => call.start >= CHUNK_SIZE * 28)).toBe(true); + }); + + it('retries a transient failure and keeps the window', async () => { + const reader = new FakeReader(SIZE, { failTimes: 2 }); + const { loader, cache } = build(reader); + loader.ensureRange(0, 16); + await flush(40); + expect(reader.calls.length).toBe(3); + expect(cache.has(0)).toBe(true); + expect(loader.failed.size).toBe(0); + }); + + it('does not retry a refusal, and offers the window again on demand', async () => { + const forbidden = () => Object.assign(new Error('nope'), { response: { status: 403 } }); + const reader = new FakeReader(SIZE, { failTimes: 99, failWith: forbidden }); + const { loader } = build(reader); + loader.ensureRange(0, 16); + await flush(40); + expect(reader.calls.length).toBe(1); + expect(loader.failed.has(0)).toBe(true); + expect(loader.lastError).toBeInstanceOf(Error); + + loader.ensureRange(0, 16); + await flush(10); + expect(reader.calls.length).toBe(1); + + loader.retryFailed(); + loader.ensureRange(0, 16); + await flush(40); + expect(reader.calls.length).toBe(2); + }); + + it('reports a file that changed size', async () => { + const reader = new FakeReader(SIZE, { reportTotal: SIZE + 1 }); + const { loader, stale } = build(reader); + loader.ensureRange(0, 16); + await flush(40); + expect(stale).toEqual([{ knownSize: SIZE, actualSize: SIZE + 1, reason: 'size' }]); + }); + + it('reports a range past the end as staleness too', async () => { + const reader = new FakeReader(SIZE, { + failTimes: 99, + failWith: () => new RangeNotSatisfiableError(10), + }); + const { loader, stale } = build(reader); + loader.ensureRange(0, 16); + await flush(40); + expect(stale[0]).toMatchObject({ reason: 'range', actualSize: 10 }); + }); + + it('stops asking once disposed', async () => { + const reader = new FakeReader(SIZE); + reader.manual = true; + const { loader, cache } = build(reader); + loader.ensureRange(0, 16); + await flush(); + loader.dispose(); + reader.release(); + await flush(20); + expect(cache.has(0)).toBe(false); + loader.ensureRange(0, 16); + await flush(); + expect(reader.calls.length).toBe(1); + }); + + it('says it has stopped loading once the last window is here', async () => { + const reader = new FakeReader(SIZE); + const { loader, seenInflight } = build(reader); + loader.ensureRange(0, 16); + await flush(40); + // The view reads the count when it is told to; the last thing it hears + // must be that nothing is in flight, or its spinner never stops. + expect(seenInflight[seenInflight.length - 1]).toBe(0); + expect(loader.inflightCount).toBe(0); + }); + + it('reads a short last chunk without complaint', async () => { + const size = CHUNK_SIZE + 100; + const reader = new FakeReader(size); + const { loader, cache } = build(reader, size); + loader.ensureRange(CHUNK_SIZE, 100); + await flush(40); + expect(cache.peek(1)?.length).toBe(100); + }); + + it('does nothing for an empty file', async () => { + const reader = new FakeReader(0); + const { loader } = build(reader, 0); + loader.ensureRange(0, 16); + await flush(10); + expect(reader.calls).toEqual([]); + }); + + it('splits a wide viewport into runs of the declared width', () => { + const indices = Array.from({ length: MAX_RUN_CHUNKS * 2 }, (_, i) => i); + const runs = splitRuns(indices, MAX_RUN_CHUNKS); + expect(runs).toHaveLength(2); + expect(runs[0].last - runs[0].first + 1).toBe(MAX_RUN_CHUNKS); + }); + + it('surfaces a changed file through the reader itself', async () => { + const reader = new FakeReader(SIZE, { + failTimes: 99, + failWith: () => new FileChangedError(SIZE, 5), + }); + const { loader, stale } = build(reader); + loader.ensureRange(0, 16); + await flush(40); + expect(stale[0]).toMatchObject({ reason: 'size', actualSize: 5 }); + expect(vi.isMockFunction(() => {})).toBe(false); + }); +}); diff --git a/frontend/src/__tests__/conflict.test.ts b/frontend/src/__tests__/conflict.test.ts new file mode 100644 index 0000000..3b81f2b --- /dev/null +++ b/frontend/src/__tests__/conflict.test.ts @@ -0,0 +1,25 @@ +import { describe, expect, it } from 'vitest'; +import { compareBaseline } from '../lib/conflict'; + +describe('compareBaseline', () => { + it('accepts a file that did not move', () => { + expect(compareBaseline({ size: 10, mtime: 5 }, { size: 10, mtime: 5 })).toBe('same'); + }); + + it('reports a changed modification time', () => { + expect(compareBaseline({ size: 10, mtime: 5 }, { size: 10, mtime: 6 })).toBe('mtime'); + }); + + it('reports a changed size first', () => { + expect(compareBaseline({ size: 10, mtime: 5 }, { size: 11, mtime: 6 })).toBe('size'); + }); + + it('compares only the size when the baseline has no time', () => { + expect(compareBaseline({ size: 10, mtime: null }, { size: 10, mtime: 99 })).toBe('same'); + expect(compareBaseline({ size: 10, mtime: null }, { size: 12, mtime: 99 })).toBe('size'); + }); + + it('has nothing to compare before the file was opened', () => { + expect(compareBaseline(null, { size: 1, mtime: 1 })).toBe('same'); + }); +}); diff --git a/frontend/src/__tests__/files.test.ts b/frontend/src/__tests__/files.test.ts new file mode 100644 index 0000000..36176d2 --- /dev/null +++ b/frontend/src/__tests__/files.test.ts @@ -0,0 +1,178 @@ +import { describe, expect, it, vi } from 'vitest'; +import type { HttpLike } from '../api/client'; +import { + FileChangedError, + HttpError, + PanelTooOldError, + RangeNotSatisfiableError, + RemoteFile, + fileInfoUrl, + fileUrl, + isTransient, + parseContentRange, + parseUnsatisfiedRange, +} from '../api/files'; +import { patternBytes } from './helpers'; + +function reader( + handler: (url: string, config: Parameters[1]) => unknown, +): HttpLike { + return { get: vi.fn(async (url, config) => handler(url, config) as never) }; +} + +function ok(status: number, data: unknown, headers: Record = {}) { + return { status, data, headers }; +} + +function fail(status: number, data: unknown = null, headers: Record = {}) { + throw Object.assign(new Error(`status ${status}`), { response: { status, data, headers } }); +} + +const SIZE = 4096; + +describe('url building', () => { + it('encodes everything a file name may hold', () => { + expect(fileUrl(3, 'a b.bin')).toContain('path=a%20b.bin'); + expect(fileUrl(3, 'weird#name%.bin')).toContain('path=weird%23name%25.bin'); + expect(fileUrl(3, 'карта/файл.bin')).toContain('%D0%BA%D0%B0%D1%80%D1%82%D0%B0'); + expect(fileUrl(3, 'a,b.bin')).toContain('path=a%2Cb.bin'); + expect(fileInfoUrl(12, 'x')).toBe('/api/plugins/hexeditor4jm2/servers/12/file/info?path=x'); + }); +}); + +describe('parseContentRange', () => { + it('reads a served range', () => { + expect(parseContentRange('bytes 0-1023/4096')).toEqual({ start: 0, end: 1023, total: 4096 }); + }); + + it('refuses anything that is not one', () => { + expect(parseContentRange('bytes */4096')).toBeNull(); + expect(parseContentRange('bytes 10-5/4096')).toBeNull(); + expect(parseContentRange('bytes 0-4096/4096')).toBeNull(); + expect(parseContentRange('garbage')).toBeNull(); + expect(parseContentRange(null)).toBeNull(); + }); + + it('reads the total of an unsatisfied range', () => { + expect(parseUnsatisfiedRange('bytes */4096')).toBe(4096); + expect(parseUnsatisfiedRange('bytes 0-1/2')).toBeNull(); + }); +}); + +describe('RemoteFile.info', () => { + it('returns what the plugin answered', async () => { + const file = new RemoteFile(3, 'x.bin', reader(() => ok(200, { size: 10, mtime: 5, path: 'x.bin' }))); + await expect(file.info()).resolves.toEqual({ size: 10, mtime: 5, path: 'x.bin' }); + }); + + it('refuses an answer it cannot read', async () => { + const file = new RemoteFile(3, 'x.bin', reader(() => ok(200, { size: 'big' }))); + await expect(file.info()).rejects.toBeInstanceOf(HttpError); + }); + + it('carries the plugin error message', async () => { + const body = new TextEncoder().encode('{"code":"NOT_FOUND","message":"file not found"}'); + const file = new RemoteFile(3, 'x.bin', reader(() => fail(404, body.buffer))); + await expect(file.info()).rejects.toMatchObject({ status: 404, code: 'NOT_FOUND', message: 'file not found' }); + }); +}); + +describe('RemoteFile.readRange', () => { + it('asks for the window and returns it', async () => { + const seen: Record[] = []; + const file = new RemoteFile( + 3, + 'x.bin', + reader((_url, config) => { + seen.push(config.headers ?? {}); + return ok(206, patternBytes(1024, 512).buffer, { 'Content-Range': 'bytes 1024-1535/4096' }); + }), + ); + const result = await file.readRange(1024, 1535, SIZE); + expect(seen[0].Range).toBe('bytes=1024-1535'); + expect(result.status).toBe(206); + expect(result.start).toBe(1024); + expect(result.total).toBe(SIZE); + expect([...result.bytes]).toEqual([...patternBytes(1024, 512)]); + }); + + it('reports a file whose size no longer matches', async () => { + const file = new RemoteFile( + 3, + 'x.bin', + reader(() => ok(206, new ArrayBuffer(512), { 'Content-Range': 'bytes 0-511/9999' })), + ); + await expect(file.readRange(0, 511, SIZE)).rejects.toBeInstanceOf(FileChangedError); + }); + + it('refuses a body shorter than its own Content-Range', async () => { + const file = new RemoteFile( + 3, + 'x.bin', + reader(() => ok(206, new ArrayBuffer(10), { 'Content-Range': 'bytes 0-511/4096' })), + ); + await expect(file.readRange(0, 511, SIZE)).rejects.toBeInstanceOf(HttpError); + }); + + it('reads an empty 200 as a panel that ignores file responses', async () => { + const file = new RemoteFile(3, 'x.bin', reader(() => ok(200, new ArrayBuffer(0)))); + await expect(file.readRange(0, 511, SIZE)).rejects.toBeInstanceOf(PanelTooOldError); + }); + + it('cuts the window out of a whole file when the range was dropped', async () => { + const whole = patternBytes(0, SIZE); + const file = new RemoteFile(3, 'x.bin', reader(() => ok(200, whole.buffer))); + const result = await file.readRange(100, 199, SIZE); + expect(result.status).toBe(200); + expect([...result.bytes]).toEqual([...patternBytes(100, 100)]); + }); + + it('turns a 416 into the size the panel reported', async () => { + const file = new RemoteFile( + 3, + 'x.bin', + reader(() => fail(416, null, { 'content-range': 'bytes */2048' })), + ); + await expect(file.readRange(0, 10, SIZE)).rejects.toMatchObject({ + name: 'RangeNotSatisfiableError', + total: 2048, + }); + }); + + it('never asks for a window past the size it knows', async () => { + const get = vi.fn(); + const file = new RemoteFile(3, 'x.bin', { get } as unknown as HttpLike); + await expect(file.readRange(SIZE, SIZE + 10, SIZE)).rejects.toBeInstanceOf(FileChangedError); + expect(get).not.toHaveBeenCalled(); + }); + + it('lets a cancellation through untouched', async () => { + const cancelled = Object.assign(new Error('canceled'), { code: 'ERR_CANCELED' }); + const file = new RemoteFile( + 3, + 'x.bin', + reader(() => { + throw cancelled; + }), + ); + await expect(file.readRange(0, 10, SIZE)).rejects.toMatchObject({ message: 'canceled' }); + }); +}); + +describe('isTransient', () => { + it('retries what may pass', () => { + expect(isTransient(new Error('network down'))).toBe(true); + expect(isTransient({ response: { status: 503 } })).toBe(true); + expect(isTransient({ response: { status: 429 } })).toBe(true); + expect(isTransient({ response: { status: 408 } })).toBe(true); + }); + + it('does not retry a decision', () => { + expect(isTransient({ response: { status: 403 } })).toBe(false); + expect(isTransient({ response: { status: 404 } })).toBe(false); + expect(isTransient({ response: { status: 501 } })).toBe(false); + expect(isTransient(new PanelTooOldError())).toBe(false); + expect(isTransient(new RangeNotSatisfiableError(10))).toBe(false); + expect(isTransient(new FileChangedError(10, 11))).toBe(false); + }); +}); diff --git a/frontend/src/__tests__/format.test.ts b/frontend/src/__tests__/format.test.ts new file mode 100644 index 0000000..50c89e8 --- /dev/null +++ b/frontend/src/__tests__/format.test.ts @@ -0,0 +1,71 @@ +import { describe, expect, it } from 'vitest'; +import { + formatByte, + formatExactBytes, + formatOffset, + formatSize, + offsetDigits, + parseOffset, + toAscii, +} from '../lib/format'; + +describe('offsetDigits', () => { + it('stays at eight up to four gigabytes', () => { + expect(offsetDigits(0)).toBe(8); + expect(offsetDigits(1)).toBe(8); + expect(offsetDigits(1024)).toBe(8); + expect(offsetDigits(0x1_0000_0000)).toBe(8); + }); + + it('grows past the four gigabyte boundary', () => { + expect(offsetDigits(0x1_0000_0001)).toBe(9); + expect(offsetDigits(5 * 1024 ** 3)).toBe(9); + expect(offsetDigits(2 ** 44)).toBe(11); + }); +}); + +describe('formatOffset', () => { + it('pads to the requested width', () => { + expect(formatOffset(0)).toBe('00000000'); + expect(formatOffset(255)).toBe('000000FF'); + expect(formatOffset(0x1_0000_0000, 9)).toBe('100000000'); + }); +}); + +describe('parseOffset', () => { + it('accepts hex with and without a prefix', () => { + expect(parseOffset('1f')).toBe(31); + expect(parseOffset('0x1F')).toBe(31); + expect(parseOffset(' 0X1f ')).toBe(31); + expect(parseOffset('100')).toBe(256); + }); + + it('refuses what it cannot address', () => { + expect(parseOffset('')).toBeNull(); + expect(parseOffset('0xZZ')).toBeNull(); + expect(parseOffset('-1')).toBeNull(); + expect(parseOffset('12 34')).toBeNull(); + expect(parseOffset('f'.repeat(16))).toBeNull(); + }); +}); + +describe('formatSize', () => { + it('uses binary units', () => { + expect(formatSize(0)).toBe('0 B'); + expect(formatSize(1023)).toBe('1023 B'); + expect(formatSize(1024)).toBe('1.0 KiB'); + expect(formatSize(11_197_597)).toBe('10.7 MiB'); + expect(formatSize(5 * 1024 ** 3)).toBe('5.0 GiB'); + }); +}); + +describe('byte helpers', () => { + it('formats and reads bytes', () => { + expect(formatByte(0)).toBe('00'); + expect(formatByte(171)).toBe('AB'); + expect(toAscii(65)).toBe('A'); + expect(toAscii(10)).toBe('.'); + expect(toAscii(127)).toBe('.'); + expect(formatExactBytes(11197597)).toBe('11 197 597'); + }); +}); diff --git a/frontend/src/__tests__/fullDocument.test.ts b/frontend/src/__tests__/fullDocument.test.ts new file mode 100644 index 0000000..2bb6bde --- /dev/null +++ b/frontend/src/__tests__/fullDocument.test.ts @@ -0,0 +1,103 @@ +import { describe, expect, it } from 'vitest'; +import { FullDocument } from '../lib/fullDocument'; + +function doc(bytes: number[]): FullDocument { + return new FullDocument(new Uint8Array(bytes)); +} + +describe('FullDocument', () => { + it('does not edit the buffer it was handed', () => { + const source = new Uint8Array([1, 2, 3]); + const document = new FullDocument(source); + document.writeByte(0, 0xff); + expect([...source]).toEqual([1, 2, 3]); + }); + + it('marks a byte as modified only while it differs', () => { + const document = doc([1, 2, 3]); + expect(document.hasUnsavedChanges()).toBe(false); + + document.writeByte(1, 0x20); + expect(document.isModified(1)).toBe(true); + expect(document.modifiedCount).toBe(1); + + document.writeByte(1, 2); + expect(document.isModified(1)).toBe(false); + expect(document.hasUnsavedChanges()).toBe(false); + }); + + it('appends a byte that is modified whatever its value', () => { + const document = doc([1]); + document.insertAt(1, 0xff); + expect(document.size).toBe(2); + expect(document.isModified(1)).toBe(true); + + // The old editor seeded the original with 0xFF, so typing FF here + // made an appended byte look untouched. + document.writeByte(1, 0xff); + expect(document.isModified(1)).toBe(true); + expect(document.structureModified).toBe(true); + }); + + it('shifts the diff when a byte is deleted', () => { + const document = doc([1, 2, 3, 4]); + document.deleteAt(1); + expect([...(document.readBytes(0, 3) ?? [])]).toEqual([1, 3, 4]); + // Everything from the cut on now lines up with a different original. + expect(document.isModified(1)).toBe(true); + expect(document.isModified(0)).toBe(false); + }); + + it('reads rows and multi-byte values', () => { + const document = doc(Array.from({ length: 20 }, (_, i) => i)); + expect(document.readRow(0)?.length).toBe(16); + expect([...(document.readRow(1) ?? [])]).toEqual([16, 17, 18, 19]); + expect(document.readRow(2)).toBeNull(); + expect([...(document.readBytes(2, 4) ?? [])]).toEqual([2, 3, 4, 5]); + expect(document.readBytes(18, 4)).toBeNull(); + }); + + it('writes a multi-byte value or none of it', () => { + const document = doc([1, 2, 3, 4]); + expect(document.writeBytes(2, new Uint8Array([9, 9, 9]))).toBe(false); + expect([...(document.readBytes(0, 4) ?? [])]).toEqual([1, 2, 3, 4]); + expect(document.writeBytes(1, new Uint8Array([9, 9]))).toBe(true); + expect([...(document.readBytes(0, 4) ?? [])]).toEqual([1, 9, 9, 4]); + }); + + it('takes what was written as the new baseline', async () => { + const document = doc([1, 2, 3]); + document.writeByte(0, 9); + const snapshot = await document.buildSnapshot(); + expect(snapshot.mode).toBe('full'); + expect(snapshot.size).toBe(3); + + document.markSaved(snapshot); + expect(document.hasUnsavedChanges()).toBe(false); + expect(document.isModified(0)).toBe(false); + expect(document.byteAt(0)).toBe(9); + }); + + it('gives the changes back on discard', () => { + const document = doc([1, 2, 3]); + document.writeByte(0, 9); + document.insertAt(3, 4); + document.discardChanges(); + expect(document.size).toBe(3); + expect([...(document.readBytes(0, 3) ?? [])]).toEqual([1, 2, 3]); + expect(document.hasUnsavedChanges()).toBe(false); + }); + + it('tells the view about every change', () => { + const document = doc([1, 2, 3]); + let notifications = 0; + const stop = document.subscribe(() => { + notifications += 1; + }); + document.writeByte(0, 9); + document.insertAt(3, 4); + stop(); + document.writeByte(0, 1); + expect(notifications).toBe(2); + }); +}); diff --git a/frontend/src/__tests__/helpers.ts b/frontend/src/__tests__/helpers.ts new file mode 100644 index 0000000..3e9eae4 --- /dev/null +++ b/frontend/src/__tests__/helpers.ts @@ -0,0 +1,76 @@ +import type { RangeResult } from '../api/files'; +import type { RangeReader } from '../lib/chunkLoader'; + +/** Deterministic bytes, so a range can be checked without holding the file. */ +export function patternByte(offset: number): number { + return (offset * 31 + (offset >> 8)) & 0xff; +} + +export function patternBytes(start: number, length: number): Uint8Array { + const out = new Uint8Array(length); + for (let i = 0; i < length; i += 1) out[i] = patternByte(start + i); + return out; +} + +export interface FakeReaderOptions { + /** Fails the first N calls with this error, then succeeds. */ + failTimes?: number; + failWith?: () => Error; + /** Answers this total instead of the real size (a file that changed). */ + reportTotal?: number; +} + +/** A `RangeReader` over generated bytes, with call recording. */ +export class FakeReader implements RangeReader { + readonly calls: Array<{ start: number; end: number }> = []; + private failures: number; + private resolvers: Array<() => void> = []; + /** When set, every read waits until `release()` is called. */ + manual = false; + + constructor( + readonly size: number, + private readonly options: FakeReaderOptions = {}, + ) { + this.failures = options.failTimes ?? 0; + } + + async readRange(start: number, end: number, expectedTotal: number): Promise { + this.calls.push({ start, end }); + if (this.manual) { + await new Promise((resolve) => this.resolvers.push(resolve)); + } + if (this.failures > 0) { + this.failures -= 1; + throw (this.options.failWith ?? (() => Object.assign(new Error('boom'), { response: { status: 503 } })))(); + } + const total = this.options.reportTotal ?? this.size; + if (total !== expectedTotal) { + const { FileChangedError } = await import('../api/files'); + throw new FileChangedError(expectedTotal, total); + } + const last = Math.min(end, this.size - 1); + return { + bytes: patternBytes(start, Math.max(0, last - start + 1)), + start, + total, + status: 206, + }; + } + + /** Lets every waiting read finish. */ + release(): void { + const waiting = this.resolvers; + this.resolvers = []; + for (const resolve of waiting) resolve(); + } + + get pending(): number { + return this.resolvers.length; + } +} + +/** Runs pending microtasks so queued loads get a chance to start. */ +export async function flush(times = 6): Promise { + for (let i = 0; i < times; i += 1) await Promise.resolve(); +} diff --git a/frontend/src/__tests__/mocks.test.ts b/frontend/src/__tests__/mocks.test.ts new file mode 100644 index 0000000..5223e58 --- /dev/null +++ b/frontend/src/__tests__/mocks.test.ts @@ -0,0 +1,65 @@ +import { describe, expect, it } from 'vitest'; +import { HARNESS_ROOT_FILES, SAMPLES, findSample, patternBytes, resolveRange } from '../mocks/files'; + +const SIZE = 4096; + +describe('resolveRange', () => { + it('serves the whole file without a header', () => { + expect(resolveRange(null, SIZE)).toEqual({ start: 0, end: SIZE - 1, partial: false, satisfiable: true }); + }); + + it('serves a closed range', () => { + expect(resolveRange('bytes=10-19', SIZE)).toMatchObject({ start: 10, end: 19, partial: true }); + }); + + it('clamps an end past the file, as the panel does', () => { + expect(resolveRange('bytes=4000-99999', SIZE)).toMatchObject({ start: 4000, end: SIZE - 1 }); + }); + + it('serves an open ended range to the end', () => { + expect(resolveRange('bytes=4000-', SIZE)).toMatchObject({ start: 4000, end: SIZE - 1 }); + }); + + it('serves a suffix range', () => { + expect(resolveRange('bytes=-100', SIZE)).toMatchObject({ start: SIZE - 100, end: SIZE - 1 }); + expect(resolveRange('bytes=-99999', SIZE)).toMatchObject({ start: 0, end: SIZE - 1 }); + }); + + it('refuses a start past the end', () => { + expect(resolveRange(`bytes=${SIZE}-`, SIZE)).toMatchObject({ satisfiable: false }); + }); + + it('falls back to the whole file for anything it cannot use', () => { + for (const header of ['bytes=0-1,5-6', 'items=0-1', 'bytes=abc', 'bytes=', 'bytes=10-5']) { + expect(resolveRange(header, SIZE)).toMatchObject({ partial: false, satisfiable: true }); + } + }); +}); + +describe('patternBytes', () => { + it('answers the same bytes wherever the window starts', () => { + const whole = patternBytes(0, 3000); + const window = patternBytes(1000, 500); + expect([...window]).toEqual([...whole.subarray(1000, 1500)]); + }); + + it('marks every mebibyte so a window can be recognised', () => { + const marker = patternBytes(1024 * 1024, 16); + expect(new TextDecoder().decode(marker)).toBe('MiB#0000000001 \n'); + expect(marker.length).toBe(16); + }); +}); + +describe('findSample', () => { + it('ignores a leading slash, as the file manager sends one', () => { + expect(findSample('/hex-samples/tiny.bin')?.size).toBe(7); + expect(findSample('hex-samples/tiny.bin')?.size).toBe(7); + expect(findSample('nothing.bin')).toBeUndefined(); + }); + + it('serves every file it lists, the harness root included', () => { + for (const path of [...SAMPLES, ...HARNESS_ROOT_FILES].map((entry) => entry.path)) { + expect(findSample(path), path).toBeDefined(); + } + }); +}); diff --git a/frontend/src/__tests__/reconstruct.test.ts b/frontend/src/__tests__/reconstruct.test.ts new file mode 100644 index 0000000..aa61293 --- /dev/null +++ b/frontend/src/__tests__/reconstruct.test.ts @@ -0,0 +1,76 @@ +import { describe, expect, it } from 'vitest'; +import { FileChangedError } from '../api/files'; +import { buildBlob } from '../lib/reconstruct'; +import { FakeReader, patternBytes } from './helpers'; + +async function bytesOf(blob: Blob): Promise { + return new Uint8Array(await blob.arrayBuffer()); +} + +describe('buildBlob', () => { + it('reads the file back block by block', async () => { + const reader = new FakeReader(1000); + const blob = await buildBlob(reader, 1000, new Map(), { blockSize: 256 }); + expect(reader.calls).toEqual([ + { start: 0, end: 255 }, + { start: 256, end: 511 }, + { start: 512, end: 767 }, + { start: 768, end: 999 }, + ]); + expect([...(await bytesOf(blob))]).toEqual([...patternBytes(0, 1000)]); + }); + + it('applies edits across a block boundary and in the short last block', async () => { + const reader = new FakeReader(1000); + const overlay = new Map([ + [0, 0x11], + [255, 0x22], + [256, 0x33], + [999, 0x44], + ]); + const bytes = await bytesOf(await buildBlob(reader, 1000, overlay, { blockSize: 256 })); + expect(bytes[0]).toBe(0x11); + expect(bytes[255]).toBe(0x22); + expect(bytes[256]).toBe(0x33); + expect(bytes[999]).toBe(0x44); + expect(bytes[1]).toBe(patternBytes(1, 1)[0]); + expect(bytes.length).toBe(1000); + }); + + it('reports progress in file terms', async () => { + const reader = new FakeReader(1000); + const seen: Array<[number, number]> = []; + await buildBlob(reader, 1000, new Map(), { + blockSize: 400, + onProgress: (done, total) => seen.push([done, total]), + }); + expect(seen).toEqual([ + [400, 1000], + [800, 1000], + [1000, 1000], + ]); + }); + + it('writes an empty file without reading anything', async () => { + const reader = new FakeReader(0); + const blob = await buildBlob(reader, 0, new Map()); + expect(blob.size).toBe(0); + expect(reader.calls).toEqual([]); + }); + + it('stops when the file changed under it', async () => { + const reader = new FakeReader(500); + await expect(buildBlob(reader, 1000, new Map(), { blockSize: 256 })).rejects.toBeInstanceOf( + FileChangedError, + ); + }); + + it('gives up when the caller aborts', async () => { + const reader = new FakeReader(1000); + const controller = new AbortController(); + controller.abort(); + await expect( + buildBlob(reader, 1000, new Map(), { blockSize: 256, signal: controller.signal }), + ).rejects.toBeTruthy(); + }); +}); diff --git a/frontend/src/__tests__/scroll.test.ts b/frontend/src/__tests__/scroll.test.ts new file mode 100644 index 0000000..0f3759a --- /dev/null +++ b/frontend/src/__tests__/scroll.test.ts @@ -0,0 +1,99 @@ +import { describe, expect, it } from 'vitest'; +import { MAX_SPACER_HEIGHT, ROW_HEIGHT } from '../lib/constants'; +import { + contentTranslateY, + firstRowToShow, + rowFromScrollTop, + scrollGeometry, + scrollTopFromRow, + wheelDeltaRows, +} from '../lib/scroll'; + +const VIEWPORT = 480; + +describe('scrollGeometry', () => { + it('maps rows one to one below the spacer cap', () => { + const geometry = scrollGeometry(1000, VIEWPORT); + expect(geometry.scaled).toBe(false); + expect(geometry.spacerHeight).toBe(1000 * ROW_HEIGHT); + expect(geometry.visibleRows).toBe(20); + expect(geometry.maxFirstRow).toBe(980); + }); + + it('caps the spacer for a file taller than the browser allows', () => { + const rows = Math.ceil((5 * 1024 ** 3) / 16); + const geometry = scrollGeometry(rows, VIEWPORT); + expect(geometry.scaled).toBe(true); + expect(geometry.spacerHeight).toBe(MAX_SPACER_HEIGHT); + expect(geometry.naturalHeight).toBeGreaterThan(MAX_SPACER_HEIGHT); + }); +}); + +describe('scroll position and rows', () => { + it('round trips unscaled', () => { + const geometry = scrollGeometry(1000, VIEWPORT); + for (const row of [0, 1, 500, geometry.maxFirstRow]) { + expect(rowFromScrollTop(geometry, scrollTopFromRow(geometry, row))).toBe(row); + } + }); + + it('round trips scaled at both ends and stays monotonic', () => { + const rows = Math.ceil((5 * 1024 ** 3) / 16); + const geometry = scrollGeometry(rows, VIEWPORT); + + expect(rowFromScrollTop(geometry, 0)).toBe(0); + expect(rowFromScrollTop(geometry, geometry.maxScrollTop)).toBe(geometry.maxFirstRow); + expect(scrollTopFromRow(geometry, geometry.maxFirstRow)).toBe(geometry.maxScrollTop); + + let previous = -1; + for (let scrollTop = 0; scrollTop <= geometry.maxScrollTop; scrollTop += 100_000) { + const row = rowFromScrollTop(geometry, scrollTop); + expect(row).toBeGreaterThanOrEqual(previous); + previous = row; + } + }); + + it('clamps out of range positions', () => { + const geometry = scrollGeometry(1000, VIEWPORT); + expect(rowFromScrollTop(geometry, -50)).toBe(0); + expect(rowFromScrollTop(geometry, 10_000_000)).toBe(geometry.maxFirstRow); + expect(scrollTopFromRow(geometry, -5)).toBe(0); + expect(scrollTopFromRow(geometry, 99_999)).toBe(geometry.maxScrollTop); + }); + + it('places the rendered block inside the spacer', () => { + const plain = scrollGeometry(1000, VIEWPORT); + expect(contentTranslateY(plain, 240, 10, 0)).toBe(0); + expect(contentTranslateY(plain, 240, 10, 5)).toBe(5 * ROW_HEIGHT); + + const rows = Math.ceil((5 * 1024 ** 3) / 16); + const scaled = scrollGeometry(rows, VIEWPORT); + expect(contentTranslateY(scaled, 5000, 100, 90)).toBe(5000 - 10 * ROW_HEIGHT); + }); +}); + +describe('wheelDeltaRows', () => { + it('understands the three delta modes', () => { + expect(wheelDeltaRows(48, 0, 20)).toBe(2); + expect(wheelDeltaRows(3, 1, 20)).toBe(3); + expect(wheelDeltaRows(1, 2, 20)).toBe(20); + }); +}); + +describe('firstRowToShow', () => { + const geometry = scrollGeometry(1000, VIEWPORT); + + it('does not move when the row is already visible', () => { + expect(firstRowToShow(geometry, 100, 105)).toBe(100); + }); + + it('moves as little as needed', () => { + expect(firstRowToShow(geometry, 100, 90)).toBe(90); + expect(firstRowToShow(geometry, 100, 130)).toBe(130 - geometry.visibleRows + 1); + }); + + it('centres on request', () => { + expect(firstRowToShow(geometry, 0, 500, 'center')).toBe(500 - 10); + expect(firstRowToShow(geometry, 0, 2, 'center')).toBe(0); + }); +}); diff --git a/frontend/src/__tests__/windowedDocument.test.ts b/frontend/src/__tests__/windowedDocument.test.ts new file mode 100644 index 0000000..6151cd4 --- /dev/null +++ b/frontend/src/__tests__/windowedDocument.test.ts @@ -0,0 +1,162 @@ +import { describe, expect, it } from 'vitest'; +import { CHUNK_SIZE, LRU_MIN_CHUNKS } from '../lib/constants'; +import { WindowedDocument } from '../lib/windowedDocument'; +import { FakeReader, flush, patternByte } from './helpers'; + +const SIZE = CHUNK_SIZE * 40; + +function build(size = SIZE, reader = new FakeReader(size)) { + const document = new WindowedDocument(reader, size, { + retry: { delays: [1], sleep: async () => {} }, + blockSize: 1024, + }); + return { document, reader }; +} + +async function load(document: WindowedDocument, offset: number, length = 16): Promise { + document.ensureRange(offset, length); + await flush(40); +} + +describe('WindowedDocument', () => { + it('knows nothing before a window is read', () => { + const { document } = build(); + expect(document.byteAt(0)).toBeUndefined(); + expect(document.readRow(0)).toBeNull(); + expect(document.isLoaded(0)).toBe(false); + }); + + it('answers bytes once the window is here', async () => { + const { document } = build(); + await load(document, 0); + expect(document.byteAt(0)).toBe(patternByte(0)); + expect([...(document.readRow(0) ?? [])]).toEqual( + Array.from({ length: 16 }, (_, i) => patternByte(i)), + ); + }); + + it('cannot insert or delete', async () => { + const { document } = build(); + await load(document, 0); + expect(document.canInsertDelete).toBe(false); + expect(document.insertAt()).toBe(false); + expect(document.deleteAt()).toBe(false); + }); + + it('refuses to write a byte it has not read', () => { + const { document } = build(); + expect(document.writeByte(0, 1)).toBe(false); + expect(document.modifiedCount).toBe(0); + }); + + it('marks a byte only while it differs from the server', async () => { + const { document } = build(); + await load(document, 0); + const original = patternByte(3); + + expect(document.writeByte(3, original ^ 0xff)).toBe(true); + expect(document.isModified(3)).toBe(true); + expect(document.byteAt(3)).toBe(original ^ 0xff); + + document.writeByte(3, original); + expect(document.isModified(3)).toBe(false); + expect(document.hasUnsavedChanges()).toBe(false); + }); + + it('remembers an edit after its window is evicted', async () => { + const size = CHUNK_SIZE * (LRU_MIN_CHUNKS + 4); + const { document } = build(size); + await load(document, 0); + document.writeByte(5, 0xaa); + + // Walk far enough that the first window is pushed out of the cache. + for (let index = 1; index <= LRU_MIN_CHUNKS + 3; index += 1) { + await load(document, CHUNK_SIZE * index); + } + + expect(document.byteAt(5)).toBe(0xaa); + expect(document.isModified(5)).toBe(true); + expect(document.modifiedCount).toBe(1); + }); + + it('writes a multi-byte value only when every byte is here', async () => { + const { document } = build(); + const boundary = CHUNK_SIZE - 2; + // Reading at the start of the file leaves the second window alone: + // the prefetch reaches 64 KiB, the boundary is at 256 KiB. + await load(document, 0); + + expect(document.isLoaded(boundary)).toBe(true); + expect(document.isLoaded(CHUNK_SIZE)).toBe(false); + // Half of the value lives in a window that has not been read. + expect(document.writeBytes(boundary, new Uint8Array([1, 2, 3, 4]))).toBe(false); + expect(document.modifiedCount).toBe(0); + + await load(document, CHUNK_SIZE, 16); + expect(document.writeBytes(boundary, new Uint8Array([1, 2, 3, 4]))).toBe(true); + expect([...(document.readBytes(boundary, 4) ?? [])]).toEqual([1, 2, 3, 4]); + expect(document.modifiedCount).toBe(4); + }); + + it('reads a short last row at the end of the file', async () => { + const size = CHUNK_SIZE + 20; + const { document } = build(size); + await load(document, CHUNK_SIZE, 20); + const lastRow = Math.floor((size - 1) / 16); + expect(document.readRow(lastRow)?.length).toBe(size - lastRow * 16); + expect(document.readRow(lastRow + 1)).toBeNull(); + }); + + it('keeps edits made while the file was being written out', async () => { + const { document } = build(); + await load(document, 0); + document.writeByte(0, 0xaa); + + const snapshot = await document.buildSnapshot(); + document.writeByte(1, 0xbb); + document.markSaved(snapshot); + + expect(document.isModified(0)).toBe(false); + expect(document.byteAt(0)).toBe(0xaa); + expect(document.isModified(1)).toBe(true); + expect(document.modifiedCount).toBe(1); + }); + + it('rebuilds the file with its edits applied', async () => { + const { document } = build(2048, new FakeReader(2048)); + await load(document, 0); + document.writeByte(0, 0xaa); + const snapshot = await document.buildSnapshot(); + expect(snapshot.mode).toBe('windowed'); + if (snapshot.mode !== 'windowed') return; + const bytes = new Uint8Array(await snapshot.payload.arrayBuffer()); + expect(bytes.length).toBe(2048); + expect(bytes[0]).toBe(0xaa); + expect(bytes[1]).toBe(patternByte(1)); + }); + + it('refuses everything once the file changed underneath', async () => { + const reader = new FakeReader(SIZE, { reportTotal: SIZE + 1 }); + const { document } = build(SIZE, reader); + await load(document, 0); + expect(document.stale).toMatchObject({ reason: 'size' }); + expect(document.writeByte(0, 1)).toBe(false); + expect(document.writeBytes(0, new Uint8Array([1]))).toBe(false); + }); + + it('drops pending edits on discard', async () => { + const { document } = build(); + await load(document, 0); + document.writeByte(0, 0xaa); + document.discardChanges(); + expect(document.hasUnsavedChanges()).toBe(false); + expect(document.byteAt(0)).toBe(patternByte(0)); + }); + + it('stops reading once disposed', async () => { + const { document, reader } = build(); + document.dispose(); + await load(document, 0); + expect(reader.calls).toEqual([]); + }); +}); diff --git a/frontend/src/api/client.ts b/frontend/src/api/client.ts new file mode 100644 index 0000000..485454a --- /dev/null +++ b/frontend/src/api/client.ts @@ -0,0 +1,106 @@ +// `axios` is externalized to `window.axios` at build time: the panel's own +// configured instance, which carries the bearer token and the base URL. It is +// not the axios package, so helpers like `axios.isAxiosError` do not exist and +// errors are unwrapped structurally instead. +import axios from 'axios'; + +export const PLUGIN_ID = 'hexeditor4jm2'; +export const BASE = `/api/plugins/${PLUGIN_ID}`; + +/** The subset of axios this plugin uses, so tests can supply their own. */ +export interface HttpLike { + get( + url: string, + config: { + responseType?: 'arraybuffer' | 'json'; + headers?: Record; + signal?: AbortSignal; + }, + ): Promise<{ status: number; data: unknown; headers: Record }>; +} + +export function defaultHttp(): HttpLike { + return axios as unknown as HttpLike; +} + +interface ErrorShape { + response?: { status?: number; data?: unknown; headers?: Record }; + code?: string; + message?: string; + __CANCEL__?: boolean; +} + +function asErrorShape(error: unknown): ErrorShape { + return (error ?? {}) as ErrorShape; +} + +export function httpStatus(error: unknown): number | null { + const status = asErrorShape(error).response?.status; + return typeof status === 'number' ? status : null; +} + +export function isCancelled(error: unknown): boolean { + const shape = asErrorShape(error); + return shape.code === 'ERR_CANCELED' || shape.__CANCEL__ === true || shape.message === 'canceled'; +} + +export interface DecodedError { + code?: string; + message?: string; +} + +/** + * The body of a failed request. Every file request asks for an `ArrayBuffer`, + * so an error body arrives as bytes and has to be decoded before it can be + * read as the JSON the panel and the plugin both answer with. + */ +export function decodeErrorBody(error: unknown): DecodedError | null { + const data = asErrorShape(error).response?.data; + if (data === undefined || data === null) return null; + + let text: string; + if (typeof data === 'string') { + text = data; + } else if (data instanceof ArrayBuffer) { + text = new TextDecoder().decode(new Uint8Array(data)); + } else if (ArrayBuffer.isView(data)) { + text = new TextDecoder().decode(data as Uint8Array); + } else if (typeof data === 'object') { + const object = data as Record; + return { + code: typeof object.code === 'string' ? object.code : undefined, + message: typeof object.message === 'string' ? object.message : undefined, + }; + } else { + return null; + } + + const trimmed = text.trim(); + if (!trimmed) return null; + if (trimmed.startsWith('{')) { + try { + const parsed = JSON.parse(trimmed) as Record; + return { + code: typeof parsed.code === 'string' ? parsed.code : undefined, + message: + typeof parsed.message === 'string' + ? parsed.message + : typeof parsed.error === 'string' + ? parsed.error + : undefined, + }; + } catch { + /* not JSON after all */ + } + } + // The panel's plain `http.Error` answers are text, not JSON. + return { message: trimmed }; +} + +export function showSuccess(text: string): void { + window.$message?.success(text); +} + +export function showWarning(text: string): void { + window.$message?.warning(text); +} diff --git a/frontend/src/api/files.ts b/frontend/src/api/files.ts new file mode 100644 index 0000000..edfc07a --- /dev/null +++ b/frontend/src/api/files.ts @@ -0,0 +1,257 @@ +import { BASE, decodeErrorBody, defaultHttp, httpStatus, isCancelled, type HttpLike } from './client'; + +export interface FileInfo { + size: number; + mtime: number; + path: string; +} + +export interface RangeResult { + bytes: Uint8Array; + start: number; + /** Size of the whole file, as the answer reported it. */ + total: number; + status: 200 | 206; +} + +/** + * A panel that ignores `HTTPResponse.file`: it answers 200 with an empty body + * instead of streaming the file, which is what every GameAP before 4.5 does. + */ +export class PanelTooOldError extends Error { + constructor() { + super('the panel does not serve plugin file responses'); + this.name = 'PanelTooOldError'; + } +} + +/** 416: the range starts past the end of the file, so the file shrank. */ +export class RangeNotSatisfiableError extends Error { + constructor(public readonly total: number | null) { + super('requested range is past the end of the file'); + this.name = 'RangeNotSatisfiableError'; + } +} + +/** The file is no longer the one that was opened. */ +export class FileChangedError extends Error { + constructor( + public readonly knownSize: number, + public readonly actualSize: number | null, + ) { + super('the file changed on the server'); + this.name = 'FileChangedError'; + } +} + +export class HttpError extends Error { + constructor( + message: string, + public readonly status: number | null, + public readonly code?: string, + ) { + super(message); + this.name = 'HttpError'; + } +} + +export function fileUrl(serverId: number, path: string): string { + return `${BASE}/servers/${serverId}/file?path=${encodeURIComponent(path)}`; +} + +export function fileInfoUrl(serverId: number, path: string): string { + return `${BASE}/servers/${serverId}/file/info?path=${encodeURIComponent(path)}`; +} + +export interface ContentRange { + start: number; + end: number; + total: number; +} + +/** + * `bytes 0-1023/4096`. An unsatisfied-range answer (`bytes * /4096`) is not a + * served range and yields null, as does anything malformed. + */ +export function parseContentRange(header: string | null | undefined): ContentRange | null { + if (!header) return null; + const match = /^\s*bytes\s+(\d+)-(\d+)\/(\d+)\s*$/i.exec(header); + if (!match) return null; + const start = Number(match[1]); + const end = Number(match[2]); + const total = Number(match[3]); + if (!Number.isSafeInteger(start) || !Number.isSafeInteger(end) || !Number.isSafeInteger(total)) { + return null; + } + if (end < start || end >= total) return null; + return { start, end, total }; +} + +/** The total from an unsatisfied-range answer (`bytes * /4096`). */ +export function parseUnsatisfiedRange(header: string | null | undefined): number | null { + if (!header) return null; + const match = /^\s*bytes\s+\*\/(\d+)\s*$/i.exec(header); + if (!match) return null; + const total = Number(match[1]); + return Number.isSafeInteger(total) ? total : null; +} + +function headerOf(headers: Record | undefined, name: string): string | undefined { + if (!headers) return undefined; + const direct = headers[name] ?? headers[name.toLowerCase()]; + if (direct !== undefined) return direct; + const lowered = name.toLowerCase(); + for (const [key, value] of Object.entries(headers)) { + if (key.toLowerCase() === lowered) return value; + } + return undefined; +} + +function asBytes(data: unknown): Uint8Array { + if (data instanceof ArrayBuffer) return new Uint8Array(data); + if (ArrayBuffer.isView(data)) { + const view = data as ArrayBufferView; + return new Uint8Array(view.buffer, view.byteOffset, view.byteLength); + } + if (typeof data === 'string') return new TextEncoder().encode(data); + return new Uint8Array(0); +} + +function toHttpError(error: unknown, fallback: string): HttpError { + const status = httpStatus(error); + const decoded = decodeErrorBody(error); + return new HttpError(decoded?.message || fallback, status, decoded?.code); +} + +/** + * One file of one server, read through the plugin's own routes. + * + * Reads go to the file route, which answers a reference the panel streams + * itself with `Range` support - so a window of a large file costs one request + * and the bytes never pass through the plugin. The stat route is the only + * place that touches the node's file system on the plugin's behalf. + */ +export class RemoteFile { + constructor( + private readonly serverId: number, + private readonly path: string, + private readonly http: HttpLike = defaultHttp(), + ) {} + + async info(signal?: AbortSignal): Promise { + let response; + try { + response = await this.http.get(fileInfoUrl(this.serverId, this.path), { + responseType: 'json', + signal, + }); + } catch (error) { + if (isCancelled(error)) throw error; + throw toHttpError(error, 'failed to read file info'); + } + const data = response.data as Partial | undefined; + if (!data || typeof data.size !== 'number' || typeof data.mtime !== 'number') { + throw new HttpError('malformed file info answer', response.status); + } + return { size: data.size, mtime: data.mtime, path: data.path ?? this.path }; + } + + /** The whole file, for the sizes the editor keeps in memory. */ + async readAll(expectedSize: number, signal?: AbortSignal): Promise { + let response; + try { + response = await this.http.get(fileUrl(this.serverId, this.path), { + responseType: 'arraybuffer', + signal, + }); + } catch (error) { + if (isCancelled(error)) throw error; + throw toHttpError(error, 'failed to read the file'); + } + const bytes = asBytes(response.data); + if (bytes.length === 0 && expectedSize > 0) throw new PanelTooOldError(); + return bytes; + } + + /** + * `[start, end]`, both inclusive. `expectedTotal` is the size the caller + * believes the file has: an answer that disagrees means the file changed + * underneath and every offset the caller holds is suspect. + */ + async readRange( + start: number, + end: number, + expectedTotal: number, + signal?: AbortSignal, + ): Promise { + if (start < 0 || end < start) { + throw new HttpError(`invalid range ${start}-${end}`, null); + } + // A request past the end would come back 416; the caller's own view of + // the file is what is wrong, so say that instead. + if (start >= expectedTotal) { + throw new FileChangedError(expectedTotal, null); + } + + let response; + try { + response = await this.http.get(fileUrl(this.serverId, this.path), { + responseType: 'arraybuffer', + headers: { Range: `bytes=${start}-${end}` }, + signal, + }); + } catch (error) { + // A cancelled request is the caller's own doing and keeps its + // identity: the loader tells it apart from a failure. + if (isCancelled(error)) throw error; + if (httpStatus(error) === 416) { + const shape = error as { response?: { headers?: Record } }; + throw new RangeNotSatisfiableError( + parseUnsatisfiedRange(headerOf(shape.response?.headers, 'Content-Range')), + ); + } + throw toHttpError(error, 'failed to read a part of the file'); + } + + const bytes = asBytes(response.data); + + if (response.status === 206) { + const range = parseContentRange(headerOf(response.headers, 'Content-Range')); + if (!range) throw new HttpError('malformed Content-Range', response.status); + if (range.total !== expectedTotal) { + throw new FileChangedError(expectedTotal, range.total); + } + if (bytes.length !== range.end - range.start + 1) { + throw new HttpError('the answer is shorter than its Content-Range', response.status); + } + return { bytes, start: range.start, total: range.total, status: 206 }; + } + + // 200 means the range was not applied. An empty body is a panel that + // ignores file responses; a whole file is a proxy that dropped the + // header, and the window can still be cut out of it. + if (bytes.length === 0 && expectedTotal > 0) throw new PanelTooOldError(); + if (bytes.length !== expectedTotal) { + throw new FileChangedError(expectedTotal, bytes.length); + } + return { + bytes: bytes.subarray(start, Math.min(end + 1, bytes.length)), + start, + total: bytes.length, + status: 200, + }; + } +} + +/** Worth another attempt: nothing came back, or the failure was transient. */ +export function isTransient(error: unknown): boolean { + if (error instanceof PanelTooOldError) return false; + if (error instanceof RangeNotSatisfiableError) return false; + if (error instanceof FileChangedError) return false; + const status = httpStatus(error) ?? (error instanceof HttpError ? error.status : null); + if (status === null) return true; + // 501 is the panel saying it does not serve plugin file responses at all, + // which no amount of asking again will change. + if (status === 501) return false; + return status === 408 || status === 429 || (status >= 500 && status < 600); +} diff --git a/frontend/src/components/DataInspector.vue b/frontend/src/components/DataInspector.vue new file mode 100644 index 0000000..ba87255 --- /dev/null +++ b/frontend/src/components/DataInspector.vue @@ -0,0 +1,270 @@ + + + diff --git a/frontend/src/components/EditorBanner.vue b/frontend/src/components/EditorBanner.vue new file mode 100644 index 0000000..ad87571 --- /dev/null +++ b/frontend/src/components/EditorBanner.vue @@ -0,0 +1,20 @@ + + + diff --git a/frontend/src/components/HexEditor.vue b/frontend/src/components/HexEditor.vue index e447d48..710b871 100644 --- a/frontend/src/components/HexEditor.vue +++ b/frontend/src/components/HexEditor.vue @@ -1,829 +1,833 @@ \ No newline at end of file + diff --git a/frontend/src/composables/useHexDocument.ts b/frontend/src/composables/useHexDocument.ts new file mode 100644 index 0000000..551e0a9 --- /dev/null +++ b/frontend/src/composables/useHexDocument.ts @@ -0,0 +1,156 @@ +import { onBeforeUnmount, ref, shallowRef, watch, type Ref } from 'vue'; +import { httpStatus, isCancelled, type HttpLike } from '../api/client'; +import { HttpError, PanelTooOldError, RemoteFile } from '../api/files'; +import type { Baseline } from '../lib/conflict'; +import { FULL_MODE_MAX_BYTES } from '../lib/constants'; +import type { HexDocument } from '../lib/document'; +import { FullDocument } from '../lib/fullDocument'; +import { WindowedDocument } from '../lib/windowedDocument'; + +export type EditorErrorKind = + | 'no_server' + | 'panel_too_old' + | 'file_responses_disabled' + | 'permission_missing' + | 'access_denied' + | 'file_not_found' + | 'network_error' + | 'http'; + +export interface EditorError { + kind: EditorErrorKind; + detail?: string; +} + +export interface HexDocumentOptions { + serverId: Ref; + path: Ref; + http?: HttpLike; +} + +/** What the panel's own error text says when the plugin lacks its grant. */ +function mentionsPluginPermission(message: string | undefined): boolean { + return !!message && /plugin permission/i.test(message); +} + +export function mapOpenError(error: unknown): EditorError { + if (error instanceof PanelTooOldError) return { kind: 'panel_too_old' }; + const status = httpStatus(error) ?? (error instanceof HttpError ? error.status : null); + const detail = error instanceof Error ? error.message : undefined; + if (status === 501) return { kind: 'file_responses_disabled', detail }; + if (status === 403) { + return mentionsPluginPermission(detail) + ? { kind: 'permission_missing', detail } + : { kind: 'access_denied', detail }; + } + if (status === 401) return { kind: 'access_denied', detail }; + if (status === 404) return { kind: 'file_not_found', detail }; + if (status === null) return { kind: 'network_error', detail }; + return { kind: 'http', detail }; +} + +/** + * Opens one file and keeps the right kind of document for it. + * + * The size decides: a small file is read once and held, which is what makes + * inserting and deleting bytes possible; a large one is read in windows and + * can only be overwritten. Either way the stat comes first, so the editor + * knows the size before it decides. + */ +export function useHexDocument(options: HexDocumentOptions) { + const phase = ref<'idle' | 'loading' | 'ready' | 'error'>('idle'); + const doc = shallowRef(null); + const remote = shallowRef(null); + const baseline = ref(null); + const error = ref(null); + /** Bumped whenever the document changes, so the view recomputes. */ + const version = ref(0); + + let controller: AbortController | null = null; + let unsubscribe: (() => void) | null = null; + + function release(): void { + unsubscribe?.(); + unsubscribe = null; + doc.value?.dispose(); + doc.value = null; + controller?.abort(); + controller = null; + } + + async function open(): Promise { + release(); + error.value = null; + baseline.value = null; + + const serverId = options.serverId.value; + if (serverId === null) { + phase.value = 'error'; + error.value = { kind: 'no_server' }; + return; + } + + phase.value = 'loading'; + controller = new AbortController(); + const signal = controller.signal; + const file = new RemoteFile(serverId, options.path.value, options.http); + remote.value = file; + + try { + let info = await file.info(signal); + let document: HexDocument; + + if (info.size <= FULL_MODE_MAX_BYTES) { + const bytes = await file.readAll(info.size, signal); + if (bytes.length !== info.size) { + // Written between the stat and the read: take the file as + // it actually arrived rather than trusting the old size. + info = { ...info, size: bytes.length }; + } + document = + bytes.length <= FULL_MODE_MAX_BYTES + ? new FullDocument(bytes) + : new WindowedDocument(file, bytes.length); + } else { + document = new WindowedDocument(file, info.size); + } + + if (signal.aborted) { + document.dispose(); + return; + } + + doc.value = document; + baseline.value = { size: info.size, mtime: info.mtime }; + unsubscribe = document.subscribe(() => { + version.value += 1; + }); + version.value += 1; + phase.value = 'ready'; + } catch (failure) { + if (isCancelled(failure) || signal.aborted) return; + error.value = mapOpenError(failure); + phase.value = 'error'; + } + } + + function retryFailed(): void { + doc.value?.retryFailed(); + } + + watch([options.serverId, options.path], () => void open(), { immediate: true }); + onBeforeUnmount(release); + + return { + phase, + doc, + remote, + baseline, + error, + version, + open, + reload: open, + retryFailed, + dispose: release, + }; +} diff --git a/frontend/src/composables/useVirtualRows.ts b/frontend/src/composables/useVirtualRows.ts new file mode 100644 index 0000000..3788c2a --- /dev/null +++ b/frontend/src/composables/useVirtualRows.ts @@ -0,0 +1,182 @@ +import { computed, onBeforeUnmount, ref, watch, type Ref } from 'vue'; +import { BUFFER_ROWS, ROW_HEIGHT, SCROLL_SETTLE_MS } from '../lib/constants'; +import { + contentTranslateY, + firstRowToShow, + rowFromScrollTop, + scrollGeometry, + scrollTopFromRow, + wheelDeltaRows, +} from '../lib/scroll'; + +export interface VirtualRowsOptions { + totalRows: Ref; + scroller: Ref; + /** Called when the visible rows have settled, and at once on a jump. */ + onVisible?: (firstRow: number, rowCount: number) => void; +} + +/** + * Turns a scroll position into the rows to render. + * + * For an ordinary file the spacer has a pixel per row and the browser does the + * work. For one too tall to represent that way the spacer stops growing and + * the scrollbar covers the file proportionally instead; the wheel is then + * handled here so that scrolling still moves whole rows rather than jumping + * hundreds at a time. + */ +export function useVirtualRows(options: VirtualRowsOptions) { + const firstRow = ref(0); + const scrollTop = ref(0); + const viewportHeight = ref(0); + /** What we last wrote to the element, to tell our scroll from the user's. */ + let programmaticScrollTop: number | null = null; + let settleTimer: ReturnType | null = null; + let wheelRemainder = 0; + let observer: ResizeObserver | null = null; + + const geometry = computed(() => scrollGeometry(options.totalRows.value, viewportHeight.value)); + const visibleRows = computed(() => geometry.value.visibleRows); + + const renderRange = computed(() => { + const start = Math.max(0, firstRow.value - BUFFER_ROWS); + const end = Math.min(options.totalRows.value, firstRow.value + visibleRows.value + BUFFER_ROWS); + return { start, end }; + }); + + const translateY = computed(() => + contentTranslateY(geometry.value, scrollTop.value, firstRow.value, renderRange.value.start), + ); + + const spacerHeight = computed(() => geometry.value.spacerHeight); + + function notifyVisible(): void { + const { start, end } = renderRange.value; + options.onVisible?.(start, Math.max(1, end - start)); + } + + /** Asks for what is on screen now, without waiting for scrolling to stop. */ + function ensureNow(): void { + if (settleTimer) { + clearTimeout(settleTimer); + settleTimer = null; + } + notifyVisible(); + } + + function scheduleEnsure(): void { + if (settleTimer) clearTimeout(settleTimer); + settleTimer = setTimeout(() => { + settleTimer = null; + notifyVisible(); + }, SCROLL_SETTLE_MS); + } + + function writeScrollTop(value: number): void { + const element = options.scroller.value; + if (!element) return; + programmaticScrollTop = value; + element.scrollTop = value; + scrollTop.value = value; + } + + function setFirstRow(row: number): void { + const clamped = Math.min(Math.max(row, 0), geometry.value.maxFirstRow); + if (clamped === firstRow.value) return; + firstRow.value = clamped; + scheduleEnsure(); + } + + function onScroll(event: Event): void { + const element = event.target as HTMLElement; + // Our own write comes back as an event; acting on it would fight the + // row we just chose, since many rows share one pixel when scaled. + if (programmaticScrollTop !== null && element.scrollTop === programmaticScrollTop) { + programmaticScrollTop = null; + return; + } + programmaticScrollTop = null; + scrollTop.value = element.scrollTop; + firstRow.value = rowFromScrollTop(geometry.value, element.scrollTop); + scheduleEnsure(); + } + + function onWheel(event: WheelEvent): void { + if (!geometry.value.scaled) return; + // Scaled, one pixel of scrollbar is many rows, so the wheel is read as + // rows here instead of being left to the browser. + event.preventDefault(); + wheelRemainder += wheelDeltaRows(event.deltaY, event.deltaMode, visibleRows.value); + const rows = Math.trunc(wheelRemainder); + if (rows === 0) return; + wheelRemainder -= rows; + setFirstRow(firstRow.value + rows); + writeScrollTop(scrollTopFromRow(geometry.value, firstRow.value)); + } + + function scrollToRow(row: number, align: 'nearest' | 'center' = 'nearest'): void { + const target = firstRowToShow(geometry.value, firstRow.value, row, align); + if (target !== firstRow.value) { + firstRow.value = target; + } + writeScrollTop(scrollTopFromRow(geometry.value, target)); + ensureNow(); + } + + function measure(): void { + const element = options.scroller.value; + if (!element) return; + viewportHeight.value = element.clientHeight || ROW_HEIGHT * 20; + } + + watch( + options.scroller, + (element) => { + observer?.disconnect(); + observer = null; + if (!element) return; + measure(); + observer = new ResizeObserver((entries) => { + for (const entry of entries) { + viewportHeight.value = entry.contentRect.height; + } + }); + observer.observe(element); + }, + { immediate: true }, + ); + + watch(options.totalRows, () => { + firstRow.value = Math.min(firstRow.value, geometry.value.maxFirstRow); + }); + + onBeforeUnmount(() => { + observer?.disconnect(); + observer = null; + if (settleTimer) clearTimeout(settleTimer); + }); + + /** Back to the top, for a file that was replaced under the editor. */ + function reset(): void { + firstRow.value = 0; + wheelRemainder = 0; + writeScrollTop(0); + ensureNow(); + } + + return { + firstRow, + visibleRows, + geometry, + renderRange, + translateY, + spacerHeight, + onScroll, + onWheel, + scrollToRow, + setFirstRow, + ensureNow, + measure, + reset, + }; +} diff --git a/frontend/src/env.d.ts b/frontend/src/env.d.ts new file mode 100644 index 0000000..8430c01 --- /dev/null +++ b/frontend/src/env.d.ts @@ -0,0 +1,55 @@ +/// + +declare module '*.vue' { + import type { DefineComponent } from 'vue'; + const component: DefineComponent, Record, unknown>; + export default component; +} + +/** One mocked request, as MSW hands it to a resolver. */ +interface MockRequestInfo { + params: Record; + request: Request; +} + +/** + * A resolver may answer `undefined` to fall through to the harness's own + * handler, which is how the file-manager listing is extended rather than + * replaced. + */ +type MockResolver = (info: MockRequestInfo) => Response | undefined | Promise; + +// Panel-provided globals (see gameap-api/web/frontend/js/plugins/loader.js). +// Optional because the debug harness may lack some of them. +interface Window { + $message?: { + success: (text: string) => void; + error: (text: string) => void; + warning: (text: string) => void; + info: (text: string) => void; + }; + gameapLang?: string; + /** Present only under `npm run debug` (@gameap/debug). */ + gameapDebug?: { + registerMockHandlers: (handlers: unknown[]) => void; + msw: { + http: { + get: (path: string, resolver: MockResolver) => unknown; + post: (path: string, resolver: MockResolver) => unknown; + put: (path: string, resolver: MockResolver) => unknown; + delete: (path: string, resolver: MockResolver) => unknown; + }; + /** + * MSW's Response subclass. It is a constructor as well as a + * namespace: a ranged read answers raw bytes with its own status + * and headers, which `json()` cannot express. + */ + HttpResponse: { + new (body?: BodyInit | null, init?: ResponseInit): Response; + json: (body: unknown, init?: ResponseInit) => Response; + text: (body: string, init?: ResponseInit) => Response; + }; + delay: (ms?: number) => Promise; + }; + }; +} diff --git a/frontend/src/index.ts b/frontend/src/index.ts index 853e0c3..dd0a4c2 100644 --- a/frontend/src/index.ts +++ b/frontend/src/index.ts @@ -1,6 +1,8 @@ import type { PluginDefinition } from '@gameap/plugin-sdk'; import './styles/main.css'; import HexEditor from './components/HexEditor.vue'; +import { registerMockHandlers } from './mocks/handlers'; +import { translations } from './translations'; export const hexEditorPlugin: PluginDefinition = { id: 'hexeditor4jm2', @@ -10,41 +12,11 @@ export const hexEditorPlugin: PluginDefinition = { description: 'View and edit files in hexadecimal format', author: 'GameAP', - translations: { - en: { - 'hex_editor': 'Hex Editor', - 'offset': 'Offset', - 'hex': 'Hex', - 'ascii': 'ASCII', - 'modified': 'Modified', - 'bytes': 'bytes', - 'file_size': 'File size', - 'selected': 'Selected', - 'byte': 'byte', - 'value': 'Value', - 'data_inspector': 'Data Inspector', - 'binary': 'Binary', - 'little_endian': 'Little-Endian', - 'big_endian': 'Big-Endian', - 'add_byte': 'Add byte', - }, - ru: { - 'hex_editor': 'Hex-редактор', - 'offset': 'Смещение', - 'hex': 'Hex', - 'ascii': 'ASCII', - 'modified': 'Изменено', - 'bytes': 'байт', - 'file_size': 'Размер файла', - 'selected': 'Выбрано', - 'byte': 'байт', - 'value': 'Значение', - 'data_inspector': 'Инспектор данных', - 'binary': 'Двоичное', - 'little_endian': 'Little-Endian', - 'big_endian': 'Big-Endian', - 'add_byte': 'Добавить байт', - }, + translations, + + onInit() { + // Only under @gameap/debug: mock answers for the plugin's own routes. + registerMockHandlers(); }, fileEditors: [ @@ -52,11 +24,22 @@ export const hexEditorPlugin: PluginDefinition = { id: 'hex-editor', name: '@:hex_editor', component: HexEditor, - match: { - allFiles: true, - }, - contentType: 'binary', + // Any file can be looked at as bytes, and the lowest specificity + // there is leaves the double click to any editor that knows the + // format. + match: { allFiles: true }, + // The file manager hands over nothing: the editor reads the file + // itself, one window at a time, which is what lets it open a file + // past the manager's own edit size cap. + contentType: 'none', + // It opens the file rather than reporting on it, so it belongs + // with Open, View and Edit rather than in a block of its own. + menuGroup: 'open', icon: 'memory', + // A large file takes seconds of visible progress to write; closing + // the modal at that point would throw away the only confirmation + // there is, and the warm windows with it. + keepOpenOnSave: true, }, ], }; diff --git a/frontend/src/lib/chunkCache.ts b/frontend/src/lib/chunkCache.ts new file mode 100644 index 0000000..592eece --- /dev/null +++ b/frontend/src/lib/chunkCache.ts @@ -0,0 +1,81 @@ +import { CHUNK_SIZE, LRU_MAX_CHUNKS, LRU_MIN_CHUNKS } from './constants'; + +export function chunkIndexOf(offset: number): number { + // Not `>>>`: a file may be larger than 4 GiB and the shift would wrap. + return Math.floor(offset / CHUNK_SIZE); +} + +export function chunkStart(index: number): number { + return index * CHUNK_SIZE; +} + +/** How many chunks are worth keeping for a file of this size. */ +export function capacityFor(size: number): number { + const needed = Math.ceil(size / CHUNK_SIZE); + return Math.min(Math.max(needed, LRU_MIN_CHUNKS), LRU_MAX_CHUNKS); +} + +/** + * The windows read so far, least-recently-used first. A `Map` keeps insertion + * order, so re-inserting a chunk on every read is all the bookkeeping an LRU + * needs. + */ +export class ChunkCache { + private readonly chunks = new Map(); + + constructor(private readonly capacity: number) {} + + get size(): number { + return this.chunks.size; + } + + has(index: number): boolean { + return this.chunks.has(index); + } + + /** Reads a chunk and marks it as the most recently used. */ + get(index: number): Uint8Array | undefined { + const chunk = this.chunks.get(index); + if (chunk === undefined) return undefined; + this.chunks.delete(index); + this.chunks.set(index, chunk); + return chunk; + } + + /** + * Reads a chunk without touching its position. Looking up one byte must + * not reorder the cache, or scanning a row would evict what the viewport + * is about to need. + */ + peek(index: number): Uint8Array | undefined { + return this.chunks.get(index); + } + + set(index: number, bytes: Uint8Array): void { + if (this.chunks.has(index)) this.chunks.delete(index); + this.chunks.set(index, bytes); + while (this.chunks.size > this.capacity) { + const oldest = this.chunks.keys().next(); + if (oldest.done) break; + this.chunks.delete(oldest.value); + } + } + + /** Writes one byte into a cached chunk; false when it is not cached. */ + patch(offset: number, value: number): boolean { + const chunk = this.chunks.get(chunkIndexOf(offset)); + if (chunk === undefined) return false; + const at = offset - chunkStart(chunkIndexOf(offset)); + if (at < 0 || at >= chunk.length) return false; + chunk[at] = value; + return true; + } + + delete(index: number): void { + this.chunks.delete(index); + } + + clear(): void { + this.chunks.clear(); + } +} diff --git a/frontend/src/lib/chunkLoader.ts b/frontend/src/lib/chunkLoader.ts new file mode 100644 index 0000000..45a8f2f --- /dev/null +++ b/frontend/src/lib/chunkLoader.ts @@ -0,0 +1,176 @@ +import type { RangeResult } from '../api/files'; +import { FileChangedError, RangeNotSatisfiableError, isTransient } from '../api/files'; +import { ChunkCache, chunkIndexOf, chunkStart } from './chunkCache'; +import { CHUNK_SIZE, MAX_CONCURRENT_LOADS, MAX_RUN_CHUNKS, PREFETCH_BYTES } from './constants'; +import type { StaleInfo } from './document'; +import { withRetry, type RetryOptions } from './retry'; + +export interface RangeReader { + readRange(start: number, end: number, expectedTotal: number, signal?: AbortSignal): Promise; +} + +export interface LoaderHooks { + onChange: () => void; + onStale: (info: StaleInfo) => void; +} + +interface Run { + first: number; + last: number; +} + +/** + * Keeps the windows the viewport needs in the cache. + * + * Consecutive missing chunks are asked for in one request, at most two + * requests are in flight, and the queue is rebuilt on every call: what the + * viewport wanted a moment ago is not worth fetching once it has moved on. + */ +export class ChunkLoader { + private queue: Run[] = []; + private readonly inflight = new Set(); + private readonly failedChunks = new Set(); + private active = 0; + private paused = false; + private disposed = false; + private controller = new AbortController(); + private error: Error | null = null; + + constructor( + private readonly reader: RangeReader, + private readonly cache: ChunkCache, + private readonly size: number, + private readonly hooks: LoaderHooks, + private readonly retryOptions: Omit = {}, + ) {} + + get inflightCount(): number { + return this.inflight.size; + } + + get failed(): ReadonlySet { + return this.failedChunks; + } + + get lastError(): Error | null { + return this.error; + } + + ensureRange(offset: number, length: number): void { + if (this.disposed || this.paused || this.size === 0) return; + + const lo = Math.max(0, offset - PREFETCH_BYTES); + const hi = Math.min(this.size - 1, offset + length - 1 + PREFETCH_BYTES); + if (hi < lo) return; + + const first = chunkIndexOf(lo); + const last = chunkIndexOf(hi); + const missing: number[] = []; + for (let index = first; index <= last; index += 1) { + if (!this.cache.has(index) && !this.inflight.has(index) && !this.failedChunks.has(index)) { + missing.push(index); + } + } + if (missing.length === 0) return; + + const runs = splitRuns(missing, MAX_RUN_CHUNKS); + const centre = chunkIndexOf(offset + Math.floor(length / 2)); + runs.sort((a, b) => Math.abs(a.first - centre) - Math.abs(b.first - centre)); + this.queue = runs; + this.pump(); + } + + retryFailed(): void { + this.failedChunks.clear(); + this.error = null; + this.hooks.onChange(); + } + + /** Stops competing with the reads that rebuild the file for a save. */ + pause(): void { + this.paused = true; + this.queue = []; + } + + resume(): void { + if (this.disposed) return; + this.paused = false; + } + + dispose(): void { + this.disposed = true; + this.queue = []; + this.controller.abort(); + } + + private pump(): void { + while (this.active < MAX_CONCURRENT_LOADS && this.queue.length > 0) { + const run = this.queue.shift(); + if (!run) return; + this.active += 1; + for (let index = run.first; index <= run.last; index += 1) this.inflight.add(index); + void this.fetchRun(run).finally(() => { + this.active -= 1; + for (let index = run.first; index <= run.last; index += 1) this.inflight.delete(index); + // After the bookkeeping, not before: a view that reads + // `inflightCount` has to see the request gone, or its loading + // indicator stays on until something else changes. + if (!this.disposed) { + this.hooks.onChange(); + this.pump(); + } + }); + } + } + + private async fetchRun(run: Run): Promise { + const start = chunkStart(run.first); + const end = Math.min(chunkStart(run.last) + CHUNK_SIZE - 1, this.size - 1); + try { + const result = await withRetry( + (signal) => this.reader.readRange(start, end, this.size, signal), + { ...this.retryOptions, shouldRetry: isTransient, signal: this.controller.signal }, + ); + if (this.disposed) return; + for (let index = run.first; index <= run.last; index += 1) { + const from = chunkStart(index) - start; + if (from >= result.bytes.length) break; + const to = Math.min(from + CHUNK_SIZE, result.bytes.length); + this.cache.set(index, result.bytes.subarray(from, to)); + } + this.error = null; + this.hooks.onChange(); + } catch (error) { + if (this.disposed || this.controller.signal.aborted) return; + if (error instanceof FileChangedError) { + this.hooks.onStale({ + knownSize: this.size, + actualSize: error.actualSize, + reason: 'size', + }); + return; + } + if (error instanceof RangeNotSatisfiableError) { + this.hooks.onStale({ knownSize: this.size, actualSize: error.total, reason: 'range' }); + return; + } + for (let index = run.first; index <= run.last; index += 1) this.failedChunks.add(index); + this.error = error instanceof Error ? error : new Error(String(error)); + this.hooks.onChange(); + } + } +} + +/** Consecutive indices grouped into runs of at most `maxLength`. */ +export function splitRuns(indices: number[], maxLength: number): Run[] { + const runs: Run[] = []; + for (const index of indices) { + const current = runs[runs.length - 1]; + if (current && index === current.last + 1 && current.last - current.first + 1 < maxLength) { + current.last = index; + } else { + runs.push({ first: index, last: index }); + } + } + return runs; +} diff --git a/frontend/src/lib/conflict.ts b/frontend/src/lib/conflict.ts new file mode 100644 index 0000000..218daf9 --- /dev/null +++ b/frontend/src/lib/conflict.ts @@ -0,0 +1,22 @@ +export interface Baseline { + size: number; + /** Unknown right after a save: the file manager reports no mtime back. */ + mtime: number | null; +} + +export interface FileStat { + size: number; + mtime: number; +} + +/** + * Whether the file still looks like the one that was opened. A size change is + * the serious one: in windowed mode every pending edit is addressed by offset, + * and those offsets no longer mean anything. + */ +export function compareBaseline(baseline: Baseline | null, fresh: FileStat): 'same' | 'mtime' | 'size' { + if (!baseline) return 'same'; + if (baseline.size !== fresh.size) return 'size'; + if (baseline.mtime !== null && baseline.mtime !== fresh.mtime) return 'mtime'; + return 'same'; +} diff --git a/frontend/src/lib/constants.ts b/frontend/src/lib/constants.ts new file mode 100644 index 0000000..a8c5c30 --- /dev/null +++ b/frontend/src/lib/constants.ts @@ -0,0 +1,68 @@ +/** Bytes per rendered row, and the row height the virtual scroller assumes. */ +export const BYTES_PER_ROW = 16; +export const ROW_HEIGHT = 24; +/** Rows rendered above and below the viewport. */ +export const BUFFER_ROWS = 10; + +/** + * Up to this size the whole file is loaded into memory and edited as one + * array, which is what makes inserting and deleting bytes possible. Above it + * the editor reads windows and can only overwrite. + */ +export const FULL_MODE_MAX_BYTES = 4 * 1024 * 1024; + +/** + * The file manager's own cap on one write (`maxUploadSize`, 100 << 20). It is + * what the limit is called in front of the user. + */ +export const PANEL_MAX_UPLOAD_BYTES = 100 * 1024 * 1024; + +/** + * What the editor allows itself. The panel counts the whole multipart body, + * framing included, so the file has to stay under the cap with room to spare. + */ +export const MAX_UPLOAD_BYTES = PANEL_MAX_UPLOAD_BYTES - 64 * 1024; + +/** + * One cached window. A multiple of BYTES_PER_ROW so a row never straddles two + * chunks. Each request costs a wasm call plus two daemon round trips whatever + * its length, so the size trades time-to-first-paint after a scrollbar jump + * against how often a boundary is crossed while scrolling; 256 KiB covers + * 16384 rows and paints quickly on a slow link. + */ +export const CHUNK_SIZE = 256 * 1024; + +/** Consecutive missing chunks coalesced into one request (≤ 1 MiB). */ +export const MAX_RUN_CHUNKS = 4; + +/** + * Read size while rebuilding the file for a save. Latency does not matter + * there and daemon round trips do, so it matches the panel's own per-round-trip + * read. + */ +export const SAVE_CHUNK_SIZE = 4 * 1024 * 1024; + +/** Cached chunks: 48 × 256 KiB caps the cache at 12 MiB. */ +export const LRU_MIN_CHUNKS = 8; +export const LRU_MAX_CHUNKS = 48; + +/** Kept loaded around the viewport, so a boundary is crossed before it shows. */ +export const PREFETCH_BYTES = 64 * 1024; + +export const MAX_CONCURRENT_LOADS = 2; + +/** Backoff for transient failures; each delay is jittered by ±20%. */ +export const RETRY_DELAYS_MS = [500, 1500, 4000]; + +/** Scrolling settles for this long before new windows are requested. */ +export const SCROLL_SETTLE_MS = 60; + +/** + * Tallest spacer the scroller will build. Browsers cap element height far + * below what a large file needs (about 33.5M px in Chromium, 17.9M px in + * Firefox); past this the scrollbar is mapped to rows proportionally instead. + */ +export const MAX_SPACER_HEIGHT = 10_000_000; + +/** How long to wait for the file manager to confirm a write. */ +export const UPLOAD_ACK_TIMEOUT_MS = 120_000; diff --git a/frontend/src/lib/document.ts b/frontend/src/lib/document.ts new file mode 100644 index 0000000..143a55e --- /dev/null +++ b/frontend/src/lib/document.ts @@ -0,0 +1,82 @@ +/** Why the document no longer matches the file on the server. */ +export interface StaleInfo { + knownSize: number; + actualSize: number | null; + reason: 'size' | 'range'; +} + +/** + * What was handed to the file manager to write. Kept so that a successful + * write can be turned into the new baseline without reading the file back. + */ +export type SaveSnapshot = + | { mode: 'full'; size: number; payload: ArrayBuffer; data: Uint8Array } + | { mode: 'windowed'; size: number; payload: Blob; overlay: Map }; + +export type ProgressCallback = (done: number, total: number) => void; + +/** + * One file open in the editor, whether it is held in memory or read in windows. + * + * Everything the view needs is here, and the two implementations differ only + * in what they can do: a windowed document cannot insert or delete bytes, + * because that would move every byte after the edit and there is no way to + * write part of a file. + */ +export interface HexDocument { + readonly mode: 'full' | 'windowed'; + readonly size: number; + readonly canInsertDelete: boolean; + readonly modifiedCount: number; + readonly structureModified: boolean; + readonly stale: StaleInfo | null; + /** Requests in flight; always 0 for a document held in memory. */ + readonly loadingCount: number; + readonly failedCount: number; + readonly lastError: Error | null; + + /** `undefined` when the byte has not been read yet. */ + byteAt(offset: number): number | undefined; + /** A row of at most 16 bytes with edits applied; null if any is missing. */ + readRow(rowIndex: number): Uint8Array | null; + /** A copy of `length` bytes with edits applied; null if any is missing. */ + readBytes(offset: number, length: number): Uint8Array | null; + isLoaded(offset: number): boolean; + isModified(offset: number): boolean; + + writeByte(offset: number, value: number): boolean; + /** All bytes or none, so a multi-byte value is never half written. */ + writeBytes(offset: number, values: Uint8Array): boolean; + insertAt(offset: number, value: number): boolean; + deleteAt(offset: number): boolean; + + ensureRange(offset: number, length: number): void; + retryFailed(): void; + hasUnsavedChanges(): boolean; + + buildSnapshot(onProgress?: ProgressCallback, signal?: AbortSignal): Promise; + markSaved(snapshot: SaveSnapshot): void; + discardChanges(): void; + dispose(): void; + subscribe(listener: () => void): () => void; +} + +/** Change notification for the view; the composable turns it into a ref bump. */ +export class Emitter { + private listeners = new Set<() => void>(); + + subscribe(listener: () => void): () => void { + this.listeners.add(listener); + return () => { + this.listeners.delete(listener); + }; + } + + emit(): void { + for (const listener of [...this.listeners]) listener(); + } + + clear(): void { + this.listeners.clear(); + } +} diff --git a/frontend/src/lib/format.ts b/frontend/src/lib/format.ts new file mode 100644 index 0000000..f7e4c9a --- /dev/null +++ b/frontend/src/lib/format.ts @@ -0,0 +1,57 @@ +/** Hex digits an offset column needs for a file of this size. */ +export function offsetDigits(size: number): number { + if (size <= 1) return 8; + return Math.max(8, (size - 1).toString(16).length); +} + +export function formatOffset(offset: number, digits = 8): string { + return offset.toString(16).toUpperCase().padStart(digits, '0'); +} + +export function formatByte(byte: number): string { + return byte.toString(16).toUpperCase().padStart(2, '0'); +} + +export function formatNibble(n: number): string { + return n.toString(16).toUpperCase().padStart(2, '0'); +} + +export function toAscii(byte: number): string { + return byte >= 32 && byte <= 126 ? String.fromCharCode(byte) : '.'; +} + +/** + * An offset typed by hand: hex, with an optional `0x` prefix and surrounding + * space. `null` when it is not a number this editor can address. + */ +export function parseOffset(input: string): number | null { + const match = /^\s*(?:0x)?([0-9a-f]+)\s*$/i.exec(input); + if (!match) return null; + const value = Number.parseInt(match[1], 16); + if (!Number.isSafeInteger(value) || value < 0) return null; + return value; +} + +const UNITS = ['B', 'KiB', 'MiB', 'GiB', 'TiB']; + +export function formatSize(bytes: number): string { + if (bytes < 1024) return `${bytes} B`; + let value = bytes; + let unit = 0; + while (value >= 1024 && unit < UNITS.length - 1) { + value /= 1024; + unit += 1; + } + return `${value.toFixed(1)} ${UNITS[unit]}`; +} + +/** The exact count, grouped, for a tooltip beside the rounded size. */ +export function formatExactBytes(bytes: number): string { + return bytes.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ' '); +} + +/** A unix-seconds timestamp in the viewer's locale; empty when unknown. */ +export function formatDateTime(unixSeconds: number | null | undefined): string { + if (!unixSeconds) return ''; + return new Date(unixSeconds * 1000).toLocaleString(); +} diff --git a/frontend/src/lib/fullDocument.ts b/frontend/src/lib/fullDocument.ts new file mode 100644 index 0000000..b31fa4e --- /dev/null +++ b/frontend/src/lib/fullDocument.ts @@ -0,0 +1,175 @@ +import { BYTES_PER_ROW } from './constants'; +import { Emitter, type HexDocument, type ProgressCallback, type SaveSnapshot, type StaleInfo } from './document'; + +/** + * A file small enough to hold twice: the working bytes and the ones the server + * has. Everything the editor can do is possible here, inserting and deleting + * bytes included, because the whole file is rewritten on save anyway. + */ +export class FullDocument implements HexDocument { + readonly mode = 'full' as const; + readonly canInsertDelete = true; + readonly loadingCount = 0; + readonly failedCount = 0; + readonly lastError = null; + readonly stale: StaleInfo | null = null; + + private data: Uint8Array; + private original: Uint8Array; + private modified = new Set(); + private structural = false; + private readonly emitter = new Emitter(); + + constructor(bytes: Uint8Array) { + // A copy of its own: the bytes arrive in the response buffer, and + // editing that in place would rewrite what the caller still holds. + this.data = bytes.slice(); + this.original = bytes.slice(); + } + + get size(): number { + return this.data.length; + } + + get modifiedCount(): number { + return this.modified.size; + } + + get structureModified(): boolean { + return this.structural; + } + + byteAt(offset: number): number | undefined { + return offset >= 0 && offset < this.data.length ? this.data[offset] : undefined; + } + + readRow(rowIndex: number): Uint8Array | null { + const start = rowIndex * BYTES_PER_ROW; + if (start >= this.data.length) return null; + return this.data.subarray(start, Math.min(start + BYTES_PER_ROW, this.data.length)); + } + + readBytes(offset: number, length: number): Uint8Array | null { + if (offset < 0 || offset + length > this.data.length) return null; + return this.data.slice(offset, offset + length); + } + + isLoaded(offset: number): boolean { + return offset >= 0 && offset < this.data.length; + } + + isModified(offset: number): boolean { + return this.modified.has(offset); + } + + writeByte(offset: number, value: number): boolean { + if (!this.isLoaded(offset)) return false; + this.data[offset] = value & 0xff; + this.remark(offset); + this.emitter.emit(); + return true; + } + + writeBytes(offset: number, values: Uint8Array): boolean { + if (offset < 0 || offset + values.length > this.data.length) return false; + this.data.set(values, offset); + for (let i = 0; i < values.length; i += 1) this.remark(offset + i); + this.emitter.emit(); + return true; + } + + insertAt(offset: number, value: number): boolean { + if (offset < 0 || offset > this.data.length) return false; + const next = new Uint8Array(this.data.length + 1); + next.set(this.data.subarray(0, offset)); + next[offset] = value & 0xff; + next.set(this.data.subarray(offset), offset + 1); + this.data = next; + this.structural = true; + this.recomputeModified(); + this.emitter.emit(); + return true; + } + + deleteAt(offset: number): boolean { + if (!this.isLoaded(offset)) return false; + const next = new Uint8Array(this.data.length - 1); + next.set(this.data.subarray(0, offset)); + next.set(this.data.subarray(offset + 1), offset); + this.data = next; + this.structural = true; + this.recomputeModified(); + this.emitter.emit(); + return true; + } + + ensureRange(): void { + /* everything is already here */ + } + + retryFailed(): void { + /* nothing can fail */ + } + + hasUnsavedChanges(): boolean { + return this.modified.size > 0 || this.structural; + } + + async buildSnapshot(onProgress?: ProgressCallback): Promise { + const data = this.data.slice(); + onProgress?.(data.length, data.length); + return { + mode: 'full', + size: data.length, + payload: data.buffer.slice(0) as ArrayBuffer, + data, + }; + } + + markSaved(snapshot: SaveSnapshot): void { + if (snapshot.mode !== 'full') return; + this.original = snapshot.data; + this.structural = false; + this.recomputeModified(); + this.emitter.emit(); + } + + discardChanges(): void { + this.data = this.original.slice(); + this.modified.clear(); + this.structural = false; + this.emitter.emit(); + } + + dispose(): void { + this.emitter.clear(); + } + + subscribe(listener: () => void): () => void { + return this.emitter.subscribe(listener); + } + + /** A byte past the end of the original is new, and so always modified. */ + private remark(offset: number): void { + const isNew = offset >= this.original.length; + if (isNew || this.data[offset] !== this.original[offset]) { + this.modified.add(offset); + } else { + this.modified.delete(offset); + } + } + + /** + * After an insert or a delete every byte from the edit onwards may line up + * with a different original, so the whole diff is redone. Cheap enough for + * the sizes this document holds. + */ + private recomputeModified(): void { + this.modified.clear(); + for (let i = 0; i < this.data.length; i += 1) { + if (i >= this.original.length || this.data[i] !== this.original[i]) { + this.modified.add(i); + } + } + } +} diff --git a/frontend/src/lib/reconstruct.ts b/frontend/src/lib/reconstruct.ts new file mode 100644 index 0000000..af74c63 --- /dev/null +++ b/frontend/src/lib/reconstruct.ts @@ -0,0 +1,61 @@ +import type { RangeReader } from './chunkLoader'; +import { SAVE_CHUNK_SIZE } from './constants'; +import { FileChangedError, isTransient } from '../api/files'; +import type { ProgressCallback } from './document'; +import { withRetry, type RetryOptions } from './retry'; + +export interface BuildOptions { + onProgress?: ProgressCallback; + signal?: AbortSignal; + /** Read size; only tests need anything but the default. */ + blockSize?: number; + retry?: Omit; +} + +/** + * The file as it should be written: read back block by block with the pending + * edits applied on the way through. + * + * The panel has no way to write part of a file, so a save means handing over + * the whole thing. Collecting the blocks into a `Blob` keeps them out of one + * contiguous allocation, and the file manager wraps that Blob without copying + * it again. + */ +export async function buildBlob( + reader: RangeReader, + size: number, + overlay: Map, + options: BuildOptions = {}, +): Promise { + const blockSize = options.blockSize ?? SAVE_CHUNK_SIZE; + const edits = [...overlay.entries()].sort((a, b) => a[0] - b[0]); + const parts: BlobPart[] = []; + let editIndex = 0; + let position = 0; + + if (size === 0) return new Blob([], { type: 'application/octet-stream' }); + + while (position < size) { + const end = Math.min(position + blockSize, size) - 1; + const result = await withRetry( + (signal) => reader.readRange(position, end, size, signal), + { ...options.retry, shouldRetry: isTransient, signal: options.signal }, + ); + const expected = end - position + 1; + if (result.bytes.length !== expected) { + throw new FileChangedError(size, null); + } + // The response buffer is ours to edit: nothing else holds it. + const block = result.bytes; + while (editIndex < edits.length && edits[editIndex][0] <= end) { + block[edits[editIndex][0] - position] = edits[editIndex][1]; + editIndex += 1; + } + // A view, not a copy: the response buffer is not shared with anyone. + parts.push(new Uint8Array(block.buffer as ArrayBuffer, block.byteOffset, block.byteLength)); + position = end + 1; + options.onProgress?.(position, size); + } + + return new Blob(parts, { type: 'application/octet-stream' }); +} diff --git a/frontend/src/lib/retry.ts b/frontend/src/lib/retry.ts new file mode 100644 index 0000000..aac8051 --- /dev/null +++ b/frontend/src/lib/retry.ts @@ -0,0 +1,48 @@ +import { RETRY_DELAYS_MS } from './constants'; + +export interface RetryOptions { + delays?: number[]; + shouldRetry?: (error: unknown) => boolean; + signal?: AbortSignal; + /** Injected in tests; defaults to a jittered `setTimeout`. */ + sleep?: (ms: number) => Promise; +} + +export class AbortedError extends Error { + constructor() { + super('aborted'); + this.name = 'AbortedError'; + } +} + +function defaultSleep(ms: number): Promise { + // ±20% of jitter, so a failure that hit several windows at once does not + // bring them all back in the same instant. + const jittered = ms * (0.8 + Math.random() * 0.4); + return new Promise((resolve) => setTimeout(resolve, jittered)); +} + +/** + * Runs `attempt` until it succeeds, the error is not worth retrying, or the + * delays run out. The signal is passed to every attempt and also ends the + * waiting between them. + */ +export async function withRetry( + attempt: (signal?: AbortSignal) => Promise, + options: RetryOptions = {}, +): Promise { + const delays = options.delays ?? RETRY_DELAYS_MS; + const shouldRetry = options.shouldRetry ?? (() => false); + const sleep = options.sleep ?? defaultSleep; + + for (let index = 0; ; index += 1) { + if (options.signal?.aborted) throw new AbortedError(); + try { + return await attempt(options.signal); + } catch (error) { + if (options.signal?.aborted) throw error; + if (index >= delays.length || !shouldRetry(error)) throw error; + await sleep(delays[index]); + } + } +} diff --git a/frontend/src/lib/scroll.ts b/frontend/src/lib/scroll.ts new file mode 100644 index 0000000..c8815d6 --- /dev/null +++ b/frontend/src/lib/scroll.ts @@ -0,0 +1,103 @@ +import { MAX_SPACER_HEIGHT, ROW_HEIGHT } from './constants'; + +/** + * How the rows of a file map onto a scrollable element. + * + * A file of any size cannot get a spacer as tall as its rows: browsers cap + * element height (about 33.5M px in Chromium, 17.9M px in Firefox), and past + * the cap the end of the file becomes unreachable. Above `MAX_SPACER_HEIGHT` + * the spacer therefore stops growing and the scroll position is mapped onto + * the first visible row proportionally instead - the scrollbar covers the + * whole file, one pixel just stands for more than one row. + */ +export interface ScrollGeometry { + totalRows: number; + naturalHeight: number; + spacerHeight: number; + viewportHeight: number; + visibleRows: number; + maxFirstRow: number; + maxScrollTop: number; + scaled: boolean; +} + +export function scrollGeometry(totalRows: number, viewportHeight: number): ScrollGeometry { + const naturalHeight = totalRows * ROW_HEIGHT; + const spacerHeight = Math.min(naturalHeight, MAX_SPACER_HEIGHT); + const visibleRows = Math.max(1, Math.floor(viewportHeight / ROW_HEIGHT)); + return { + totalRows, + naturalHeight, + spacerHeight, + viewportHeight, + visibleRows, + maxFirstRow: Math.max(0, totalRows - visibleRows), + maxScrollTop: Math.max(0, spacerHeight - viewportHeight), + scaled: naturalHeight > MAX_SPACER_HEIGHT, + }; +} + +function clamp(value: number, min: number, max: number): number { + return Math.min(Math.max(value, min), max); +} + +export function rowFromScrollTop(geometry: ScrollGeometry, scrollTop: number): number { + if (!geometry.scaled) { + return clamp(Math.floor(scrollTop / ROW_HEIGHT), 0, geometry.maxFirstRow); + } + if (geometry.maxScrollTop === 0) return 0; + const ratio = clamp(scrollTop / geometry.maxScrollTop, 0, 1); + return clamp(Math.round(ratio * geometry.maxFirstRow), 0, geometry.maxFirstRow); +} + +export function scrollTopFromRow(geometry: ScrollGeometry, row: number): number { + const clamped = clamp(row, 0, geometry.maxFirstRow); + if (!geometry.scaled) { + return clamp(clamped * ROW_HEIGHT, 0, geometry.maxScrollTop); + } + if (geometry.maxFirstRow === 0) return 0; + const ratio = clamped / geometry.maxFirstRow; + return clamp(Math.round(ratio * geometry.maxScrollTop), 0, geometry.maxScrollTop); +} + +/** + * Where the rendered block sits inside the spacer. Unscaled that is the row + * offset; scaled the block has to follow the scroll position itself, since the + * spacer no longer has a pixel per row. + */ +export function contentTranslateY( + geometry: ScrollGeometry, + scrollTop: number, + firstRow: number, + renderStart: number, +): number { + if (!geometry.scaled) { + return renderStart * ROW_HEIGHT; + } + return scrollTop - (firstRow - renderStart) * ROW_HEIGHT; +} + +/** Rows one wheel notch moves, for the three delta modes browsers report. */ +export function wheelDeltaRows(deltaY: number, deltaMode: number, visibleRows: number): number { + if (deltaMode === 1) return deltaY; + if (deltaMode === 2) return deltaY * visibleRows; + return deltaY / ROW_HEIGHT; +} + +/** The first row that brings `row` into view, moving as little as possible. */ +export function firstRowToShow( + geometry: ScrollGeometry, + firstRow: number, + row: number, + align: 'nearest' | 'center' = 'nearest', +): number { + if (align === 'center') { + return clamp(row - Math.floor(geometry.visibleRows / 2), 0, geometry.maxFirstRow); + } + if (row < firstRow) return clamp(row, 0, geometry.maxFirstRow); + const lastVisible = firstRow + geometry.visibleRows - 1; + if (row > lastVisible) { + return clamp(row - geometry.visibleRows + 1, 0, geometry.maxFirstRow); + } + return clamp(firstRow, 0, geometry.maxFirstRow); +} diff --git a/frontend/src/lib/windowedDocument.ts b/frontend/src/lib/windowedDocument.ts new file mode 100644 index 0000000..aeb3eda --- /dev/null +++ b/frontend/src/lib/windowedDocument.ts @@ -0,0 +1,227 @@ +import { ChunkCache, capacityFor, chunkIndexOf, chunkStart } from './chunkCache'; +import { ChunkLoader, type RangeReader } from './chunkLoader'; +import { BYTES_PER_ROW } from './constants'; +import { Emitter, type HexDocument, type ProgressCallback, type SaveSnapshot, type StaleInfo } from './document'; +import { buildBlob } from './reconstruct'; +import type { RetryOptions } from './retry'; + +export interface WindowedOptions { + retry?: Omit; + blockSize?: number; +} + +/** + * A file too large to hold: windows are read on demand and edits are kept as + * an overlay of single bytes. + * + * Only overwriting is possible. Inserting or deleting would move every byte + * after the edit, and since nothing in the stack can write part of a file, the + * whole thing would have to be rewritten from an offset the editor has not + * even read. + */ +export class WindowedDocument implements HexDocument { + readonly mode = 'windowed' as const; + readonly canInsertDelete = false; + readonly structureModified = false; + + private readonly cache: ChunkCache; + private readonly loader: ChunkLoader; + private readonly overlay = new Map(); + private readonly emitter = new Emitter(); + private staleInfo: StaleInfo | null = null; + private disposed = false; + + constructor( + private readonly reader: RangeReader, + readonly size: number, + private readonly options: WindowedOptions = {}, + ) { + this.cache = new ChunkCache(capacityFor(size)); + this.loader = new ChunkLoader( + reader, + this.cache, + size, + { + onChange: () => this.emitter.emit(), + onStale: (info) => { + this.staleInfo = info; + this.loader.pause(); + this.emitter.emit(); + }, + }, + options.retry ?? {}, + ); + } + + get modifiedCount(): number { + return this.overlay.size; + } + + get stale(): StaleInfo | null { + return this.staleInfo; + } + + get loadingCount(): number { + return this.loader.inflightCount; + } + + get failedCount(): number { + return this.loader.failed.size; + } + + get lastError(): Error | null { + return this.loader.lastError; + } + + byteAt(offset: number): number | undefined { + if (offset < 0 || offset >= this.size) return undefined; + const edited = this.overlay.get(offset); + if (edited !== undefined) return edited; + // `peek`, not `get`: reading a byte must not reorder the cache. + const chunk = this.cache.peek(chunkIndexOf(offset)); + if (chunk === undefined) return undefined; + const at = offset - chunkStart(chunkIndexOf(offset)); + return at < chunk.length ? chunk[at] : undefined; + } + + readRow(rowIndex: number): Uint8Array | null { + const start = rowIndex * BYTES_PER_ROW; + if (start >= this.size) return null; + const length = Math.min(BYTES_PER_ROW, this.size - start); + return this.readBytes(start, length); + } + + readBytes(offset: number, length: number): Uint8Array | null { + if (offset < 0 || length < 0 || offset + length > this.size) return null; + const out = new Uint8Array(length); + for (let i = 0; i < length; i += 1) { + const byte = this.byteAt(offset + i); + if (byte === undefined) return null; + out[i] = byte; + } + // One `get` per read keeps the chunks a row touches warm without + // reordering the cache once per byte. + this.cache.get(chunkIndexOf(offset)); + return out; + } + + isLoaded(offset: number): boolean { + return this.byteAt(offset) !== undefined; + } + + /** + * The overlay only ever holds a value that differs from the byte the + * server has, so membership is the answer even after the chunk it came + * from has been evicted. + */ + isModified(offset: number): boolean { + return this.overlay.has(offset); + } + + writeByte(offset: number, value: number): boolean { + if (this.staleInfo || offset < 0 || offset >= this.size) return false; + const original = this.originalAt(offset); + if (original === undefined) return false; + const next = value & 0xff; + if (next === original) { + this.overlay.delete(offset); + } else { + this.overlay.set(offset, next); + } + this.emitter.emit(); + return true; + } + + writeBytes(offset: number, values: Uint8Array): boolean { + if (this.staleInfo) return false; + // All or nothing: a multi-byte value that straddles a chunk boundary + // waits until both windows are here rather than landing half written. + if (this.readBytes(offset, values.length) === null) return false; + for (let i = 0; i < values.length; i += 1) { + const at = offset + i; + const original = this.originalAt(at); + if (original === undefined) return false; + const next = values[i] & 0xff; + if (next === original) { + this.overlay.delete(at); + } else { + this.overlay.set(at, next); + } + } + this.emitter.emit(); + return true; + } + + insertAt(): boolean { + return false; + } + + deleteAt(): boolean { + return false; + } + + ensureRange(offset: number, length: number): void { + if (this.disposed || this.staleInfo) return; + this.loader.ensureRange(offset, length); + } + + retryFailed(): void { + this.loader.retryFailed(); + } + + hasUnsavedChanges(): boolean { + return this.overlay.size > 0; + } + + async buildSnapshot(onProgress?: ProgressCallback, signal?: AbortSignal): Promise { + // The overlay is taken now: an edit made while the file is being read + // back belongs to the next save, not this one. + const overlay = new Map(this.overlay); + this.loader.pause(); + try { + const payload = await buildBlob(this.reader, this.size, overlay, { + onProgress, + signal, + blockSize: this.options.blockSize, + retry: this.options.retry, + }); + return { mode: 'windowed', size: this.size, payload, overlay }; + } finally { + this.loader.resume(); + } + } + + markSaved(snapshot: SaveSnapshot): void { + if (snapshot.mode !== 'windowed') return; + for (const [offset, value] of snapshot.overlay) { + // The cache now holds what the server holds, so the window stays + // usable and nothing has to be read again. + this.cache.patch(offset, value); + if (this.overlay.get(offset) === value) this.overlay.delete(offset); + } + this.emitter.emit(); + } + + discardChanges(): void { + this.overlay.clear(); + this.emitter.emit(); + } + + dispose(): void { + this.disposed = true; + this.loader.dispose(); + this.emitter.clear(); + } + + subscribe(listener: () => void): () => void { + return this.emitter.subscribe(listener); + } + + /** The server's byte, ignoring any edit; undefined when not read yet. */ + private originalAt(offset: number): number | undefined { + const chunk = this.cache.peek(chunkIndexOf(offset)); + if (chunk === undefined) return undefined; + const at = offset - chunkStart(chunkIndexOf(offset)); + return at < chunk.length ? chunk[at] : undefined; + } +} diff --git a/frontend/src/mocks/files.ts b/frontend/src/mocks/files.ts new file mode 100644 index 0000000..de0b8e2 --- /dev/null +++ b/frontend/src/mocks/files.ts @@ -0,0 +1,125 @@ +/** + * Sample files for the debug harness, generated rather than stored: a few + * hundred megabytes of fixtures would not fit in the repository, and the + * editor only ever asks for windows anyway. + */ +export interface SampleFile { + path: string; + size: number; + mtime: number; + /** Answers an empty 200, like a panel that ignores file responses. */ + legacyEmpty?: boolean; + /** Answers 403 to everything. */ + forbidden?: boolean; + /** Fails every third request, so the retry banner can be seen. */ + flaky?: boolean; + /** Reports a new mtime on every stat, so a save hits a conflict. */ + drifting?: boolean; +} + +const HOUR = 3600; +const NOW = Math.floor(Date.now() / 1000); + +export const SAMPLES: SampleFile[] = [ + { path: 'hex-samples/empty.bin', size: 0, mtime: NOW - HOUR }, + { path: 'hex-samples/tiny.bin', size: 7, mtime: NOW - HOUR }, + { path: 'hex-samples/exact-4mib.bin', size: 4 * 1024 * 1024, mtime: NOW - 2 * HOUR }, + { path: 'hex-samples/4mib-plus-1.bin', size: 4 * 1024 * 1024 + 1, mtime: NOW - 2 * HOUR }, + { path: 'hex-samples/big-40mib.bin', size: 40 * 1024 * 1024, mtime: NOW - 3 * HOUR }, + { path: 'hex-samples/huge-150mib.bin', size: 150 * 1024 * 1024, mtime: NOW - 4 * HOUR }, + { path: 'hex-samples/giant-5gib.bin', size: 5 * 1024 ** 3, mtime: NOW - 5 * HOUR }, + { path: 'hex-samples/changing.bin', size: 1024 * 1024, mtime: NOW - HOUR, drifting: true }, + { path: 'hex-samples/flaky.bin', size: 8 * 1024 * 1024, mtime: NOW - HOUR, flaky: true }, + { path: 'hex-samples/forbidden.bin', size: 1024, mtime: NOW - HOUR, forbidden: true }, + { path: 'hex-samples/legacy-empty.bin', size: 2048, mtime: NOW - HOUR, legacyEmpty: true }, +]; + +/** + * The harness's own root, mirrored so the sample directory can be added to it + * (see its `src/mocks/files.ts`). A resolver cannot extend an answer it does + * not produce, so listing the root means producing all of it. + */ +export const HARNESS_ROOT_DIRECTORIES = ['config', 'logs', 'plugins', 'world']; + +export const HARNESS_ROOT_FILES: SampleFile[] = [ + { path: 'server.properties', size: 512, mtime: NOW - 24 * HOUR }, + { path: 'start.sh', size: 256, mtime: NOW - 48 * HOUR }, + { path: 'readme.txt', size: 454, mtime: NOW - HOUR }, + { path: 'server.jar', size: 11_197_597, mtime: NOW - 604_800 }, + { path: 'backup.zip', size: 27_160, mtime: NOW - 2 * HOUR }, +]; + +/** + * Every file the mock lists, wherever it was listed: an editor offered a file + * it cannot then read would be a worse fixture than no fixture. + */ +export function findSample(path: string): SampleFile | undefined { + const normalized = path.replace(/^\/+/, ''); + return [...SAMPLES, ...HARNESS_ROOT_FILES].find((sample) => sample.path === normalized); +} + +const MARKER = 'MiB#'; + +/** + * Deterministic bytes with a readable marker at every mebibyte, so a window + * can be recognised on sight in the editor. + */ +export function patternBytes(start: number, length: number): Uint8Array { + const out = new Uint8Array(length); + for (let i = 0; i < length; i += 1) { + const offset = start + i; + const inMarker = offset % (1024 * 1024); + if (inMarker < 16) { + // Exactly sixteen bytes, so one marker fills one row of the grid. + const text = `${MARKER}${String(Math.floor(offset / (1024 * 1024))).padStart(10, '0')} \n`; + out[i] = text.charCodeAt(inMarker); + } else { + out[i] = (Math.imul(offset, 0x9e3779b1) >>> 24) & 0xff; + } + } + return out; +} + +export interface ResolvedRange { + start: number; + end: number; + partial: boolean; + satisfiable: boolean; +} + +/** + * A `Range` header against a known size, resolved the way the panel's own + * fileref server resolves it: a single range only, an end past the file + * clamped, a start past the file unsatisfiable, anything malformed ignored in + * favour of the whole file. + */ +export function resolveRange(header: string | null, size: number): ResolvedRange { + const whole: ResolvedRange = { start: 0, end: Math.max(0, size - 1), partial: false, satisfiable: true }; + if (!header) return whole; + const spec = header.trim(); + if (!spec.toLowerCase().startsWith('bytes=')) return whole; + const value = spec.slice('bytes='.length).trim(); + if (!value || value.includes(',')) return whole; + + const dash = value.indexOf('-'); + if (dash < 0) return whole; + const first = value.slice(0, dash).trim(); + const last = value.slice(dash + 1).trim(); + + if (first === '') { + const suffix = Number(last); + if (!Number.isSafeInteger(suffix) || suffix <= 0) return whole; + if (size === 0) return { start: 0, end: 0, partial: true, satisfiable: false }; + const length = Math.min(suffix, size); + return { start: size - length, end: size - 1, partial: true, satisfiable: true }; + } + + const start = Number(first); + if (!Number.isSafeInteger(start)) return whole; + if (start >= size) return { start, end: start, partial: true, satisfiable: false }; + if (last === '') return { start, end: size - 1, partial: true, satisfiable: true }; + + const end = Number(last); + if (!Number.isSafeInteger(end) || end < start) return whole; + return { start, end: Math.min(end, size - 1), partial: true, satisfiable: true }; +} diff --git a/frontend/src/mocks/handlers.ts b/frontend/src/mocks/handlers.ts new file mode 100644 index 0000000..bb273b7 --- /dev/null +++ b/frontend/src/mocks/handlers.ts @@ -0,0 +1,203 @@ +import { BASE } from '../api/client'; +import { + HARNESS_ROOT_DIRECTORIES, + HARNESS_ROOT_FILES, + SAMPLES, + findSample, + patternBytes, + resolveRange, + type SampleFile, +} from './files'; + +/** + * Answers the plugin's own routes under `npm run debug`. + * + * The harness mocks the panel's file manager but knows nothing about a + * plugin's routes, and its download mock serves neither ranges nor anything + * larger than a kilobyte. Everything a large file needs is generated here. + */ +export function registerMockHandlers(): void { + const debug = window.gameapDebug; + if (!debug) return; + + const { http, HttpResponse, delay } = debug.msw; + /** mtime the mock reports, moved on by a save or by a drifting sample. */ + const times = new Map(); + const sizes = new Map(); + let flakyCalls = 0; + + function mtimeOf(sample: SampleFile): number { + if (sample.drifting) { + const next = (times.get(sample.path) ?? sample.mtime) + 1; + times.set(sample.path, next); + return next; + } + return times.get(sample.path) ?? sample.mtime; + } + + function sizeOf(sample: SampleFile): number { + return sizes.get(sample.path) ?? sample.size; + } + + function jsonError(status: number, code: string, message: string): Response { + return HttpResponse.json({ code, message }, { status }); + } + + debug.registerMockHandlers([ + http.get(`${BASE}/servers/:serverId/file/info`, async ({ request }) => { + await delay(80); + const path = new URL(request.url).searchParams.get('path') ?? ''; + const sample = findSample(path); + if (!sample) return jsonError(404, 'NOT_FOUND', 'file not found'); + if (sample.forbidden) { + return jsonError(403, 'FORBIDDEN', "no access to this server's files"); + } + return HttpResponse.json({ + size: sizeOf(sample), + mtime: mtimeOf(sample), + path: path.replace(/^\/+/, ''), + }); + }), + + http.get(`${BASE}/servers/:serverId/file`, async ({ request }) => { + await delay(120); + const path = new URL(request.url).searchParams.get('path') ?? ''; + const sample = findSample(path); + if (!sample) return jsonError(404, 'NOT_FOUND', 'file not found'); + if (sample.forbidden) { + return jsonError(403, 'FORBIDDEN', "no access to this server's files"); + } + if (sample.legacyEmpty) { + // What a panel older than 4.5 sends: the file reference is + // ignored and the body stays empty. + return new HttpResponse(null, { status: 200 }); + } + if (sample.flaky) { + flakyCalls += 1; + if (flakyCalls % 3 === 0) { + return jsonError(500, 'INTERNAL_ERROR', 'the daemon is having a moment'); + } + } + + const size = sizeOf(sample); + const range = resolveRange(request.headers.get('range'), size); + if (!range.satisfiable) { + return new HttpResponse(null, { + status: 416, + headers: { 'Content-Range': `bytes */${size}` }, + }); + } + + const length = size === 0 ? 0 : range.end - range.start + 1; + const body = patternBytes(range.start, length); + const headers: Record = { + 'Content-Type': 'application/octet-stream', + 'Accept-Ranges': 'bytes', + 'Content-Length': String(length), + }; + if (range.partial) { + headers['Content-Range'] = `bytes ${range.start}-${range.end}/${size}`; + } + return new HttpResponse(body as unknown as BodyInit, { + status: range.partial ? 206 : 200, + headers, + }); + }), + + // The harness only mocks the legacy update path, and the editor saves + // through the current one. + http.post('/api/file-manager/:serverId/update-file', async ({ request }) => { + await delay(200); + const form = await request.formData(); + const path = String(form.get('path') ?? ''); + const file = form.get('file'); + const name = file instanceof File ? file.name : ''; + const full = [path, name].filter(Boolean).join('/').replace(/^\/+/, ''); + const sample = findSample(full); + if (!sample || !(file instanceof File)) { + return HttpResponse.json({ result: { status: 'error', message: 'unknown file' } }, { status: 404 }); + } + sizes.set(sample.path, file.size); + const written = Math.floor(Date.now() / 1000); + times.set(sample.path, written); + // The real endpoint answers the written file as well, and the + // panel's store puts it straight back into the listing; without + // it the store throws and the editor never hears that the write + // went through. + return HttpResponse.json({ + result: { status: 'success', message: 'File updated!' }, + file: { ...fileEntry(sample, file.size, written), mimetype: 'application/octet-stream' }, + }); + }), + + // Adds the sample directory to the root and lists it; every other + // directory falls through to the harness's own file system. + http.get('/api/file-manager/:serverId/content', async ({ request }) => { + const path = new URL(request.url).searchParams.get('path') ?? ''; + + if (path === 'hex-samples') { + await delay(80); + return HttpResponse.json({ + result: { status: 'success', message: null }, + directories: [], + files: SAMPLES.filter((sample) => sample.path.startsWith('hex-samples/')).map( + (sample) => fileEntry(sample, sizeOf(sample), mtimeOf(sample)), + ), + }); + } + + if (path === '' || path === '.' || path === '/') { + await delay(80); + return HttpResponse.json({ + result: { status: 'success', message: null }, + // Mirrors the harness's own root (see its + // src/mocks/files.ts): a resolver cannot add to an answer + // it does not produce, so listing the root means + // producing all of it. + directories: [ + ...HARNESS_ROOT_DIRECTORIES.map(directoryEntry), + directoryEntry('hex-samples'), + ], + files: HARNESS_ROOT_FILES.map((entry) => + fileEntry( + entry, + sizes.get(entry.path) ?? entry.size, + times.get(entry.path) ?? entry.mtime, + ), + ), + }); + } + + return undefined; + }), + ]); +} + +function directoryEntry(name: string) { + return { + type: 'dir', + path: name, + basename: name, + dirname: '', + extension: '', + filename: name, + timestamp: Math.floor(Date.now() / 1000), + visibility: 'public', + }; +} + +function fileEntry(sample: Pick, size: number, mtime: number) { + const basename = sample.path.split('/').pop() ?? sample.path; + const dot = basename.lastIndexOf('.'); + return { + type: 'file', + path: sample.path, + basename, + dirname: sample.path.split('/').slice(0, -1).join('/'), + extension: dot > 0 ? basename.slice(dot + 1) : '', + filename: dot > 0 ? basename.slice(0, dot) : basename, + size, + timestamp: mtime, + visibility: 'private', + }; +} diff --git a/frontend/src/translations/de.ts b/frontend/src/translations/de.ts new file mode 100644 index 0000000..f76212a --- /dev/null +++ b/frontend/src/translations/de.ts @@ -0,0 +1,58 @@ +export const de = { + hex_editor: 'Hex-Editor', + offset: 'Offset', + hex: 'Hex', + ascii: 'ASCII', + modified: 'Geändert', + bytes: 'Bytes', + file_size: 'Dateigröße', + selected: 'Ausgewählt', + byte: 'Byte', + value: 'Wert', + data_inspector: 'Dateninspektor', + binary: 'Binär', + little_endian: 'Little-Endian', + big_endian: 'Big-Endian', + add_byte: 'Byte hinzufügen', + + loading: 'Laden', + loading_file: 'Datei wird geladen…', + go_to_offset: 'Zum Offset springen', + go_to_offset_placeholder: '0x0000', + invalid_offset: 'Ungültiger Offset', + offset_past_end: 'Offset liegt hinter dem Dateiende', + last_modified: 'Zuletzt geändert', + windowed_mode: 'Große Datei', + windowed_mode_hint: + 'Dateien größer als :limit werden direkt bearbeitet: Bytes können überschrieben, aber nicht eingefügt oder gelöscht werden', + insert_delete_unavailable: + 'Einfügen und Löschen von Bytes ist für Dateien größer als :limit nicht verfügbar', + view_only: 'Nur ansehen', + view_only_hint: 'Dateien größer als :limit können nicht aus dem Browser gespeichert werden', + not_loaded: 'Noch nicht geladen', + load_failed: 'Ein Teil der Datei konnte nicht geladen werden', + retry: 'Wiederholen', + reload: 'Neu laden', + file_changed_size: + 'Die Datei hat sich auf dem Server geändert (:old → :new). Neu laden, um fortzufahren.', + file_changed_overwrite: 'Die Datei hat sich auf dem Server seit dem Öffnen geändert. Überschreiben?', + overwrite: 'Überschreiben', + cancel: 'Abbrechen', + discard_changes: ':count geänderte Bytes verwerfen?', + discard: 'Verwerfen', + no_changes: 'Keine Änderungen zu speichern', + preparing_upload: 'Datei wird für den Upload vorbereitet… :percent%', + uploading: 'Wird hochgeladen…', + saved: 'Gespeichert', + save_failed: 'Die Datei konnte nicht zum Speichern vorbereitet werden', + panel_too_old: 'Diese Version des Hex-Editors erfordert GameAP 4.5 oder neuer', + panel_too_old_hint: + 'Aktualisieren Sie das Panel oder installieren Sie den Hex-Editor 1.0.x für ältere Panels', + file_responses_disabled: 'Dateiantworten sind auf diesem Panel nicht aktiviert', + permission_missing: 'Das Plugin hat keine Berechtigung, Serverdateien zu lesen', + access_denied: 'Zugriff verweigert', + file_not_found: 'Datei nicht gefunden', + network_error: 'Netzwerkfehler', + no_server: 'Der Editor kann nur aus dem Dateimanager eines Servers geöffnet werden', + empty_file: 'Leere Datei', +}; diff --git a/frontend/src/translations/en.ts b/frontend/src/translations/en.ts new file mode 100644 index 0000000..e1d662e --- /dev/null +++ b/frontend/src/translations/en.ts @@ -0,0 +1,55 @@ +export const en = { + hex_editor: 'Hex Editor', + offset: 'Offset', + hex: 'Hex', + ascii: 'ASCII', + modified: 'Modified', + bytes: 'bytes', + file_size: 'File size', + selected: 'Selected', + byte: 'byte', + value: 'Value', + data_inspector: 'Data Inspector', + binary: 'Binary', + little_endian: 'Little-Endian', + big_endian: 'Big-Endian', + add_byte: 'Add byte', + + loading: 'Loading', + loading_file: 'Loading file…', + go_to_offset: 'Go to offset', + go_to_offset_placeholder: '0x0000', + invalid_offset: 'Invalid offset', + offset_past_end: 'Offset is past the end of the file', + last_modified: 'Last modified', + windowed_mode: 'Large file', + windowed_mode_hint: + 'Files larger than :limit are edited in place: bytes can be overwritten, but not inserted or deleted', + insert_delete_unavailable: 'Inserting and deleting bytes is unavailable for files larger than :limit', + view_only: 'View only', + view_only_hint: 'Files larger than :limit cannot be saved from the browser', + not_loaded: 'Not loaded yet', + load_failed: 'Failed to load part of the file', + retry: 'Retry', + reload: 'Reload', + file_changed_size: 'The file changed on the server (:old → :new). Reload to continue.', + file_changed_overwrite: 'The file changed on the server since it was opened. Overwrite it?', + overwrite: 'Overwrite', + cancel: 'Cancel', + discard_changes: 'Discard :count modified bytes?', + discard: 'Discard', + no_changes: 'No changes to save', + preparing_upload: 'Preparing file for upload… :percent%', + uploading: 'Uploading…', + saved: 'Saved', + save_failed: 'Could not prepare the file for saving', + panel_too_old: 'This version of the hex editor requires GameAP 4.5 or newer', + panel_too_old_hint: 'Update the panel, or install hex editor 1.0.x for older panels', + file_responses_disabled: 'File responses are not enabled on this panel', + permission_missing: 'The plugin has no permission to read server files', + access_denied: 'Access denied', + file_not_found: 'File not found', + network_error: 'Network error', + no_server: "The editor can only be opened from a server's file manager", + empty_file: 'Empty file', +}; diff --git a/frontend/src/translations/es.ts b/frontend/src/translations/es.ts new file mode 100644 index 0000000..23bcda8 --- /dev/null +++ b/frontend/src/translations/es.ts @@ -0,0 +1,56 @@ +export const es = { + hex_editor: 'Editor hexadecimal', + offset: 'Desplazamiento', + hex: 'Hex', + ascii: 'ASCII', + modified: 'Modificado', + bytes: 'bytes', + file_size: 'Tamaño del archivo', + selected: 'Seleccionado', + byte: 'byte', + value: 'Valor', + data_inspector: 'Inspector de datos', + binary: 'Binario', + little_endian: 'Little-Endian', + big_endian: 'Big-Endian', + add_byte: 'Añadir byte', + + loading: 'Cargando', + loading_file: 'Cargando archivo…', + go_to_offset: 'Ir al desplazamiento', + go_to_offset_placeholder: '0x0000', + invalid_offset: 'Desplazamiento no válido', + offset_past_end: 'El desplazamiento está fuera del archivo', + last_modified: 'Última modificación', + windowed_mode: 'Archivo grande', + windowed_mode_hint: + 'Los archivos de más de :limit se editan in situ: los bytes se pueden sobrescribir, pero no insertar ni eliminar', + insert_delete_unavailable: + 'Insertar y eliminar bytes no está disponible para archivos de más de :limit', + view_only: 'Solo lectura', + view_only_hint: 'Los archivos de más de :limit no se pueden guardar desde el navegador', + not_loaded: 'Aún no cargado', + load_failed: 'No se pudo cargar parte del archivo', + retry: 'Reintentar', + reload: 'Recargar', + file_changed_size: 'El archivo cambió en el servidor (:old → :new). Recarga para continuar.', + file_changed_overwrite: 'El archivo cambió en el servidor desde que se abrió. ¿Sobrescribirlo?', + overwrite: 'Sobrescribir', + cancel: 'Cancelar', + discard_changes: '¿Descartar :count bytes modificados?', + discard: 'Descartar', + no_changes: 'No hay cambios para guardar', + preparing_upload: 'Preparando el archivo para subir… :percent%', + uploading: 'Subiendo…', + saved: 'Guardado', + save_failed: 'No se pudo preparar el archivo para guardarlo', + panel_too_old: 'Esta versión del editor hexadecimal requiere GameAP 4.5 o superior', + panel_too_old_hint: 'Actualiza el panel o instala el editor hexadecimal 1.0.x para paneles antiguos', + file_responses_disabled: 'Este panel no tiene habilitadas las respuestas de archivos', + permission_missing: 'El plugin no tiene permiso para leer los archivos del servidor', + access_denied: 'Acceso denegado', + file_not_found: 'Archivo no encontrado', + network_error: 'Error de red', + no_server: 'El editor solo se puede abrir desde el gestor de archivos de un servidor', + empty_file: 'Archivo vacío', +}; diff --git a/frontend/src/translations/index.ts b/frontend/src/translations/index.ts new file mode 100644 index 0000000..a01116f --- /dev/null +++ b/frontend/src/translations/index.ts @@ -0,0 +1,6 @@ +import { de } from './de'; +import { en } from './en'; +import { es } from './es'; +import { ru } from './ru'; + +export const translations = { de, en, es, ru }; diff --git a/frontend/src/translations/ru.ts b/frontend/src/translations/ru.ts new file mode 100644 index 0000000..c9b9c3b --- /dev/null +++ b/frontend/src/translations/ru.ts @@ -0,0 +1,55 @@ +export const ru = { + hex_editor: 'Hex-редактор', + offset: 'Смещение', + hex: 'Hex', + ascii: 'ASCII', + modified: 'Изменено', + bytes: 'байт', + file_size: 'Размер файла', + selected: 'Выбрано', + byte: 'байт', + value: 'Значение', + data_inspector: 'Инспектор данных', + binary: 'Двоичное', + little_endian: 'Little-Endian', + big_endian: 'Big-Endian', + add_byte: 'Добавить байт', + + loading: 'Загрузка', + loading_file: 'Загрузка файла…', + go_to_offset: 'Перейти к смещению', + go_to_offset_placeholder: '0x0000', + invalid_offset: 'Некорректное смещение', + offset_past_end: 'Смещение за пределами файла', + last_modified: 'Изменён', + windowed_mode: 'Большой файл', + windowed_mode_hint: + 'Файлы больше :limit редактируются на месте: байты можно перезаписать, но не вставить или удалить', + insert_delete_unavailable: 'Вставка и удаление байтов недоступны для файлов больше :limit', + view_only: 'Только просмотр', + view_only_hint: 'Файлы больше :limit нельзя сохранить из браузера', + not_loaded: 'Ещё не загружено', + load_failed: 'Не удалось загрузить часть файла', + retry: 'Повторить', + reload: 'Перезагрузить', + file_changed_size: 'Файл изменился на сервере (:old → :new). Перезагрузите, чтобы продолжить.', + file_changed_overwrite: 'Файл изменился на сервере с момента открытия. Перезаписать?', + overwrite: 'Перезаписать', + cancel: 'Отмена', + discard_changes: 'Отменить изменения (:count байт)?', + discard: 'Отменить', + no_changes: 'Нет изменений для сохранения', + preparing_upload: 'Подготовка файла к загрузке… :percent%', + uploading: 'Загрузка на сервер…', + saved: 'Сохранено', + save_failed: 'Не удалось подготовить файл к сохранению', + panel_too_old: 'Эта версия hex-редактора требует GameAP 4.5 или новее', + panel_too_old_hint: 'Обновите панель или установите hex-редактор 1.0.x для старых версий', + file_responses_disabled: 'На этой панели отключена отдача файлов плагинами', + permission_missing: 'У плагина нет разрешения на чтение файлов сервера', + access_denied: 'Доступ запрещён', + file_not_found: 'Файл не найден', + network_error: 'Ошибка сети', + no_server: 'Редактор открывается только из файлового менеджера сервера', + empty_file: 'Пустой файл', +}; diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json index d2d6d59..85e0f71 100644 --- a/frontend/tsconfig.json +++ b/frontend/tsconfig.json @@ -16,8 +16,16 @@ "strict": true, "noUnusedLocals": true, "noUnusedParameters": true, - "noFallthroughCasesInSwitch": true + "noFallthroughCasesInSwitch": true, + + "types": ["vitest/globals"], + + "baseUrl": ".", + "paths": { + "@/*": ["src/*"], + "vue": ["./node_modules/vue"] + } }, "include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"], "references": [{ "path": "./tsconfig.node.json" }] -} \ No newline at end of file +} diff --git a/frontend/tsconfig.node.json b/frontend/tsconfig.node.json index c7f60c6..354867f 100644 --- a/frontend/tsconfig.node.json +++ b/frontend/tsconfig.node.json @@ -1,6 +1,7 @@ { "compilerOptions": { "composite": true, + "allowJs": true, "skipLibCheck": true, "module": "ESNext", "moduleResolution": "bundler", diff --git a/frontend/vite.config.js b/frontend/vite.config.js index fb5b215..8bd2784 100644 --- a/frontend/vite.config.js +++ b/frontend/vite.config.js @@ -8,6 +8,10 @@ function globalExternalsPlugin() { 'vue-router': 'window.VueRouter', 'pinia': 'window.Pinia', 'axios': 'window.axios', + 'naive-ui': 'window.NaiveUI', + // Re-exported by @gameap/plugin-sdk; the panel and the debug harness + // both publish it, so the components stay out of the bundle. + '@gameap/ui': 'window.gameapUI', }; return { @@ -39,6 +43,15 @@ function globalExternalsPlugin() { return `const ${name} = ${globalVar};`; }); + // A side-effect-only import of an externalized module would + // reach the browser as a bare specifier and fail to resolve; + // the global is already there, so the line simply goes. + const bareImportRegex = new RegExp( + `import\\s*["']${moduleId}["'];?`, + 'g' + ); + result = result.replace(bareImportRegex, ''); + const importDefaultRegex = new RegExp( `import\\s+(\\w+)\\s*from\\s*["']${moduleId}["'];?`, 'g' @@ -83,13 +96,15 @@ export default defineConfig({ outDir: 'dist', emptyOutDir: true, rollupOptions: { - external: ['vue', 'vue-router', 'pinia', 'axios'], + external: ['vue', 'vue-router', 'pinia', 'axios', 'naive-ui', '@gameap/ui'], output: { globals: { vue: 'Vue', 'vue-router': 'VueRouter', pinia: 'Pinia', axios: 'axios', + 'naive-ui': 'NaiveUI', + '@gameap/ui': 'gameapUI', }, }, plugins: [globalExternalsPlugin(), wrapInIIFEPlugin()], @@ -101,4 +116,11 @@ export default defineConfig({ // @gameap/plugin-sdk is resolved from node_modules automatically }, }, + test: { + // Everything under test is pure logic: chunk math, byte overlays, + // scroll geometry, URL building. No DOM, so no jsdom. + environment: 'node', + globals: true, + include: ['src/**/__tests__/**/*.test.ts'], + }, }); \ No newline at end of file diff --git a/rust-toolchain.toml b/rust-toolchain.toml index 55e6ebd..cdd166b 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,3 +1,5 @@ [toolchain] channel = "1.94.0" targets = ["wasm32-wasip1"] +# GitHub runners install rustup with the minimal profile, which omits clippy +components = ["clippy"] diff --git a/src/authz.rs b/src/authz.rs new file mode 100644 index 0000000..69581fe --- /dev/null +++ b/src/authz.rs @@ -0,0 +1,194 @@ +//! Authorization: session extraction and RBAC checks through `gameap-authz`. +//! +//! The host enforces `requires_auth` before the guest runs; the checks here add +//! the per-server file ability the panel's own file manager requires. An authz +//! host failure maps to 502 and is never read as a denial. + +use gameap_plugin_sdk::proto::gameap::plugin as pb; + +use crate::host_api::{Entity, HostApi, HostApiError}; +use crate::http::{ApiError, ERR_AUTHZ_UNAVAILABLE, ERR_HOST_CALL_FAILED}; + +/// The panel's built-in ability that only administrator roles hold +/// (`internal/domain/rbac.go: AbilityNameAdminRolesPermissions`). +pub const ADMIN_ABILITY: &str = "admin roles & permissions"; + +/// The panel's per-server file ability, the one its file manager checks before +/// reading or writing a server file (`internal/domain/rbac.go: +/// AbilityNameGameServerFiles`). Reading bytes through this plugin is the same +/// act, so it is gated on the same ability rather than on one of its own. +pub const PANEL_ABILITY_FILES: &str = "game-server-files"; + +/// The authenticated caller, extracted from the request session. +#[derive(Debug, Clone, PartialEq, Eq, Default)] +pub struct SessionInfo { + pub user_id: u64, + pub login: String, +} + +impl SessionInfo { + pub fn from_request(req: &pb::HttpRequest) -> Self { + match req.session.as_ref().and_then(|s| s.user.as_ref()) { + Some(user) => Self { + user_id: user.id, + login: user.login.clone(), + }, + None => Self::default(), + } + } + + pub fn is_anonymous(&self) -> bool { + self.user_id == 0 + } +} + +fn authz_error(err: HostApiError) -> ApiError { + match err { + HostApiError::Call(m) => ApiError::new(502, ERR_HOST_CALL_FAILED, m), + HostApiError::Op(m) => ApiError::new( + 502, + ERR_AUTHZ_UNAVAILABLE, + format!("permission check could not be performed: {m}"), + ), + } +} + +/// Per-request authorization helper. Memoizes the admin check, which several +/// steps of one request consult. +#[derive(Default)] +pub struct Authorizer { + is_admin_memo: Option, +} + +impl Authorizer { + pub fn new() -> Self { + Self::default() + } + + /// Whether the caller holds the panel admin ability. + pub fn is_admin( + &mut self, + host: &mut H, + session: &SessionInfo, + ) -> Result { + if session.is_anonymous() { + return Ok(false); + } + if let Some(memo) = self.is_admin_memo { + return Ok(memo); + } + let allowed = host + .authz_can(session.user_id, &[ADMIN_ABILITY]) + .map_err(authz_error)?; + self.is_admin_memo = Some(allowed); + Ok(allowed) + } + + /// Admin, or a user granted the panel ability on the server. The panel's + /// built-in abilities are checked verbatim, without a plugin namespace. + pub fn can_panel_server( + &mut self, + host: &mut H, + session: &SessionInfo, + server_id: u64, + ability: &str, + ) -> Result { + if session.is_anonymous() { + return Ok(false); + } + if self.is_admin(host, session)? { + return Ok(true); + } + host.authz_can_any_for_entity(session.user_id, Entity::server(server_id), &[ability]) + .map_err(authz_error) + } + + pub fn require_files_access( + &mut self, + host: &mut H, + session: &SessionInfo, + server_id: u64, + ) -> Result<(), ApiError> { + if self.can_panel_server(host, session, server_id, PANEL_ABILITY_FILES)? { + Ok(()) + } else { + Err(ApiError::forbidden("no access to this server's files")) + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::host_api::mock::MockHost; + + fn session(user_id: u64) -> SessionInfo { + SessionInfo { + user_id, + login: format!("user{user_id}"), + } + } + + #[test] + fn anonymous_is_denied_without_a_host_call() { + let mut host = MockHost::standard(); + host.authz_down = true; + let mut authz = Authorizer::new(); + let err = authz + .require_files_access(&mut host, &SessionInfo::default(), 3) + .unwrap_err(); + assert_eq!(err.status, 403); + assert_eq!(host.authz_calls, 0); + } + + #[test] + fn admin_bypasses_the_server_ability() { + let mut host = MockHost::standard(); + let mut authz = Authorizer::new(); + assert!(authz.is_admin(&mut host, &session(1)).unwrap()); + authz.require_files_access(&mut host, &session(1), 3).unwrap(); + authz.require_files_access(&mut host, &session(1), 999).unwrap(); + } + + #[test] + fn the_files_ability_on_the_server_allows() { + let mut host = MockHost::standard(); + host.grant_server(7, 3, PANEL_ABILITY_FILES); + let mut authz = Authorizer::new(); + authz.require_files_access(&mut host, &session(7), 3).unwrap(); + } + + #[test] + fn an_ability_on_another_server_does_not_allow() { + let mut host = MockHost::standard(); + host.grant_server(7, 4, PANEL_ABILITY_FILES); + let mut authz = Authorizer::new(); + let err = authz + .require_files_access(&mut host, &session(7), 3) + .unwrap_err(); + assert_eq!(err.status, 403); + assert_eq!(err.code, "FORBIDDEN"); + } + + #[test] + fn an_authz_failure_is_502_not_a_denial() { + let mut host = MockHost::standard(); + host.authz_down = true; + let mut authz = Authorizer::new(); + let err = authz + .require_files_access(&mut host, &session(7), 3) + .unwrap_err(); + assert_eq!(err.status, 502); + assert_eq!(err.code, ERR_AUTHZ_UNAVAILABLE); + } + + #[test] + fn the_admin_check_is_memoized() { + let mut host = MockHost::standard(); + let mut authz = Authorizer::new(); + assert!(authz.is_admin(&mut host, &session(1)).unwrap()); + assert!(authz.is_admin(&mut host, &session(1)).unwrap()); + authz.require_files_access(&mut host, &session(1), 3).unwrap(); + assert_eq!(host.authz_calls, 1); + } +} diff --git a/src/handlers/files.rs b/src/handlers/files.rs new file mode 100644 index 0000000..29a6317 --- /dev/null +++ b/src/handlers/files.rs @@ -0,0 +1,102 @@ +//! The two file routes. Both resolve the same thing - which node file the +//! caller may read - and differ only in the answer: a file reference the panel +//! streams, or a stat. + +use serde::Serialize; + +use crate::authz::Authorizer; +use crate::host_api::{HostApi, NodeInfo, ServerInfo}; +use crate::http::{ApiError, ApiResult, file_response, json_response, parse_u64_param, query_param_joined}; +use crate::paths; +use crate::router::RequestParts; + +const CONTENT_TYPE_BINARY: &str = "application/octet-stream"; + +pub struct FileCtx { + pub server: ServerInfo, + pub node: NodeInfo, + /// The validated path, relative to the server directory. + pub rel: String, + /// The node path, joined as the panel's file manager joins it. + pub abs: String, +} + +#[derive(Serialize)] +struct InfoResponse { + size: u64, + mtime: u64, + path: String, +} + +/// Session → input → ability → server → node → path. The ability is checked +/// before the server is looked up, so a caller without access learns nothing +/// about which server ids exist. Authorization failures are a 502, never a +/// denial. Every step is a panel-local call; the daemon is not involved. +fn resolve(host: &mut H, parts: &RequestParts) -> Result { + if parts.session.is_anonymous() { + return Err(ApiError::unauthenticated("authentication required")); + } + let server_id = parse_u64_param(&parts.params, "serverId")?; + let raw = query_param_joined(parts.query, "path"); + let rel = paths::validate_relative(&raw).map_err(|err| ApiError::bad_request(err.message()))?; + + let mut authz = Authorizer::new(); + authz.require_files_access(host, &parts.session, server_id)?; + + let server = host + .get_server(server_id)? + .ok_or_else(|| ApiError::not_found(format!("server {server_id} not found")))?; + // The panel's own file endpoints keep a blocked server's files from its + // owner while an administrator can still reach them. + if server.blocked && !authz.is_admin(host, &parts.session)? { + return Err(ApiError::forbidden("server is blocked")); + } + // The panel would join an empty directory into the node work path itself; + // refusing is the only answer that cannot expose the wrong tree. + if server.dir.trim().is_empty() { + return Err(ApiError::internal("server directory is not configured")); + } + let node = host + .get_node(server.node_id)? + .ok_or_else(|| ApiError::not_found(format!("node {} not found", server.node_id)))?; + let abs = paths::join_node_path(&node.work_path, &server.dir, &rel); + Ok(FileCtx { + server, + node, + rel, + abs, + }) +} + +/// The file itself, as a reference the panel streams. No stat here: the panel +/// stats the file when it opens it (404 missing, 400 directory) and serves a +/// single `Range`, which is how the frontend reads a window of a large file. +pub fn file(host: &mut H, parts: &RequestParts) -> ApiResult { + let ctx = resolve(host, parts)?; + Ok(file_response( + ctx.node.id, + &ctx.abs, + paths::file_name(&ctx.rel), + CONTENT_TYPE_BINARY, + )) +} + +/// Size and modification time from a fresh stat on the node, and the +/// normalized relative path echoed back. +pub fn info(host: &mut H, parts: &RequestParts) -> ApiResult { + let ctx = resolve(host, parts)?; + let Some(meta) = host.get_file_info(ctx.node.id, &ctx.abs)? else { + return Err(ApiError::not_found("file not found")); + }; + if meta.is_dir { + return Err(ApiError::bad_request("path is a directory")); + } + Ok(json_response( + 200, + &InfoResponse { + size: meta.size, + mtime: meta.mtime, + path: ctx.rel, + }, + )) +} diff --git a/src/handlers/mod.rs b/src/handlers/mod.rs new file mode 100644 index 0000000..6bff1a3 --- /dev/null +++ b/src/handlers/mod.rs @@ -0,0 +1,17 @@ +//! Per-route handlers: parse → authorize → resolve the node file → answer. + +pub mod files; + +#[cfg(test)] +mod tests; + +use crate::host_api::HostApi; +use crate::http::ApiResult; +use crate::router::{RequestParts, RouteId}; + +pub fn handle(host: &mut H, route: RouteId, parts: &RequestParts) -> ApiResult { + match route { + RouteId::ServerFile => files::file(host, parts), + RouteId::ServerFileInfo => files::info(host, parts), + } +} diff --git a/src/handlers/tests.rs b/src/handlers/tests.rs new file mode 100644 index 0000000..7b5e239 --- /dev/null +++ b/src/handlers/tests.rs @@ -0,0 +1,291 @@ +use std::collections::HashMap; + +use gameap_plugin_sdk::proto::gameap::User; +use gameap_plugin_sdk::proto::gameap::plugin as pb; +use serde_json::Value; + +use crate::authz::PANEL_ABILITY_FILES; +use crate::host_api::mock::MockHost; +use crate::router::dispatch; + +const ADMIN: u64 = 1; +const OWNER: u64 = 7; +const CS_MAP: &str = "/srv/gameap/servers/cs/cstrike/maps/de_dust2.bsp"; + +fn request(user_id: Option, method: &str, path: &str, query: &[(&str, &[&str])]) -> pb::HttpRequest { + let session = user_id.map(|id| pb::Session { + id: "sess".into(), + user: Some(User { + id, + login: format!("user{id}"), + ..Default::default() + }), + token: None, + }); + let query_params: HashMap = query + .iter() + .map(|(key, values)| { + ( + (*key).to_owned(), + pb::QueryParamValues { + values: values.iter().map(|v| (*v).to_owned()).collect(), + }, + ) + }) + .collect(); + pb::HttpRequest { + context: None, + method: method.into(), + path: path.into(), + headers: HashMap::new(), + path_params: HashMap::new(), + query_params, + body: Vec::new(), + session, + } +} + +fn get(user_id: Option, path: &str, file_path: &str) -> pb::HttpRequest { + request(user_id, "GET", path, &[("path", &[file_path])]) +} + +fn owner_host() -> MockHost { + let mut host = MockHost::standard(); + host.grant_server(OWNER, 3, PANEL_ABILITY_FILES); + host +} + +fn json(resp: &pb::HttpResponse) -> Value { + serde_json::from_slice(&resp.body).expect("json body") +} + +fn code(resp: &pb::HttpResponse) -> String { + json(resp)["code"].as_str().expect("code").to_owned() +} + +fn message(resp: &pb::HttpResponse) -> String { + json(resp)["message"].as_str().expect("message").to_owned() +} + +#[test] +fn file_route_without_session_is_401() { + let mut host = owner_host(); + let resp = dispatch(&mut host, &get(None, "/servers/3/file", "cstrike/maps/de_dust2.bsp")); + assert_eq!(resp.status_code, 401); + assert_eq!(code(&resp), "UNAUTHENTICATED"); + assert!(resp.file.is_none()); +} + +#[test] +fn file_route_without_ability_is_403() { + let mut host = MockHost::standard(); + let resp = dispatch(&mut host, &get(Some(OWNER), "/servers/3/file", "cstrike/maps/de_dust2.bsp")); + assert_eq!(resp.status_code, 403); + assert_eq!(code(&resp), "FORBIDDEN"); + assert!(resp.file.is_none()); +} + +#[test] +fn file_route_admin_bypasses_the_ability() { + let mut host = MockHost::standard(); + let resp = dispatch(&mut host, &get(Some(ADMIN), "/servers/3/file", "cstrike/maps/de_dust2.bsp")); + assert_eq!(resp.status_code, 200); + assert_eq!(resp.file.as_ref().map(|f| f.path.as_str()), Some(CS_MAP)); +} + +#[test] +fn file_route_unknown_server_is_404() { + let mut host = MockHost::standard(); + let resp = dispatch(&mut host, &get(Some(ADMIN), "/servers/404/file", "x.bin")); + assert_eq!(resp.status_code, 404); + assert_eq!(message(&resp), "server 404 not found"); +} + +#[test] +fn file_route_hides_server_existence_from_callers_without_access() { + let mut host = MockHost::standard(); + let resp = dispatch(&mut host, &get(Some(OWNER), "/servers/404/file", "x.bin")); + assert_eq!(resp.status_code, 403); +} + +#[test] +fn file_route_unknown_node_is_404() { + let mut host = owner_host(); + host.nodes.clear(); + let resp = dispatch(&mut host, &get(Some(OWNER), "/servers/3/file", "x.bin")); + assert_eq!(resp.status_code, 404); + assert_eq!(message(&resp), "node 1 not found"); +} + +#[test] +fn file_route_blocked_server_is_403_for_the_owner_and_open_for_an_admin() { + let mut host = owner_host(); + host.servers.get_mut(&3).expect("server 3").blocked = true; + + let resp = dispatch(&mut host, &get(Some(OWNER), "/servers/3/file", "x.bin")); + assert_eq!(resp.status_code, 403); + assert_eq!(message(&resp), "server is blocked"); + + let resp = dispatch(&mut host, &get(Some(ADMIN), "/servers/3/file", "x.bin")); + assert_eq!(resp.status_code, 200); +} + +#[test] +fn file_route_returns_the_file_reference() { + let mut host = owner_host(); + let resp = dispatch(&mut host, &get(Some(OWNER), "/servers/3/file", "cstrike/maps/de_dust2.bsp")); + assert_eq!(resp.status_code, 200); + assert!(resp.body.is_empty()); + assert_eq!(resp.headers.get("Content-Type").map(String::as_str), Some("application/octet-stream")); + assert_eq!(resp.headers.get("Cache-Control").map(String::as_str), Some("no-store")); + let file = resp.file.expect("file reference"); + assert_eq!(file.node_id, 1); + assert_eq!(file.path, CS_MAP); + assert_eq!(file.filename, "de_dust2.bsp"); +} + +#[test] +fn file_route_on_a_windows_node_keeps_the_panels_join() { + let mut host = MockHost::standard().with_windows_node(); + host.grant_server(OWNER, 9, PANEL_ABILITY_FILES); + let resp = dispatch(&mut host, &get(Some(OWNER), "/servers/9/file", "cstrike/maps/de_dust2.bsp")); + assert_eq!(resp.status_code, 200); + let file = resp.file.expect("file reference"); + assert_eq!(file.node_id, 7); + assert_eq!(file.path, r"C:\gameap/servers\cs2/cstrike/maps/de_dust2.bsp"); +} + +#[test] +fn file_route_strips_the_leading_slash_and_backslashes() { + let mut host = owner_host(); + for raw in ["/cstrike/maps/de_dust2.bsp", r"\cstrike\maps\de_dust2.bsp", "cstrike//maps/./de_dust2.bsp"] { + let resp = dispatch(&mut host, &get(Some(OWNER), "/servers/3/file", raw)); + assert_eq!(resp.status_code, 200, "{raw}"); + assert_eq!(resp.file.as_ref().map(|f| f.path.as_str()), Some(CS_MAP), "{raw}"); + } +} + +#[test] +fn file_route_rejects_traversal_nul_empty_root_and_directories() { + let mut host = owner_host(); + for raw in ["../etc/passwd", r"..\..\x", "a/../b", "a\0b", "", "/", ".", "cstrike/maps/"] { + let resp = dispatch(&mut host, &get(Some(OWNER), "/servers/3/file", raw)); + assert_eq!(resp.status_code, 400, "{raw:?}"); + assert_eq!(code(&resp), "INVALID_INPUT", "{raw:?}"); + assert!(resp.file.is_none(), "{raw:?}"); + } +} + +#[test] +fn file_route_rejoins_a_comma_split_path() { + let mut host = owner_host(); + let req = request(Some(OWNER), "GET", "/servers/3/file", &[("path", &["maps/a", "b.bsp"])]); + let resp = dispatch(&mut host, &req); + assert_eq!(resp.status_code, 200); + assert_eq!( + resp.file.as_ref().map(|f| f.path.as_str()), + Some("/srv/gameap/servers/cs/maps/a,b.bsp") + ); + assert_eq!(resp.file.as_ref().map(|f| f.filename.as_str()), Some("a,b.bsp")); +} + +#[test] +fn file_route_makes_no_daemon_call() { + let mut host = owner_host(); + dispatch(&mut host, &get(Some(OWNER), "/servers/3/file", "x.bin")); + assert!(host.file_info_calls.is_empty()); +} + +#[test] +fn file_route_with_an_unconfigured_server_dir_is_500() { + let mut host = owner_host(); + host.servers.get_mut(&3).expect("server 3").dir = " ".into(); + let resp = dispatch(&mut host, &get(Some(OWNER), "/servers/3/file", "x.bin")); + assert_eq!(resp.status_code, 500); + assert_eq!(code(&resp), "INTERNAL_ERROR"); +} + +#[test] +fn info_route_maps_size_and_mtime_and_echoes_the_relative_path() { + let mut host = owner_host(); + host.add_file(1, CS_MAP, 11_197_597, 1_725_000_000); + let resp = dispatch(&mut host, &get(Some(OWNER), "/servers/3/file/info", "/cstrike/maps/de_dust2.bsp")); + assert_eq!(resp.status_code, 200); + assert_eq!(resp.headers.get("Content-Type").map(String::as_str), Some("application/json")); + assert_eq!(resp.headers.get("Cache-Control").map(String::as_str), Some("no-store")); + assert_eq!( + json(&resp), + serde_json::json!({ "size": 11_197_597, "mtime": 1_725_000_000, "path": "cstrike/maps/de_dust2.bsp" }) + ); + assert_eq!(host.file_info_calls, vec![(1, CS_MAP.to_owned())]); +} + +#[test] +fn info_route_missing_file_is_404() { + let mut host = owner_host(); + let resp = dispatch(&mut host, &get(Some(OWNER), "/servers/3/file/info", "missing.bin")); + assert_eq!(resp.status_code, 404); + assert_eq!(message(&resp), "file not found"); +} + +#[test] +fn info_route_directory_is_400() { + let mut host = owner_host(); + host.add_dir(1, "/srv/gameap/servers/cs/cstrike"); + let resp = dispatch(&mut host, &get(Some(OWNER), "/servers/3/file/info", "cstrike")); + assert_eq!(resp.status_code, 400); + assert_eq!(message(&resp), "path is a directory"); +} + +#[test] +fn info_route_host_error_is_502_with_the_panels_message() { + let mut host = owner_host(); + host.file_info_error = Some("plugin permission files_read required".into()); + let resp = dispatch(&mut host, &get(Some(OWNER), "/servers/3/file/info", "x.bin")); + assert_eq!(resp.status_code, 502); + assert_eq!(code(&resp), "HOST_CALL_FAILED"); + assert_eq!(message(&resp), "plugin permission files_read required"); +} + +#[test] +fn info_route_requires_the_same_access_as_the_file_route() { + let mut host = MockHost::standard(); + assert_eq!(dispatch(&mut host, &get(None, "/servers/3/file/info", "x.bin")).status_code, 401); + assert_eq!(dispatch(&mut host, &get(Some(OWNER), "/servers/3/file/info", "x.bin")).status_code, 403); + assert!(host.file_info_calls.is_empty()); +} + +#[test] +fn authz_outage_is_502_not_a_denial() { + let mut host = owner_host(); + host.authz_down = true; + let resp = dispatch(&mut host, &get(Some(OWNER), "/servers/3/file", "x.bin")); + assert_eq!(resp.status_code, 502); + assert_eq!(code(&resp), "AUTHZ_UNAVAILABLE"); +} + +#[test] +fn server_lookup_failure_is_502() { + let mut host = owner_host(); + host.server_error = Some("servers unavailable".into()); + let resp = dispatch(&mut host, &get(Some(OWNER), "/servers/3/file", "x.bin")); + assert_eq!(resp.status_code, 502); + assert_eq!(code(&resp), "HOST_CALL_FAILED"); +} + +#[test] +fn invalid_server_id_is_400() { + let mut host = owner_host(); + let resp = dispatch(&mut host, &get(Some(OWNER), "/servers/abc/file", "x.bin")); + assert_eq!(resp.status_code, 400); + assert_eq!(message(&resp), "invalid serverId: must be a number"); +} + +#[test] +fn unknown_route_is_a_json_404() { + let mut host = owner_host(); + let resp = dispatch(&mut host, &get(Some(OWNER), "/servers/3/files", "x.bin")); + assert_eq!(resp.status_code, 404); + assert_eq!(code(&resp), "NOT_FOUND"); + assert_eq!(resp.headers.get("Cache-Control").map(String::as_str), Some("no-store")); +} diff --git a/src/host_api.rs b/src/host_api.rs new file mode 100644 index 0000000..8625f09 --- /dev/null +++ b/src/host_api.rs @@ -0,0 +1,450 @@ +//! Seam between the handlers and the SDK host functions. +//! +//! `gameap_plugin_sdk::host` is compiled only for wasm32, so the rest of the +//! crate depends on this narrow trait instead; native `cargo test` runs the +//! real router and handlers against [`mock::MockHost`]. + +use gameap_plugin_sdk::proto::gameap::EntityType; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum HostApiError { + /// ABI/transport failure of the host call itself. + Call(String), + /// The panel reported a failed operation (`error` set in the answer). + Op(String), +} + +impl HostApiError { + pub fn into_message(self) -> String { + match self { + Self::Call(m) | Self::Op(m) => m, + } + } +} + +pub type HostResult = Result; + +/// An RBAC entity: `gameap.EntityType` plus the entity id. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct Entity { + pub entity_type: i32, + pub entity_id: u64, +} + +impl Entity { + pub fn server(id: u64) -> Self { + Self { + entity_type: EntityType::Server as i32, + entity_id: id, + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ServerInfo { + pub id: u64, + pub node_id: u64, + pub name: String, + /// Directory of the game server as the panel stores it: relative to the + /// node work path on current installs, absolute on some older ones. + pub dir: String, + pub enabled: bool, + pub blocked: bool, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct NodeInfo { + pub id: u64, + pub name: String, + pub work_path: String, + pub os: String, + pub enabled: bool, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct FileInfo { + pub name: String, + pub size: u64, + /// Modification time, unix seconds. + pub mtime: u64, + pub is_dir: bool, +} + +pub trait HostApi { + fn get_server(&mut self, id: u64) -> HostResult>; + fn get_node(&mut self, id: u64) -> HostResult>; + /// `Ok(None)` when nothing exists at the path; `Err(Op)` for every other + /// refusal (permission, path policy, rate limit, daemon failure). + fn get_file_info(&mut self, node_id: u64, path: &str) -> HostResult>; + /// RBAC: the user holds ALL listed abilities globally. `Err(Op)` means the + /// check could not be performed and must NOT be read as a denial. + fn authz_can(&mut self, user_id: u64, abilities: &[&str]) -> HostResult; + /// RBAC: the user holds AT LEAST ONE of the abilities for the entity. + fn authz_can_any_for_entity( + &mut self, + user_id: u64, + entity: Entity, + abilities: &[&str], + ) -> HostResult; + /// The permissions the operator granted this plugin, read live from the + /// panel. An `Err` means the question could not be asked at all, which is + /// not the same answer as an empty set. + fn plugin_grants(&mut self) -> HostResult>; + fn log_info(&mut self, message: &str); + fn log_error(&mut self, message: &str); +} + +/// The words the panel and the daemons use for a path that does not exist. +/// `get_file_info` reports a missing file as `found = false` together with an +/// error text, so "not found" is only recognisable by the message. +/// +/// Only the wasm host calls it; natively it exists for its own tests. +#[cfg_attr(not(target_arch = "wasm32"), allow(dead_code))] +pub(crate) fn is_not_found_message(message: &str) -> bool { + let lowered = message.to_ascii_lowercase(); + [ + "file not found", + "no such file", + "does not exist", + "cannot find the", + ] + .iter() + .any(|needle| lowered.contains(needle)) +} + +pub struct WasmHost; + +#[cfg(target_arch = "wasm32")] +mod wasm { + use gameap_plugin_sdk::host; + use gameap_plugin_sdk::proto::gameap::plugin::sdk::{ + authz, host as hostpb, nodefs, nodes, servers, + }; + + use super::*; + + fn call_err(err: gameap_plugin_sdk::HostError) -> HostApiError { + HostApiError::Call(err.to_string()) + } + + fn can_decision(resp: authz::CanResponse) -> HostResult { + match resp.error { + Some(err) if !err.is_empty() => Err(HostApiError::Op(err)), + _ => Ok(resp.allowed), + } + } + + impl HostApi for WasmHost { + fn get_server(&mut self, id: u64) -> HostResult> { + let resp = + host::servers::get_server(&servers::GetServerRequest { id }).map_err(call_err)?; + Ok(resp.found.then_some(resp.server).flatten().map(|s| ServerInfo { + id: s.id, + node_id: s.ds_id, + name: s.name, + dir: s.dir, + enabled: s.enabled, + blocked: s.blocked, + })) + } + + fn get_node(&mut self, id: u64) -> HostResult> { + let resp = host::nodes::get_node(&nodes::GetNodeRequest { id }).map_err(call_err)?; + Ok(resp.found.then_some(resp.node).flatten().map(|n| NodeInfo { + id: n.id, + name: n.name, + work_path: n.work_path, + os: n.os, + enabled: n.enabled, + })) + } + + fn get_file_info(&mut self, node_id: u64, path: &str) -> HostResult> { + let resp = host::nodefs::get_file_info(&nodefs::GetFileInfoRequest { + node_id, + path: path.to_owned(), + }) + .map_err(call_err)?; + if let Some(err) = resp.error.filter(|e| !e.is_empty()) { + return if is_not_found_message(&err) { + Ok(None) + } else { + Err(HostApiError::Op(err)) + }; + } + if !resp.found { + return Ok(None); + } + Ok(resp.file.map(|f| FileInfo { + name: f.name, + size: f.size, + mtime: f.modification_time, + is_dir: f.r#type == nodefs::FileType::Dir as i32, + })) + } + + fn authz_can(&mut self, user_id: u64, abilities: &[&str]) -> HostResult { + let resp = host::authz::can(&authz::CanRequest { + user_id, + abilities: abilities.iter().map(|s| (*s).to_owned()).collect(), + }) + .map_err(call_err)?; + can_decision(resp) + } + + fn authz_can_any_for_entity( + &mut self, + user_id: u64, + entity: Entity, + abilities: &[&str], + ) -> HostResult { + let resp = host::authz::can_any_for_entity(&authz::CanForEntityRequest { + user_id, + entity_type: entity.entity_type, + entity_id: entity.entity_id, + abilities: abilities.iter().map(|s| (*s).to_owned()).collect(), + }) + .map_err(call_err)?; + can_decision(resp) + } + + fn plugin_grants(&mut self) -> HostResult> { + // Only `get_grants` is imported from `gameap-host` on purpose: an + // import the panel does not export makes the whole module fail to + // load, and `get_config` / `report_status` are not on every panel + // that has the module. + let resp = host::host::get_grants(&hostpb::GetGrantsRequest {}).map_err(call_err)?; + Ok(resp.permissions) + } + + fn log_info(&mut self, message: &str) { + host::log::info(message); + } + + fn log_error(&mut self, message: &str) { + host::log::error(message); + } + } +} + +#[cfg(test)] +pub mod mock { + use std::collections::{BTreeMap, BTreeSet}; + + use super::*; + + #[derive(Default)] + pub struct MockHost { + pub servers: BTreeMap, + pub nodes: BTreeMap, + /// (node_id, absolute node path) → what `get_file_info` answers. + pub files: BTreeMap<(u64, String), FileInfo>, + /// Users holding the panel admin ability. + pub admins: BTreeSet, + /// (user_id, entity_type, entity_id, ability) grants. + pub grants: BTreeSet<(u64, i32, u64, String)>, + /// When set, every authz call fails with `Op`. + pub authz_down: bool, + pub server_error: Option, + pub node_error: Option, + pub file_info_error: Option, + /// What `plugin_grants` answers; empty means "nothing granted", which + /// is a different thing from `plugin_grants_error` ("cannot tell"). + pub plugin_grants: Vec, + pub plugin_grants_error: Option, + pub logs: Vec, + /// (node_id, path) per `get_file_info` call, in call order. + pub file_info_calls: Vec<(u64, String)>, + pub authz_calls: usize, + } + + impl MockHost { + /// Node 1 ("node-1", linux, /srv/gameap) with server 3 ("cs", dir + /// `servers/cs`, relative to the work path as the panel stores it); + /// user 1 is an administrator, user 7 holds nothing. + pub fn standard() -> Self { + let mut host = Self::default(); + host.nodes.insert( + 1, + NodeInfo { + id: 1, + name: "node-1".into(), + work_path: "/srv/gameap".into(), + os: "linux".into(), + enabled: true, + }, + ); + host.servers.insert( + 3, + ServerInfo { + id: 3, + node_id: 1, + name: "cs".into(), + dir: "servers/cs".into(), + enabled: true, + blocked: false, + }, + ); + host.admins.insert(1); + host + } + + /// Adds node 7 ("win-1", windows, C:\gameap) with server 9 ("cs2", + /// dir `servers\cs2`). + pub fn with_windows_node(mut self) -> Self { + self.nodes.insert( + 7, + NodeInfo { + id: 7, + name: "win-1".into(), + work_path: r"C:\gameap".into(), + os: "windows".into(), + enabled: true, + }, + ); + self.servers.insert( + 9, + ServerInfo { + id: 9, + node_id: 7, + name: "cs2".into(), + dir: r"servers\cs2".into(), + enabled: true, + blocked: false, + }, + ); + self + } + + pub fn grant_server(&mut self, user_id: u64, server_id: u64, ability: &str) { + self.grants.insert(( + user_id, + EntityType::Server as i32, + server_id, + ability.to_owned(), + )); + } + + pub fn add_file(&mut self, node_id: u64, path: &str, size: u64, mtime: u64) { + let name = path.rsplit('/').next().unwrap_or(path).to_owned(); + self.files.insert( + (node_id, path.to_owned()), + FileInfo { + name, + size, + mtime, + is_dir: false, + }, + ); + } + + pub fn add_dir(&mut self, node_id: u64, path: &str) { + let name = path.rsplit('/').next().unwrap_or(path).to_owned(); + self.files.insert( + (node_id, path.to_owned()), + FileInfo { + name, + size: 4096, + mtime: 1_700_000_000, + is_dir: true, + }, + ); + } + } + + impl HostApi for MockHost { + fn get_server(&mut self, id: u64) -> HostResult> { + if let Some(err) = &self.server_error { + return Err(HostApiError::Op(err.clone())); + } + Ok(self.servers.get(&id).cloned()) + } + + fn get_node(&mut self, id: u64) -> HostResult> { + if let Some(err) = &self.node_error { + return Err(HostApiError::Op(err.clone())); + } + Ok(self.nodes.get(&id).cloned()) + } + + fn get_file_info(&mut self, node_id: u64, path: &str) -> HostResult> { + self.file_info_calls.push((node_id, path.to_owned())); + if let Some(err) = &self.file_info_error { + return Err(HostApiError::Op(err.clone())); + } + Ok(self.files.get(&(node_id, path.to_owned())).cloned()) + } + + fn authz_can(&mut self, user_id: u64, _abilities: &[&str]) -> HostResult { + self.authz_calls += 1; + if self.authz_down { + return Err(HostApiError::Op("rbac unavailable".into())); + } + Ok(self.admins.contains(&user_id)) + } + + fn authz_can_any_for_entity( + &mut self, + user_id: u64, + entity: Entity, + abilities: &[&str], + ) -> HostResult { + self.authz_calls += 1; + if self.authz_down { + return Err(HostApiError::Op("rbac unavailable".into())); + } + Ok(abilities.iter().any(|ability| { + self.grants.contains(&( + user_id, + entity.entity_type, + entity.entity_id, + (*ability).to_owned(), + )) + })) + } + + fn plugin_grants(&mut self) -> HostResult> { + match &self.plugin_grants_error { + Some(err) => Err(HostApiError::Call(err.clone())), + None => Ok(self.plugin_grants.clone()), + } + } + + fn log_info(&mut self, message: &str) { + self.logs.push(format!("INFO {message}")); + } + + fn log_error(&mut self, message: &str) { + self.logs.push(format!("ERROR {message}")); + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn not_found_messages_are_recognised() { + for message in [ + "file not found", + "stat: no such file or directory", + "The system cannot find the file specified.", + "path does not exist", + ] { + assert!(is_not_found_message(message), "{message}"); + } + } + + #[test] + fn other_refusals_are_not_read_as_not_found() { + for message in [ + "permission denied", + "path policy: outside the server directories", + "plugin permission files_read required", + "rate limited: gameap-nodefs allows 50 calls/s", + ] { + assert!(!is_not_found_message(message), "{message}"); + } + } +} diff --git a/src/http.rs b/src/http.rs new file mode 100644 index 0000000..e9f52bb --- /dev/null +++ b/src/http.rs @@ -0,0 +1,242 @@ +//! HTTP response helpers and the error model. +//! +//! `handle_http_request` must never return `Err` for domain failures (the host +//! turns that into a plain-text 500), so every error becomes a JSON response +//! here, in the GameAP plugin convention `{"code": "...", "message": "..."}`. + +use std::collections::HashMap; + +use gameap_plugin_sdk::proto::gameap::plugin as pb; +use serde::Serialize; + +use crate::host_api::HostApiError; + +pub type ApiResult = Result; + +pub const ERR_INVALID_INPUT: &str = "INVALID_INPUT"; +pub const ERR_UNAUTHENTICATED: &str = "UNAUTHENTICATED"; +pub const ERR_FORBIDDEN: &str = "FORBIDDEN"; +pub const ERR_NOT_FOUND: &str = "NOT_FOUND"; +pub const ERR_HOST_CALL_FAILED: &str = "HOST_CALL_FAILED"; +pub const ERR_AUTHZ_UNAVAILABLE: &str = "AUTHZ_UNAVAILABLE"; +pub const ERR_INTERNAL: &str = "INTERNAL_ERROR"; + +const HEADER_CONTENT_TYPE: &str = "Content-Type"; +const HEADER_CACHE_CONTROL: &str = "Cache-Control"; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ApiError { + pub status: i32, + pub code: &'static str, + pub message: String, +} + +impl ApiError { + pub fn new(status: i32, code: &'static str, message: impl Into) -> Self { + Self { + status, + code, + message: message.into(), + } + } + + pub fn bad_request(message: impl Into) -> Self { + Self::new(400, ERR_INVALID_INPUT, message) + } + + pub fn unauthenticated(message: impl Into) -> Self { + Self::new(401, ERR_UNAUTHENTICATED, message) + } + + pub fn forbidden(message: impl Into) -> Self { + Self::new(403, ERR_FORBIDDEN, message) + } + + pub fn not_found(message: impl Into) -> Self { + Self::new(404, ERR_NOT_FOUND, message) + } + + pub fn internal(message: impl Into) -> Self { + Self::new(500, ERR_INTERNAL, message) + } + + pub fn host_failed(message: impl Into) -> Self { + Self::new(502, ERR_HOST_CALL_FAILED, message) + } + + pub fn into_response(self) -> pb::HttpResponse { + #[derive(Serialize)] + struct ErrorBody { + code: &'static str, + message: String, + } + json_response( + self.status, + &ErrorBody { + code: self.code, + message: self.message, + }, + ) + } +} + +impl From for ApiError { + fn from(err: HostApiError) -> Self { + Self::host_failed(err.into_message()) + } +} + +pub fn json_response(status: i32, value: &T) -> pb::HttpResponse { + match serde_json::to_vec(value) { + Ok(body) => pb::HttpResponse { + status_code: status, + headers: json_headers(), + body, + file: None, + }, + Err(_) => pb::HttpResponse { + status_code: 500, + headers: json_headers(), + body: br#"{"code":"INTERNAL_ERROR","message":"failed to serialize response"}"#.to_vec(), + file: None, + }, + } +} + +/// A node file the panel streams to the client on the plugin's behalf. The +/// bytes never enter guest memory and the transfer runs after the guest call +/// returns, so neither the call deadline nor the plugin's call gate bounds the +/// size. The panel stats the file itself (404 missing, 400 directory), checks +/// the `files_read` grant and the session, owns `Content-Disposition` and +/// serves a single `Range` (206/416), which is what lets the frontend read a +/// file of any size one window at a time. +pub fn file_response( + node_id: u64, + path: &str, + filename: &str, + content_type: &str, +) -> pb::HttpResponse { + pb::HttpResponse { + status_code: 200, + headers: HashMap::from([(HEADER_CONTENT_TYPE.to_owned(), content_type.to_owned())]), + body: Vec::new(), + file: Some(pb::FileRef { + node_id, + path: path.to_owned(), + filename: filename.to_owned(), + }), + } +} + +/// Both routes answer per file and per session; nothing here is cacheable. +pub fn with_no_store(mut resp: pb::HttpResponse) -> pb::HttpResponse { + resp.headers + .entry(HEADER_CACHE_CONTROL.to_owned()) + .or_insert_with(|| "no-store".to_owned()); + resp +} + +fn json_headers() -> HashMap { + HashMap::from([(HEADER_CONTENT_TYPE.to_owned(), "application/json".to_owned())]) +} + +pub fn parse_u64_param(params: &HashMap, key: &str) -> Result { + let val = params + .get(key) + .ok_or_else(|| ApiError::bad_request(format!("missing parameter: {key}")))?; + val.parse::() + .map_err(|_| ApiError::bad_request(format!("invalid {key}: must be a number"))) +} + +/// A query parameter as one string. The panel splits every query value on +/// commas before it reaches the guest, so a file name with a comma in it +/// arrives as several values; joining them back is the only way to get the +/// name the browser sent. +pub fn query_param_joined(query: &HashMap, key: &str) -> String { + query + .get(key) + .map(|v| v.values.join(",")) + .unwrap_or_default() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn error_body_shape() { + let resp = ApiError::not_found("route not found").into_response(); + assert_eq!(resp.status_code, 404); + assert_eq!( + resp.headers.get("Content-Type").map(String::as_str), + Some("application/json") + ); + assert_eq!( + String::from_utf8(resp.body).unwrap(), + r#"{"code":"NOT_FOUND","message":"route not found"}"# + ); + } + + #[test] + fn host_errors_become_502() { + let err: ApiError = HostApiError::Op("stat failed: daemon not connected".into()).into(); + assert_eq!(err.status, 502); + assert_eq!(err.code, ERR_HOST_CALL_FAILED); + assert_eq!(err.message, "stat failed: daemon not connected"); + } + + #[test] + fn file_response_names_the_node_file_and_sends_no_body() { + let resp = file_response(1, "/srv/gameap/servers/cs/maps/de_dust2.bsp", "de_dust2.bsp", "application/octet-stream"); + assert_eq!(resp.status_code, 200); + assert!(resp.body.is_empty()); + assert_eq!( + resp.headers.get("Content-Type").map(String::as_str), + Some("application/octet-stream") + ); + let file = resp.file.expect("file reference"); + assert_eq!(file.node_id, 1); + assert_eq!(file.path, "/srv/gameap/servers/cs/maps/de_dust2.bsp"); + assert_eq!(file.filename, "de_dust2.bsp"); + } + + #[test] + fn no_store_is_added_once() { + let resp = with_no_store(json_response(200, &serde_json::json!({}))); + assert_eq!(resp.headers.get("Cache-Control").map(String::as_str), Some("no-store")); + + let mut custom = json_response(200, &serde_json::json!({})); + custom.headers.insert("Cache-Control".into(), "private".into()); + let resp = with_no_store(custom); + assert_eq!(resp.headers.get("Cache-Control").map(String::as_str), Some("private")); + } + + #[test] + fn query_param_joined_restores_commas_the_panel_split() { + let mut query = HashMap::new(); + query.insert( + "path".to_owned(), + pb::QueryParamValues { + values: vec!["maps/a".into(), "b.bsp".into()], + }, + ); + assert_eq!(query_param_joined(&query, "path"), "maps/a,b.bsp"); + assert_eq!(query_param_joined(&query, "missing"), ""); + } + + #[test] + fn parse_u64_param_messages() { + let mut params = HashMap::new(); + assert_eq!( + parse_u64_param(¶ms, "serverId").unwrap_err().message, + "missing parameter: serverId" + ); + params.insert("serverId".into(), "abc".into()); + assert_eq!( + parse_u64_param(¶ms, "serverId").unwrap_err().message, + "invalid serverId: must be a number" + ); + params.insert("serverId".into(), "42".into()); + assert_eq!(parse_u64_param(¶ms, "serverId").unwrap(), 42); + } +} diff --git a/src/lib.rs b/src/lib.rs index db2da07..e0c0a0a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,33 +1,124 @@ //! GameAP plugin: view and edit files in hexadecimal format. //! //! A `wasm32-wasip1` reactor module. The panel loads it and calls the exported -//! `plugin_service_*` functions. All hex-editing logic lives in the embedded Vue -//! frontend; the backend only reports metadata and serves the bundle. +//! `plugin_service_*` functions. The hex editing itself happens in the embedded +//! Vue frontend; this side serves the bundle and the two routes the frontend +//! reads server files through: a stat (`/servers/{id}/file/info`) and a file +//! response (`/servers/{id}/file`) the panel streams to the browser with +//! `Range` support, so a file of any size is reachable one window at a time +//! without ever entering wasm memory. Saving goes through the panel's own +//! file manager; nothing here writes. -#![cfg(target_arch = "wasm32")] +#![cfg_attr(test, allow(clippy::expect_used, clippy::unwrap_used))] + +pub mod authz; +pub mod handlers; +pub mod host_api; +pub mod http; +pub mod paths; +pub mod router; use gameap_plugin_sdk::proto::gameap::plugin as pb; use gameap_plugin_sdk::{Plugin, PluginError, register_plugin}; +use crate::host_api::HostApi; + +/// Round-trip stable under the panel's base32 plugin id normalization, so +/// `/api/plugins/hexeditor4jm2/...` resolves literally. It is also the id the +/// plugin store and existing installs know - never change it. +pub const PLUGIN_ID: &str = "hexeditor4jm2"; + +/// `files_read` alone: the file route hands the panel a reference it streams +/// itself and the info route is a stat, both reads. `files` would include it +/// but disagree with the upload dry-run, which derives the used permissions +/// from the module's imports. +pub const REQUIRED_PERMISSIONS: [&str; 1] = ["files_read"]; + +/// Says once, at load, which declared permissions the operator did not grant. +/// +/// A diagnosis, not a gate: whether a missing grant actually denies anything +/// depends on the panel's `PLUGINS_PERMISSIONS_ENFORCE`, which the host does +/// not expose, and the call that really is denied carries the panel's own +/// message. A host that cannot answer at all is not evidence of anything. +fn report_missing_grants(host: &mut H) { + let Ok(granted) = host.plugin_grants() else { + return; + }; + let missing: Vec<&str> = REQUIRED_PERMISSIONS + .iter() + .copied() + .filter(|required| !granted.iter().any(|g| g == required)) + .collect(); + + if !missing.is_empty() { + host.log_error(&format!( + "hex editor: the panel has not granted {}; with permission enforcement on, \ + opening files will be refused until an administrator adds it", + missing.join(", "), + )); + } +} + const FRONTEND_JS: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/plugin.js")); const FRONTEND_CSS: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/plugin.css")); -#[derive(Default)] -struct HexEditorPlugin; +pub struct HexEditorPlugin { + host: H, +} -impl Plugin for HexEditorPlugin { +impl HexEditorPlugin { + pub fn new(host: H) -> Self { + Self { host } + } +} + +impl Plugin for HexEditorPlugin { fn get_info(&mut self, _req: pb::GetInfoRequest) -> Result { Ok(pb::PluginInfo { - id: "hexeditor4jm2".into(), + id: PLUGIN_ID.into(), name: "Hex Editor".into(), version: env!("CARGO_PKG_VERSION").into(), description: "View and edit files in hexadecimal format".into(), author: "GameAP".into(), + license: "MIT".into(), api_version: "1".into(), + required_permissions: REQUIRED_PERMISSIONS + .iter() + .map(|p| (*p).to_string()) + .collect(), ..Default::default() }) } + fn initialize( + &mut self, + _req: pb::InitializeRequest, + ) -> Result { + report_missing_grants(&mut self.host); + self.host.log_info("plugin initialized"); + Ok(pb::InitializeResponse { + result: Some(gameap_plugin_sdk::ok_result()), + }) + } + + fn get_http_routes( + &mut self, + _req: pb::GetHttpRoutesRequest, + ) -> Result { + Ok(pb::GetHttpRoutesResponse { + routes: router::http_routes(), + }) + } + + fn handle_http_request( + &mut self, + req: pb::HttpRequest, + ) -> Result { + // Total dispatch: every failure becomes a JSON error response. An Err + // here would surface as a plain-text host 500. + Ok(router::dispatch(&mut self.host, &req)) + } + fn get_frontend_bundle( &mut self, _req: pb::GetFrontendBundleRequest, @@ -41,4 +132,107 @@ impl Plugin for HexEditorPlugin { } } -register_plugin!(HexEditorPlugin); +register_plugin!( + HexEditorPlugin, + HexEditorPlugin::new(host_api::WasmHost) +); + +#[cfg(test)] +mod tests { + use super::*; + use crate::host_api::mock::MockHost; + + /// A host that answers exactly these grants. + fn granting(grants: &[&str]) -> MockHost { + MockHost { + plugin_grants: grants.iter().map(|g| (*g).to_string()).collect(), + ..Default::default() + } + } + + fn manifest(host: MockHost) -> pb::PluginInfo { + HexEditorPlugin::new(host) + .get_info(pb::GetInfoRequest {}) + .expect("manifest") + } + + #[test] + fn the_manifest_declares_exactly_files_read() { + let info = manifest(MockHost::default()); + assert_eq!(info.required_permissions, vec!["files_read".to_string()]); + } + + #[test] + fn the_manifest_id_and_api_version() { + let info = manifest(MockHost::default()); + assert_eq!(info.id, "hexeditor4jm2"); + assert_eq!(info.api_version, "1"); + assert_eq!(info.version, env!("CARGO_PKG_VERSION")); + } + + #[test] + fn the_routes_are_declared() { + let routes = HexEditorPlugin::new(MockHost::default()) + .get_http_routes(pb::GetHttpRoutesRequest {}) + .expect("routes") + .routes; + let paths: Vec<&str> = routes.iter().map(|r| r.path.as_str()).collect(); + assert_eq!(paths, vec!["/servers/{serverId}/file/info", "/servers/{serverId}/file"]); + } + + #[test] + fn a_missing_grant_is_named_once_at_load() { + let mut host = granting(&["listen_events"]); + + report_missing_grants(&mut host); + + assert_eq!(host.logs.len(), 1, "{:?}", host.logs); + assert!(host.logs[0].starts_with("ERROR"), "{:?}", host.logs); + assert!(host.logs[0].contains("files_read"), "{:?}", host.logs); + } + + #[test] + fn a_complete_grant_set_says_nothing() { + let mut host = granting(&["files_read"]); + + report_missing_grants(&mut host); + + assert!(host.logs.is_empty(), "{:?}", host.logs); + } + + #[test] + fn the_wider_files_grant_covers_files_read() { + // The panel answers the grants it stores, and an operator who granted + // `files` has granted every read; the diagnosis must not contradict it. + let mut host = granting(&["files", "files_read"]); + + report_missing_grants(&mut host); + + assert!(host.logs.is_empty(), "{:?}", host.logs); + } + + #[test] + fn a_host_that_cannot_answer_is_not_evidence_of_a_missing_grant() { + let mut host = MockHost { + plugin_grants_error: Some("unknown import".into()), + ..Default::default() + }; + + report_missing_grants(&mut host); + + assert!(host.logs.is_empty(), "{:?}", host.logs); + } + + #[test] + fn handle_http_request_never_errs() { + let mut plugin = HexEditorPlugin::new(MockHost::default()); + let resp = plugin + .handle_http_request(pb::HttpRequest { + method: "DELETE".into(), + path: "/nope".into(), + ..Default::default() + }) + .expect("a response, never an error"); + assert_eq!(resp.status_code, 404); + } +} diff --git a/src/paths.rs b/src/paths.rs new file mode 100644 index 0000000..580fdfc --- /dev/null +++ b/src/paths.rs @@ -0,0 +1,177 @@ +//! File-manager paths and their node counterparts. +//! +//! The frontend names a file the way the panel's file manager does: relative to +//! the server directory, with `/` (or `\` from a Windows listing) as the +//! separator and an optional leading slash that means the server root. The +//! panel turns such a path into a node path with Go's +//! `filepath.Join(node.WorkPath, server.Dir, path)` on a Linux host; this +//! module reproduces both the validation (`filemanagerpath.ValidatePath`) and +//! the join, so the plugin asks the node for exactly the file the panel's own +//! endpoints would. + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum PathError { + NullByte, + Traversal, + NotAFile, + Root, +} + +impl PathError { + pub fn message(self) -> &'static str { + match self { + Self::NullByte => "path contains a null byte", + Self::Traversal => "path contains invalid directory traversal", + Self::NotAFile => "path must name a file, not a directory", + Self::Root => "path names the server root; a file is required", + } + } +} + +/// Validates a file-manager path and normalizes it to `a/b/c`: no leading +/// slash, `/` separators, no empty or `.` segments. Any `..` segment is refused +/// (a literal `a..b` is fine), as are NUL bytes, the root itself and a path +/// that ends in a separator. Whitespace is kept: file names may carry it and +/// the panel does not trim either. +pub fn validate_relative(raw: &str) -> Result { + if raw.contains('\0') { + return Err(PathError::NullByte); + } + let unified = raw.replace('\\', "/"); + let parts: Vec<&str> = unified.split('/').collect(); + if parts.contains(&"..") { + return Err(PathError::Traversal); + } + let segments: Vec<&str> = parts + .iter() + .copied() + .filter(|segment| !segment.is_empty() && *segment != ".") + .collect(); + if segments.is_empty() { + return Err(PathError::Root); + } + let last = parts.last().copied().unwrap_or(""); + if last.is_empty() || last == "." { + return Err(PathError::NotAFile); + } + Ok(segments.join("/")) +} + +/// The node path of a server file, joined as the panel joins it: the work +/// path, the server directory and the relative path glued with `/` and cleaned +/// of empty and `.` segments. Nothing else is interpreted - a backslash inside +/// `work_path` or `server_dir` is an ordinary byte to Go's `filepath.Join` on +/// the Linux panel host and stays one here, and a `..` in either would be +/// refused by the panel's own path check rather than resolved. +pub fn join_node_path(work_path: &str, server_dir: &str, rel: &str) -> String { + let joined = [work_path, server_dir, rel] + .iter() + .filter(|part| !part.is_empty()) + .copied() + .collect::>() + .join("/"); + clean_slashes(&joined) +} + +fn clean_slashes(path: &str) -> String { + let body = path + .split('/') + .filter(|segment| !segment.is_empty() && *segment != ".") + .collect::>() + .join("/"); + if path.starts_with('/') { + format!("/{body}") + } else { + body + } +} + +/// The last segment of a validated relative path. +pub fn file_name(rel: &str) -> &str { + rel.rsplit('/').next().unwrap_or(rel) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn validation_table() { + let cases: &[(&str, Result<&str, PathError>)] = &[ + ("cstrike/maps/de_dust2.bsp", Ok("cstrike/maps/de_dust2.bsp")), + ("/cstrike/x", Ok("cstrike/x")), + (r"\cstrike\x", Ok("cstrike/x")), + ("cstrike/./x", Ok("cstrike/x")), + ("cstrike//x", Ok("cstrike/x")), + ("a..b/c", Ok("a..b/c")), + ("maps/карта de_dust2.bsp", Ok("maps/карта de_dust2.bsp")), + (" spaced .bin", Ok(" spaced .bin")), + ("", Err(PathError::Root)), + ("/", Err(PathError::Root)), + (".", Err(PathError::Root)), + ("./", Err(PathError::Root)), + ("//", Err(PathError::Root)), + ("..", Err(PathError::Traversal)), + ("../x", Err(PathError::Traversal)), + ("a/../b", Err(PathError::Traversal)), + (r"a\..\b", Err(PathError::Traversal)), + ("configs/..", Err(PathError::Traversal)), + ("a\0b", Err(PathError::NullByte)), + ("cstrike/maps/", Err(PathError::NotAFile)), + ("cstrike/maps/.", Err(PathError::NotAFile)), + ]; + for (raw, want) in cases { + let got = validate_relative(raw); + let got: Result<&str, PathError> = got.as_deref().map_err(|err| *err); + assert_eq!(got, *want, "{raw:?}"); + } + } + + #[test] + fn join_table() { + let cases: &[(&str, &str, &str, &str)] = &[ + ( + "/srv/gameap", + "servers/cs", + "cstrike/maps/de_dust2.bsp", + "/srv/gameap/servers/cs/cstrike/maps/de_dust2.bsp", + ), + ( + "/srv/gameap/", + "servers/cs/", + "cstrike/maps/de_dust2.bsp", + "/srv/gameap/servers/cs/cstrike/maps/de_dust2.bsp", + ), + ( + r"C:\gameap", + r"servers\cs2", + "cstrike/maps/de_dust2.bsp", + r"C:\gameap/servers\cs2/cstrike/maps/de_dust2.bsp", + ), + ("/srv/gameap", "/servers/cs", "x", "/srv/gameap/servers/cs/x"), + ( + "/srv/gameap", + "/srv/gameap/servers/cs", + "x", + "/srv/gameap/srv/gameap/servers/cs/x", + ), + ( + "/srv/gameap", + "servers/cs", + "maps/карта de_dust2.bsp", + "/srv/gameap/servers/cs/maps/карта de_dust2.bsp", + ), + ("", "servers/cs", "x", "servers/cs/x"), + ("/srv/gameap", "./servers/cs", "x", "/srv/gameap/servers/cs/x"), + ]; + for (work, dir, rel, want) in cases { + assert_eq!(join_node_path(work, dir, rel), *want, "{work} + {dir} + {rel}"); + } + } + + #[test] + fn file_name_is_the_last_segment() { + assert_eq!(file_name("cstrike/maps/de_dust2.bsp"), "de_dust2.bsp"); + assert_eq!(file_name("a b.bin"), "a b.bin"); + } +} diff --git a/src/router.rs b/src/router.rs new file mode 100644 index 0000000..b7368fc --- /dev/null +++ b/src/router.rs @@ -0,0 +1,156 @@ +//! Route table and dispatch. The matcher mirrors the host's `matchPath` +//! (segment-wise comparison, `{name}` captures, first match wins), so the +//! declared table stays the single source of truth for what the panel routes +//! to this plugin. + +use std::collections::HashMap; + +use gameap_plugin_sdk::proto::gameap::plugin as pb; + +use crate::authz::SessionInfo; +use crate::handlers; +use crate::host_api::HostApi; +use crate::http::{ApiError, ApiResult, with_no_store}; + +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub enum RouteId { + ServerFileInfo, + ServerFile, +} + +pub struct RouteDef { + pub id: RouteId, + pub method: &'static str, + pub pattern: &'static str, + pub description: &'static str, +} + +pub const ROUTES: &[RouteDef] = &[ + RouteDef { + id: RouteId::ServerFileInfo, + method: "GET", + pattern: "/servers/{serverId}/file/info", + description: "Size and modification time of one file of a game server (?path=)", + }, + RouteDef { + id: RouteId::ServerFile, + method: "GET", + pattern: "/servers/{serverId}/file", + description: "One file of a game server, streamed by the panel (?path=; Range honoured)", + }, +]; + +pub fn http_routes() -> Vec { + ROUTES + .iter() + .map(|route| pb::HttpRoute { + path: route.pattern.into(), + methods: vec![route.method.into()], + requires_auth: true, + admin_only: false, + description: route.description.into(), + }) + .collect() +} + +pub fn match_route(method: &str, path: &str) -> Option<(RouteId, HashMap)> { + ROUTES.iter().find_map(|route| { + if !route.method.eq_ignore_ascii_case(method) { + return None; + } + match_pattern(route.pattern, path).map(|params| (route.id, params)) + }) +} + +fn match_pattern(pattern: &str, path: &str) -> Option> { + let pattern_segments: Vec<&str> = pattern.trim_matches('/').split('/').collect(); + let path_segments: Vec<&str> = path.trim_matches('/').split('/').collect(); + if pattern_segments.len() != path_segments.len() { + return None; + } + let mut params = HashMap::new(); + for (pat, seg) in pattern_segments.iter().zip(path_segments.iter()) { + if let Some(name) = pat.strip_prefix('{').and_then(|p| p.strip_suffix('}')) { + params.insert(name.to_string(), (*seg).to_string()); + } else if pat != seg { + return None; + } + } + Some(params) +} + +/// Everything a handler needs from the incoming request. +pub struct RequestParts<'a> { + pub params: HashMap, + pub query: &'a HashMap, + pub session: SessionInfo, +} + +pub fn dispatch(host: &mut H, req: &pb::HttpRequest) -> pb::HttpResponse { + let Some((route, params)) = match_route(&req.method, &req.path) else { + return with_no_store(ApiError::not_found("route not found").into_response()); + }; + let parts = RequestParts { + params, + query: &req.query_params, + session: SessionInfo::from_request(req), + }; + let result: ApiResult = handlers::handle(host, route, &parts); + with_no_store(result.unwrap_or_else(ApiError::into_response)) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn matches_both_routes_and_captures_server_id() { + let (id, params) = match_route("GET", "/servers/3/file").expect("matches"); + assert_eq!(id, RouteId::ServerFile); + assert_eq!(params.get("serverId").map(String::as_str), Some("3")); + + let (id, params) = match_route("get", "/servers/12/file/info").expect("matches"); + assert_eq!(id, RouteId::ServerFileInfo); + assert_eq!(params.get("serverId").map(String::as_str), Some("12")); + } + + #[test] + fn trailing_slash_matches_like_the_panel() { + assert_eq!( + match_route("GET", "/servers/3/file/").map(|(id, _)| id), + Some(RouteId::ServerFile) + ); + } + + #[test] + fn rejects_extra_segments_unknown_paths_and_methods() { + assert!(match_route("GET", "/servers/3/file/info/extra").is_none()); + assert!(match_route("GET", "/servers/3/files").is_none()); + assert!(match_route("GET", "/servers/3").is_none()); + assert!(match_route("POST", "/servers/3/file").is_none()); + assert!(match_route("PUT", "/servers/3/file/info").is_none()); + assert!(match_route("GET", "/").is_none()); + assert!(match_route("GET", "").is_none()); + } + + #[test] + fn declared_routes_require_auth_and_are_not_admin_only() { + let routes = http_routes(); + assert_eq!(routes.len(), 2); + for route in &routes { + assert!(route.requires_auth, "{}", route.path); + assert!(!route.admin_only, "{}", route.path); + assert_eq!(route.methods, vec!["GET".to_string()], "{}", route.path); + } + } + + #[test] + fn route_ids_are_unique() { + for (i, a) in ROUTES.iter().enumerate() { + for b in &ROUTES[i + 1..] { + assert_ne!(a.id, b.id); + assert_ne!((a.method, a.pattern), (b.method, b.pattern)); + } + } + } +}