From 0492c3b10d6f32ef490d4528f6ac677f7cd8dea2 Mon Sep 17 00:00:00 2001 From: Hermet Date: Mon, 31 Aug 2026 13:57:25 -0400 Subject: [PATCH 1/5] refactor: replace Electron/React shell with vanilla TS web viewer Drops Electron 42, React 19 and the @moq/* transport stack. The client is now a plain Vite + TypeScript web app that runs in any browser with WebTransport and WebCodecs, matching the agent's MoQ-less raw WebTransport protocol. - src/transport.ts: one bidirectional stream for newline-delimited JSON control, server-initiated unidirectional streams for H.264 Annex B - src/decoder.ts: WebCodecs VideoDecoder fed from Annex B, SPS/PPS to avcC, rendered to canvas - src/ui/: multi-connection sidebar, stream panel, stats and debug overlays, replacing the single App.tsx - devDependencies reduced to typescript + vite; no runtime dependencies --- README.md | 34 +- forge.config.js | 31 - index.html | 18 +- package-lock.json | 4006 ++------------------ package.json | 30 +- scripts/build-main.mjs | 12 - src/decoder.ts | 184 + src/input.ts | 143 + src/main.ts | 77 + src/main/main.ts | 40 - src/main/preload.ts | 5 - src/renderer/App.tsx | 136 - src/renderer/components/AddConnection.tsx | 78 - src/renderer/components/ConnectionList.tsx | 54 - src/renderer/components/DebugOverlay.tsx | 55 - src/renderer/components/StreamPanel.tsx | 148 - src/renderer/lib/debug.ts | 25 - src/renderer/lib/moq-integration.ts | 98 - src/renderer/lib/types.ts | 45 - src/renderer/lib/webtransport-client.ts | 230 -- src/renderer/main.tsx | 9 - src/renderer/vite-env.d.ts | 9 - src/{renderer/App.css => style.css} | 272 +- src/transport.ts | 258 ++ src/types.ts | 81 + src/ui/debug.ts | 65 + src/ui/sidebar.ts | 224 ++ src/ui/stats.ts | 55 + src/ui/stream.ts | 328 ++ src/util.ts | 63 + src/vite-env.d.ts | 3 + tsconfig.json | 8 +- tsconfig.main.json | 19 - vite.config.ts | 10 +- 34 files changed, 2147 insertions(+), 4706 deletions(-) delete mode 100644 forge.config.js delete mode 100644 scripts/build-main.mjs create mode 100644 src/decoder.ts create mode 100644 src/input.ts create mode 100644 src/main.ts delete mode 100644 src/main/main.ts delete mode 100644 src/main/preload.ts delete mode 100644 src/renderer/App.tsx delete mode 100644 src/renderer/components/AddConnection.tsx delete mode 100644 src/renderer/components/ConnectionList.tsx delete mode 100644 src/renderer/components/DebugOverlay.tsx delete mode 100644 src/renderer/components/StreamPanel.tsx delete mode 100644 src/renderer/lib/debug.ts delete mode 100644 src/renderer/lib/moq-integration.ts delete mode 100644 src/renderer/lib/types.ts delete mode 100644 src/renderer/lib/webtransport-client.ts delete mode 100644 src/renderer/main.tsx delete mode 100644 src/renderer/vite-env.d.ts rename src/{renderer/App.css => style.css} (71%) create mode 100644 src/transport.ts create mode 100644 src/types.ts create mode 100644 src/ui/debug.ts create mode 100644 src/ui/sidebar.ts create mode 100644 src/ui/stats.ts create mode 100644 src/ui/stream.ts create mode 100644 src/util.ts create mode 100644 src/vite-env.d.ts delete mode 100644 tsconfig.main.json diff --git a/README.md b/README.md index 5049237..4cc8f02 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,34 @@ # client -Desktop App for Distance. + +Web viewer for [Distance](https://github.com/distancedesktop/agent) — connects to +a Distance agent over raw WebTransport and decodes its H.264 stream in the +browser. + +No Electron, no framework: vanilla TypeScript built with Vite. + +## Usage + +```bash +npm install +npm run dev # dev server on :5173 +npm run build # static bundle in dist/ +``` + +Serve `dist/` from any static host. WebTransport requires a secure context, so +use `https://` (or `localhost`) when serving it. + +Add a connection with the agent's host, port (`52020` by default) and the +SHA-256 certificate fingerprint the agent prints on startup. If the agent runs +with `--cert`/`--key` or sits behind a reverse proxy, tick **Trusted +certificate** instead and leave the fingerprint blank. + +## Protocol + +Control messages are newline-delimited JSON on one bidirectional stream; video +is raw H.264 Annex B on server-initiated unidirectional streams. See +`src/types.ts` for the message shapes, which track the agent's `src/session.go`. + +## Requirements + +A browser with WebTransport and WebCodecs: Chrome/Edge 97+, or Firefox 114+ +with WebCodecs enabled. Safari does not support WebTransport yet. diff --git a/forge.config.js b/forge.config.js deleted file mode 100644 index 224e07d..0000000 --- a/forge.config.js +++ /dev/null @@ -1,31 +0,0 @@ -module.exports = { - packagerConfig: { - name: 'Distance Desktop', - appBundleId: 'com.distance.desktop-client', - appCategoryType: 'public.app-category.utilities', - asar: true, - ignore: [ - /^\/src/, - /^\/scripts/, - /^\/node_modules\/\.cache/, - /^\/\.git/, - /^\/\.gitignore/, - /^\/forge\.config\.js/, - /^\/tsconfig.*/, - /^\/vite\.config\.ts/, - /^\/README\.md/, - /^\/LICENSE/, - /^\/\.DS_Store/, - ], - }, - makers: [ - { - name: '@electron-forge/maker-dmg', - config: {}, - }, - { - name: '@electron-forge/maker-zip', - config: {}, - }, - ], -} diff --git a/index.html b/index.html index 062619b..021c691 100644 --- a/index.html +++ b/index.html @@ -3,11 +3,23 @@ - + + + Distance Client -
- +
+ +
+
+

Select a connection to begin

+
+ +
+ + +
+ diff --git a/package-lock.json b/package-lock.json index b7bf1b8..b07a174 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,457 +1,50 @@ { "name": "distance-client", - "version": "0.1.0", + "version": "0.2.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "distance-client", - "version": "0.1.0", - "dependencies": { - "@moq/hang": "^0.2.7", - "@moq/net": "^0.1.2", - "@moq/watch": "^0.2.13", - "react": "^19.2.0", - "react-dom": "^19.2.0", - "uuid": "^14.0.0" - }, + "version": "0.2.0", "devDependencies": { - "@electron-forge/cli": "^8.0.0-alpha.9", - "@electron-forge/maker-dmg": "^8.0.0-alpha.9", - "@electron-forge/maker-zip": "^8.0.0-alpha.9", - "@types/react": "^19.2.0", - "@types/react-dom": "^19.2.0", - "@vitejs/plugin-react": "^6.0.0", - "electron": "^42.3.0", - "esbuild": "^0.28.0", "typescript": "^6.0.0", "vite": "^8.0.0" } }, - "node_modules/@electron-forge/cli": { - "version": "8.0.0-alpha.9", - "resolved": "https://registry.npmjs.org/@electron-forge/cli/-/cli-8.0.0-alpha.9.tgz", - "integrity": "sha512-tXtpGawJmu10m29P6WR9H0TifWz8UQHMkmanKvZPqg7tbU1DzNFwaFZa7qA432y9fF3LF98yxGr6YnAHCu3bXg==", - "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/malept" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/subscription/pkg/npm-.electron-forge-cli?utm_medium=referral&utm_source=npm_fund" - } - ], - "license": "MIT", - "dependencies": { - "@electron-forge/core": "8.0.0-alpha.9", - "@electron-forge/core-utils": "8.0.0-alpha.9", - "@electron-forge/shared-types": "8.0.0-alpha.9", - "@electron/get": "^5.0.0", - "commander": "^11.1.0", - "debug": "^4.3.1", - "fs-extra": "^10.0.0", - "listr2": "^7.0.2", - "semver": "^7.2.1" - }, - "bin": { - "electron-forge": "dist/electron-forge.js", - "electron-forge-vscode-nix": "script/vscode.sh", - "electron-forge-vscode-win": "script/vscode.cmd" - }, - "engines": { - "node": ">= 22.12.0" - } - }, - "node_modules/@electron-forge/core": { - "version": "8.0.0-alpha.9", - "resolved": "https://registry.npmjs.org/@electron-forge/core/-/core-8.0.0-alpha.9.tgz", - "integrity": "sha512-rVqK4luxw0mZoptoFLrFsJwCyh+iFywupbq04D+uxN4mWGE6GM02l/6tfQX5+3Agj5l54ZsPVsrzHKphzQ/e0g==", - "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/malept" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/subscription/pkg/npm-.electron-forge-core?utm_medium=referral&utm_source=npm_fund" - } - ], - "license": "MIT", - "dependencies": { - "@electron-forge/core-utils": "8.0.0-alpha.9", - "@electron-forge/maker-base": "8.0.0-alpha.9", - "@electron-forge/plugin-base": "8.0.0-alpha.9", - "@electron-forge/publisher-base": "8.0.0-alpha.9", - "@electron-forge/shared-types": "8.0.0-alpha.9", - "@electron-forge/tracer": "8.0.0-alpha.9", - "@electron/get": "^5.0.0", - "@electron/packager": "^20.0.0", - "debug": "^4.3.1", - "filenamify": "^6.0.0", - "fs-extra": "^10.0.0", - "jiti": "^2.4.2", - "listr2": "^7.0.2" - }, - "engines": { - "node": ">= 22.12.0" - } - }, - "node_modules/@electron-forge/core-utils": { - "version": "8.0.0-alpha.9", - "resolved": "https://registry.npmjs.org/@electron-forge/core-utils/-/core-utils-8.0.0-alpha.9.tgz", - "integrity": "sha512-iDsZN3OOaLpYKfgYdQ9NsKrgz/PkwV7H2uhMHb/ajNTxv9L6BTjmc0z00MCruHPXKBzf+LzfUr64snhkaEihzA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@electron-forge/shared-types": "8.0.0-alpha.9", - "@electron/rebuild": "^4.0.1", - "@malept/cross-spawn-promise": "^2.0.0", - "debug": "^4.3.1", - "fs-extra": "^10.0.0", - "parse-author": "^2.0.0", - "semver": "^7.2.1" - }, - "engines": { - "node": ">= 22.12.0" - } - }, - "node_modules/@electron-forge/maker-base": { - "version": "8.0.0-alpha.9", - "resolved": "https://registry.npmjs.org/@electron-forge/maker-base/-/maker-base-8.0.0-alpha.9.tgz", - "integrity": "sha512-YRIMVWz4sTSRJ54Uk5LBDw8sJMCWojtrvrIi2tVGvEYMCxWm5uC4u0sPUv9mTTNeD9B8xHD6doAcMTHQvyqPPA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@electron-forge/shared-types": "8.0.0-alpha.9", - "fs-extra": "^10.0.0", - "which": "^6.0.0" - }, - "engines": { - "node": ">= 22.12.0" - } - }, - "node_modules/@electron-forge/maker-dmg": { - "version": "8.0.0-alpha.9", - "resolved": "https://registry.npmjs.org/@electron-forge/maker-dmg/-/maker-dmg-8.0.0-alpha.9.tgz", - "integrity": "sha512-37SHzL0fAwZXRL5hPpn5453WMe8bQYDVDYQELkn9pT89jsXh2Xr9sxAtVC5nuX7lZ2/y4lK/jCPzlRJj4aE/jA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@electron-forge/maker-base": "8.0.0-alpha.9", - "@electron-forge/shared-types": "8.0.0-alpha.9", - "fs-extra": "^10.0.0" - }, - "engines": { - "node": ">= 22.12.0" - }, - "optionalDependencies": { - "electron-installer-dmg": "^5.0.1" - } - }, - "node_modules/@electron-forge/maker-zip": { - "version": "8.0.0-alpha.9", - "resolved": "https://registry.npmjs.org/@electron-forge/maker-zip/-/maker-zip-8.0.0-alpha.9.tgz", - "integrity": "sha512-dYZzwxuEms1JAzGV8TPzJvcXyZRZZ0pIKf9OIC1uaQfBZ6xqVYu8q6PDYI6rbqlEn0lzCBAU326T/yGDRg0n9A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@electron-forge/maker-base": "8.0.0-alpha.9", - "@electron-forge/shared-types": "8.0.0-alpha.9", - "cross-zip": "^4.0.0", - "fs-extra": "^10.0.0" - }, - "engines": { - "node": ">= 22.12.0" - } - }, - "node_modules/@electron-forge/plugin-base": { - "version": "8.0.0-alpha.9", - "resolved": "https://registry.npmjs.org/@electron-forge/plugin-base/-/plugin-base-8.0.0-alpha.9.tgz", - "integrity": "sha512-tegSquDio9Yu58833buRXCGy6IjMQMi5SwtNM8FiWtrNKt7a1pyKyHFOXm9dL37mFgg2dcyZGcmMdn0tTE6CUA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@electron-forge/shared-types": "8.0.0-alpha.9" - }, - "engines": { - "node": ">= 22.12.0" - } - }, - "node_modules/@electron-forge/publisher-base": { - "version": "8.0.0-alpha.9", - "resolved": "https://registry.npmjs.org/@electron-forge/publisher-base/-/publisher-base-8.0.0-alpha.9.tgz", - "integrity": "sha512-1mYz9Wa2FwiQLTx76s1petPPfz8LcxxFcTmHF2NQHlVuVlUPWtJMi6tDtuiq2nkHxnIafbYbgb1EIPCIsPPeLw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@electron-forge/shared-types": "8.0.0-alpha.9" - }, - "engines": { - "node": ">= 22.12.0" - } - }, - "node_modules/@electron-forge/shared-types": { - "version": "8.0.0-alpha.9", - "resolved": "https://registry.npmjs.org/@electron-forge/shared-types/-/shared-types-8.0.0-alpha.9.tgz", - "integrity": "sha512-+PwkEXe4YbsKxOo7XuzQzbgddVH0bGq9q19ll5dlw183Y/KFf4cCIle/KoKeeaU2B6wxwSWczoGOthYXlxJocw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@electron-forge/tracer": "8.0.0-alpha.9", - "@electron/packager": "^20.0.0", - "@electron/rebuild": "^4.0.1", - "listr2": "^7.0.2" - }, - "engines": { - "node": ">= 22.12.0" - } - }, - "node_modules/@electron-forge/tracer": { - "version": "8.0.0-alpha.9", - "resolved": "https://registry.npmjs.org/@electron-forge/tracer/-/tracer-8.0.0-alpha.9.tgz", - "integrity": "sha512-69/l4+0bvCi1x1TO3l40jHj5bi2YNLgS6zFGGdagF3FBSPbNEgZBZl4SwrbGD6D1TaRWYjuQDPDfad113o011g==", - "dev": true, - "license": "MIT", - "dependencies": { - "chrome-trace-event": "^1.0.3" - }, - "engines": { - "node": ">= 22.12.0" - } - }, - "node_modules/@electron/asar": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@electron/asar/-/asar-4.2.0.tgz", - "integrity": "sha512-npW1NW5yy8EB9XY/vEw9sUdgmq0sJEhmSBb6bqyFOAw1CSkrhvAvO6QWlW8CdIMo8VN1lkdF345l/MeW0LrY0Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "commander": "^13.1.0", - "glob": "^13.0.2", - "minimatch": "^10.0.1" - }, - "bin": { - "asar": "bin/asar.mjs" - }, - "engines": { - "node": ">=22.12.0" - } - }, - "node_modules/@electron/asar/node_modules/commander": { - "version": "13.1.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-13.1.0.tgz", - "integrity": "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/@electron/get": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/@electron/get/-/get-5.0.0.tgz", - "integrity": "sha512-pjoBpru1KdEtcExBnuHAP1cAc/5faoedw0hzJkL3o4/IJp7HNF1+fbrdxT3gMYRX2oJfvnA/WXeCTVQpYYxyJA==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^4.1.1", - "env-paths": "^3.0.0", - "graceful-fs": "^4.2.11", - "progress": "^2.0.3", - "semver": "^7.6.3", - "sumchecker": "^3.0.1" - }, - "engines": { - "node": ">=22.12.0" - }, - "optionalDependencies": { - "undici": "^7.24.4" - } - }, - "node_modules/@electron/notarize": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/@electron/notarize/-/notarize-3.1.1.tgz", - "integrity": "sha512-uQQSlOiJnqRkTL1wlEBAxe90nVN/Fc/hEmk0bqpKk8nKjV1if/tXLHKUPePtv9Xsx90PtZU8aidx5lAiOpjkQQ==", + "node_modules/@oxc-project/types": { + "version": "0.147.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.147.0.tgz", + "integrity": "sha512-IJ3s6ltHLp45S0bh7phkX+gJO7A1Wuz2EaqpAhb8WjqDwbzMiWKHhyyT42tskaWjEYXtHtVCPpnBJVT9+dcRLg==", "dev": true, "license": "MIT", - "dependencies": { - "debug": "^4.4.0", - "promise-retry": "^2.0.1" - }, - "engines": { - "node": ">= 22.12.0" - } - }, - "node_modules/@electron/osx-sign": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/@electron/osx-sign/-/osx-sign-2.4.0.tgz", - "integrity": "sha512-9mJiKF/yrn2FYt1Pd9hIN8bUWs6w6uhm8DJJ/tZtTPMVmqv9/NO1JvRh+NyOJp8kE2bfffK4YkU3ZyM+rHipUw==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "debug": "^4.3.4", - "isbinaryfile": "^4.0.8", - "plist": "^3.0.5", - "semver": "^7.7.1" - }, - "bin": { - "electron-osx-flat": "bin/electron-osx-flat.mjs", - "electron-osx-sign": "bin/electron-osx-sign.mjs" - }, - "engines": { - "node": ">=22.12.0" - } - }, - "node_modules/@electron/packager": { - "version": "20.0.0", - "resolved": "https://registry.npmjs.org/@electron/packager/-/packager-20.0.0.tgz", - "integrity": "sha512-kl4c4LcsrQflg0wAi3mqpmmZFRdTYoZFLZPBP9YeEIvWwQR1NmkZphwT7558UeLOdhr6Ac5l83r8W69KaJBedg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "@electron/asar": "^4.0.1", - "@electron/get": "^5.0.0", - "@electron/notarize": "^3.1.0", - "@electron/osx-sign": "^2.2.0", - "@electron/universal": "^3.0.1", - "@electron/windows-sign": "^2.0.2", - "@malept/cross-spawn-promise": "^2.0.0", - "debug": "^4.4.1", - "extract-zip": "^2.0.1", - "filenamify": "^6.0.0", - "galactus": "^2.0.2", - "graceful-fs": "^4.2.11", - "junk": "^4.0.1", - "parse-author": "^2.0.0", - "plist": "^3.1.0", - "resedit": "^2.0.3", - "semver": "^7.7.2", - "yargs-parser": "^22.0.0" - }, - "bin": { - "electron-packager": "bin/electron-packager.mjs" - }, - "engines": { - "node": ">= 22.12.0" - }, "funding": { - "url": "https://github.com/electron/packager?sponsor=1" - } - }, - "node_modules/@electron/rebuild": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/@electron/rebuild/-/rebuild-4.0.4.tgz", - "integrity": "sha512-Rzc39XPdk/+/wBG8MfwAHohXflep0ITUfulb6Rgz3R0NeSB1noE+E9/M/cb8ftCAiyDD9PPhLuuWgE1GaInbKg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@malept/cross-spawn-promise": "^2.0.0", - "debug": "^4.1.1", - "node-abi": "^4.2.0", - "node-api-version": "^0.2.1", - "node-gyp": "^12.2.0", - "read-binary-file-arch": "^1.0.6" - }, - "bin": { - "electron-rebuild": "lib/cli.js" - }, - "engines": { - "node": ">=22.12.0" - } - }, - "node_modules/@electron/universal": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@electron/universal/-/universal-3.0.4.tgz", - "integrity": "sha512-G7mNdfZIdPbx54dTorGlV7SN7nHAKlbJU1NjjeuEo7RzEMYBG62+tKpA2VrzocGcvFLxqJI5XYIknca9EijwOw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@electron/asar": "^4.0.0", - "debug": "^4.3.1", - "plist": "^3.1.0" - }, - "engines": { - "node": ">=22.12.0" - } - }, - "node_modules/@electron/windows-sign": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@electron/windows-sign/-/windows-sign-2.0.3.tgz", - "integrity": "sha512-lJGpt2artEZNiOsQtU1JmcLr4Ow/AGskwjTTNaxL2+R2wLN7G2BHL4Z9unOoW321N69kBP1nzObSMdDbQydjbA==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "debug": "^4.3.4", - "graceful-fs": "^4.2.11", - "postject": "^1.0.0-alpha.6" - }, - "bin": { - "electron-windows-sign": "bin/electron-windows-sign.mjs" - }, - "engines": { - "node": ">=22.12.0" - } - }, - "node_modules/@emnapi/core": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", - "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/wasi-threads": "1.2.1", - "tslib": "^2.4.0" - } - }, - "node_modules/@emnapi/runtime": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", - "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@emnapi/wasi-threads": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", - "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" + "url": "https://github.com/sponsors/Boshen" } }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.0.tgz", - "integrity": "sha512-lhRUCeuOyJQURhTxl4WkpFTjIsbDayJHih5kZC1giwE+MhIzAb7mEsQMqMf18rHLsrb5qI1tafG20mLxEWcWlA==", + "node_modules/@rolldown/binding-android-arm-eabi": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.6.tgz", + "integrity": "sha512-b+jTcARdTiFLI6jB4a5XjTm0RWd6KcRfQj/I2356fxUZemiho9zQLxo0RtCuMDAyKcLo6cEltkgbQp6d1+sjjQ==", "cpu": [ - "ppc64" + "arm" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "aix" + "android" ], "engines": { - "node": ">=18" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@esbuild/android-arm": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.0.tgz", - "integrity": "sha512-wqh0ByljabXLKHeWXYLqoJ5jKC4XBaw6Hk08OfMrCRd2nP2ZQ5eleDZC41XHyCNgktBGYMbqnrJKq/K/lzPMSQ==", + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.6.tgz", + "integrity": "sha512-lkWU8ZJaRk9q3CIEY1Tc7vIFALp3Xw5NfGJo2hQg5oIqNgxWi1zI+IiDEK3r70BF5Dzol1tcXsnzsRc8NLhG+Q==", "cpu": [ - "arm" + "arm64" ], "dev": true, "license": "MIT", @@ -460,13 +53,13 @@ "android" ], "engines": { - "node": ">=18" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@esbuild/android-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.0.tgz", - "integrity": "sha512-+WzIXQOSaGs33tLEgYPYe/yQHf0WTU0X42Jca3y8NWMbUVhp7rUnw+vAsRC/QiDrdD31IszMrZy+qwPOPjd+rw==", + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.6.tgz", + "integrity": "sha512-dgR56NYnvAszm7Ob1B2/Vn0e8bUQYZH2UjVaMMtMVOCKFSfjhfLmuA/9+O+F+ajUdG6B/bSssrKW6JJYASa8jA==", "cpu": [ "arm64" ], @@ -474,16 +67,16 @@ "license": "MIT", "optional": true, "os": [ - "android" + "darwin" ], "engines": { - "node": ">=18" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@esbuild/android-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.0.tgz", - "integrity": "sha512-+VJggoaKhk2VNNqVL7f6S189UzShHC/mR9EE8rDdSkdpN0KflSwWY/gWjDrNxxisg8Fp1ZCD9jLMo4m0OUfeUA==", + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.6.tgz", + "integrity": "sha512-vpVxFvUCFioJqug7OTvqptkc4yb8UX0AwfDmJpaR/0sWz+BUmqSVAf7c8JkUgnN8YLspb4a/N6NhTyMAmdyQ7Q==", "cpu": [ "x64" ], @@ -491,271 +84,187 @@ "license": "MIT", "optional": true, "os": [ - "android" + "darwin" ], "engines": { - "node": ">=18" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.0.tgz", - "integrity": "sha512-0T+A9WZm+bZ84nZBtk1ckYsOvyA3x7e2Acj1KdVfV4/2tdG4fzUp91YHx+GArWLtwqp77pBXVCPn2We7Letr0Q==", + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.6.tgz", + "integrity": "sha512-h1wG6Y6K3JlRswxsI64qQJqBAy4vrLuHgRbc8CZMGSWTOFRY6ghMApM1NKzB2I0n5xV1fjkE18SuVl2QpLeNpA==", "cpu": [ - "arm64" + "x64" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "darwin" + "freebsd" ], "engines": { - "node": ">=18" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.0.tgz", - "integrity": "sha512-fyzLm/DLDl/84OCfp2f/XQ4flmORsjU7VKt8HLjvIXChJoFFOIL6pLJPH4Yhd1n1gGFF9mPwtlN5Wf82DZs+LQ==", + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.6.tgz", + "integrity": "sha512-tbCiqub0q2MVWJKgF5PoAlNWCtQydiOYSLIkd8sByqK/6MMYLJRcSXSYodqYtd0O+Fw7QaVmKKlS4oL94YRZ0w==", "cpu": [ - "x64" + "arm" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "darwin" + "linux" ], "engines": { - "node": ">=18" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.0.tgz", - "integrity": "sha512-l9GeW5UZBT9k9brBYI+0WDffcRxgHQD8ShN2Ur4xWq/NFzUKm3k5lsH4PdaRgb2w7mI9u61nr2gI2mLI27Nh3Q==", + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.6.tgz", + "integrity": "sha512-oxK9+baEBPhZG5HB4URY+uU04zJWeZlH6Tb9rB5DK4DF9XR1uXNLXt5Q5ZsugTKayNCNLhkcwz/ye74hRI98dg==", "cpu": [ "arm64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ - "freebsd" + "linux" ], "engines": { - "node": ">=18" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.0.tgz", - "integrity": "sha512-BXoQai/A0wPO6Es3yFJ7APCiKGc1tdAEOgeTNy3SsB491S3aHn4S4r3e976eUnPdU+NbdtmBuLncYir2tMU9Nw==", + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.6.tgz", + "integrity": "sha512-muWCk27FVBEZtv0MsK8gnfSmgczA8KQ0uRVJbTABKhkRfQc38aUrcb7fhi3BNiyseFmgcRsoMfQsSNJ+DbZdSw==", "cpu": [ - "x64" + "arm64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ - "freebsd" + "linux" ], "engines": { - "node": ">=18" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@esbuild/linux-arm": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.0.tgz", - "integrity": "sha512-CjaaREJagqJp7iTaNQjjidaNbCKYcd4IDkzbwwxtSvjI7NZm79qiHc8HqciMddQ6CKvJT6aBd8lO9kN/ZudLlw==", + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.6.tgz", + "integrity": "sha512-eWDoSfU7Co2qj3vgB3Dt4lj1mG6CoWbcJQkRMP3XJplyCMtuaq3LHvPFjS9QIPvMGWVadJC04Xiy0IdcVPtnwQ==", "cpu": [ - "arm" + "ppc64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=18" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.0.tgz", - "integrity": "sha512-RVyzfb3FWsGA55n6WY0MEIEPURL1FcbhFE6BffZEMEekfCzCIMtB5yyDcFnVbTnwk+CLAgTujmV/Lgvih56W+A==", + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.6.tgz", + "integrity": "sha512-2bWNjRSIayvupRKxXUY2tWG9fYdoUlTqWywHRvE8Eq3GvuQ+f2HeIkve697fIt+IQs/PV8yFsdWuhp1aJ1PdnA==", "cpu": [ - "arm64" + "s390x" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=18" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.0.tgz", - "integrity": "sha512-KBnSTt1kxl9x70q+ydterVdl+Cn0H18ngRMRCEQfrbqdUuntQQ0LoMZv47uB97NljZFzY6HcfqEZ2SAyIUTQBQ==", + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.6.tgz", + "integrity": "sha512-KekI0gS0wLxe1UBSQSjenBVwou/JkcQPDzBPICGZjxUv9k3RteHDPBQaiOicZUFKRIH2wKEimGwVpnJsbPzu7w==", "cpu": [ - "ia32" + "x64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=18" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.0.tgz", - "integrity": "sha512-zpSlUce1mnxzgBADvxKXX5sl8aYQHo2ezvMNI8I0lbblJtp8V4odlm3Yzlj7gPyt3T8ReksE6bK+pT3WD+aJRg==", + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.6.tgz", + "integrity": "sha512-TvtPnfVr+HtyGiDmPK4VWmlNm7QhNNAcK5Q9A7aOXsI8545yCyaoMaicXrFZ72JzeYjaUVk7yT243zT0jzjFKQ==", "cpu": [ - "loong64" + "x64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=18" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.0.tgz", - "integrity": "sha512-2jIfP6mmjkdmeTlsX/9vmdmhBmKADrWqN7zcdtHIeNSCH1SqIoNI63cYsjQR8J+wGa4Y5izRcSHSm8K3QWmk3w==", + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.6.tgz", + "integrity": "sha512-iOo0VEay2XFhaCcH0sps5XIimkSuOnNaZrf6+ZkoSOQBJPKNU48RkmJv0/lSpipexu5P+ouFgafe5IGr/DiQfg==", "cpu": [ - "mips64el" + "arm64" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "linux" + "openharmony" ], "engines": { - "node": ">=18" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.0.tgz", - "integrity": "sha512-bc0FE9wWeC0WBm49IQMPSPILRocGTQt3j5KPCA8os6VprfuJ7KD+5PzESSrJ6GmPIPJK965ZJHTUlSA6GNYEhg==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.0.tgz", - "integrity": "sha512-SQPZOwoTTT/HXFXQJG/vBX8sOFagGqvZyXcgLA3NhIqcBv1BJU1d46c0rGcrij2B56Z2rNiSLaZOYW5cUk7yLQ==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.0.tgz", - "integrity": "sha512-SCfR0HN8CEEjnYnySJTd2cw0k9OHB/YFzt5zgJEwa+wL/T/raGWYMBqwDNAC6dqFKmJYZoQBRfHjgwLHGSrn3Q==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.0.tgz", - "integrity": "sha512-us0dSb9iFxIi8srnpl931Nvs65it/Jd2a2K3qs7fz2WfGPHqzfzZTfec7oxZJRNPXPnNYZtanmRc4AL/JwVzHQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.0.tgz", - "integrity": "sha512-CR/RYotgtCKwtftMwJlUU7xCVNg3lMYZ0RzTmAHSfLCXw3NtZtNpswLEj/Kkf6kEL3Gw+BpOekRX0BYCtklhUw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.0.tgz", - "integrity": "sha512-nU1yhmYutL+fQ71Kxnhg8uEOdC0pwEW9entHykTgEbna2pw2dkbFSMeqjjyHZoCmt8SBkOSvV+yNmm94aUrrqw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.0.tgz", - "integrity": "sha512-cXb5vApOsRsxsEl4mcZ1XY3D4DzcoMxR/nnc4IyqYs0rTI8ZKmW6kyyg+11Z8yvgMfAEldKzP7AdP64HnSC/6g==", + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.6.tgz", + "integrity": "sha512-y5NTmmasMS455JlOCO4ZM9krIchv3Mvm1crL1iUPGOPgEzSkves9n0SdC5Sjz6+qWDFhd8/JpfWMH8NSWNHe+A==", "cpu": [ "arm64" ], @@ -763,16 +272,16 @@ "license": "MIT", "optional": true, "os": [ - "openbsd" + "win32" ], "engines": { - "node": ">=18" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.0.tgz", - "integrity": "sha512-8wZM2qqtv9UP3mzy7HiGYNH/zjTA355mpeuA+859TyR+e+Tc08IHYpLJuMsfpDJwoLo1ikIJI8jC3GFjnRClzA==", + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.6.tgz", + "integrity": "sha512-np8iZSLfXlAD4kWhiyq/u0Yt8oZDtRQ8lGhQaCXo2rl37KNjeU0GjJuwr4P3oeZ++ROfofsKNBqR5LTO8aXyWQ==", "cpu": [ "x64" ], @@ -780,2950 +289,445 @@ "license": "MIT", "optional": true, "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openharmony-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.0.tgz", - "integrity": "sha512-FLGfyizszcef5C3YtoyQDACyg95+dndv79i2EekILBofh5wpCa1KuBqOWKrEHZg3zrL3t5ouE5jgr94vA+Wb2w==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" + "win32" ], "engines": { - "node": ">=18" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.0.tgz", - "integrity": "sha512-1ZgjUoEdHZZl/YlV76TSCz9Hqj9h9YmMGAgAPYd+q4SicWNX3G5GCyx9uhQWSLcbvPW8Ni7lj4gDa1T40akdlw==", - "cpu": [ - "x64" - ], + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" - } + "license": "MIT" }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.0.tgz", - "integrity": "sha512-Q9StnDmQ/enxnpxCCLSg0oo4+34B9TdXpuyPeTedN/6+iXBJ4J+zwfQI28u/Jl40nOYAxGoNi7mFP40RUtkmUA==", - "cpu": [ - "arm64" - ], + "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": "MIT", - "optional": true, - "os": [ - "win32" - ], + "license": "Apache-2.0", "engines": { - "node": ">=18" + "node": ">=8" } }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.0.tgz", - "integrity": "sha512-zF3ag/gfiCe6U2iczcRzSYJKH1DCI+ByzSENHlM2FcDbEeo5Zd2C86Aq0tKUYAJJ1obRP84ymxIAksZUcdztHA==", - "cpu": [ - "ia32" - ], + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "win32" - ], "engines": { - "node": ">=18" + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } } }, - "node_modules/@esbuild/win32-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.0.tgz", - "integrity": "sha512-pEl1bO9mfAmIC+tW5btTmrKaujg3zGtUmWNdCw/xs70FBjwAL3o9OEKNHvNmnyylD6ubxUERiEhdsL0xBQ9efw==", - "cpu": [ - "x64" - ], + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", "dev": true, + "hasInstallScript": true, "license": "MIT", "optional": true, "os": [ - "win32" + "darwin" ], "engines": { - "node": ">=18" - } - }, - "node_modules/@isaacs/fs-minipass": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", - "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", - "dev": true, - "license": "ISC", - "dependencies": { - "minipass": "^7.0.4" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@kixelated/libavjs-webcodecs-polyfill": { - "version": "0.5.5", - "resolved": "https://registry.npmjs.org/@kixelated/libavjs-webcodecs-polyfill/-/libavjs-webcodecs-polyfill-0.5.5.tgz", - "integrity": "sha512-Q1zgnTMMQ2F7IE9ylx3C1XzVbg5vYN18jiDINO5U3kNPBOHdYuUlJsMhtBoqr1M6ocLtoiqdHmLs7tHFgrw5KA==", - "license": "0BSD", - "dependencies": { - "@libav.js/types": "^6.7.7", - "@ungap/global-this": "^0.4.4" + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, - "node_modules/@libav.js/types": { - "version": "6.8.8", - "resolved": "https://registry.npmjs.org/@libav.js/types/-/types-6.8.8.tgz", - "integrity": "sha512-Lbik/0Q3x2R8cI7mOtRgt+nUWLqGXh7UinMndmpdXSDY4YEjYyVUDsq6fxkuriL78+LCYx8frZIN1r+oDsvYCQ==", - "license": "LGPL-2.1" - }, - "node_modules/@libav.js/variant-opus-af": { - "version": "6.8.8", - "resolved": "https://registry.npmjs.org/@libav.js/variant-opus-af/-/variant-opus-af-6.8.8.tgz", - "integrity": "sha512-8KBQyA8n5goN7lyctOaPxpcx7dapOgqKh8dWW/NAcl87AgM/WoUGSex3fFc46oCtTHYrUKEm1OmZUrtkt3Q56A==", - "license": "LGPL-2.1" - }, - "node_modules/@malept/cross-spawn-promise": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@malept/cross-spawn-promise/-/cross-spawn-promise-2.0.0.tgz", - "integrity": "sha512-1DpKU0Z5ThltBwjNySMC14g0CkbyhCaz9FkhxqNsZI6uAPJXFS8cMXlBKo26FJ8ZuW6S9GCMcR9IO5k2X5/9Fg==", + "node_modules/lightningcss": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", + "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/malept" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/subscription/pkg/npm-.malept-cross-spawn-promise?utm_medium=referral&utm_source=npm_fund" - } - ], - "license": "Apache-2.0", + "license": "MPL-2.0", "dependencies": { - "cross-spawn": "^7.0.1" + "detect-libc": "^2.0.3" }, "engines": { - "node": ">= 12.13.0" - } - }, - "node_modules/@moq/hang": { - "version": "0.2.7", - "resolved": "https://registry.npmjs.org/@moq/hang/-/hang-0.2.7.tgz", - "integrity": "sha512-HLPpzDLsUf1dFA1M6qJMuo3Qupbyq/aiaXagh0anTjhOIjMReTF0f34Rqz+OlDWXBD3U18sPixc/WBHCwBvc0Q==", - "license": "(MIT OR Apache-2.0)", - "dependencies": { - "@kixelated/libavjs-webcodecs-polyfill": "^0.5.5", - "@libav.js/variant-opus-af": "^6.8.8", - "@moq/loc": "^0.1.0", - "@moq/net": "^0.1.2", - "@moq/signals": "^0.1.7", - "@svta/cml-iso-bmff": "^1.0.0-alpha.9", - "zod": "^4.1.5" - } - }, - "node_modules/@moq/loc": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/@moq/loc/-/loc-0.1.0.tgz", - "integrity": "sha512-8ouPBhoZU/vAaHCrcsi97zOw0BJX/rQbfUJ7Dx2/Cmao6c4knrAXl8EVjlwKM4HLjfa8xzPd4Lqxkfv7G4QyhA==", - "license": "(MIT OR Apache-2.0)", - "dependencies": { - "@moq/net": "^0.1.1" - } - }, - "node_modules/@moq/msf": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/@moq/msf/-/msf-0.1.1.tgz", - "integrity": "sha512-+Fjw0r4U4FCZS4Lo+pb06+jZqXgMmInNdY7RIZs9M8kDYdkC9wi/hymdIOAcmwr6f1kF9fLNZ7Spfx8oEgcFSg==", - "license": "(MIT OR Apache-2.0)", - "dependencies": { - "@moq/net": "^0.1.2", - "zod": "^4.1.5" - } - }, - "node_modules/@moq/net": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/@moq/net/-/net-0.1.2.tgz", - "integrity": "sha512-j8Eyu188n06RGWXIoarVFYPTWEq52WhdXVg3S0a3CGpXP0j5t0t2p9kUzUSgShdk4W6ItbvZ30mR+Ihwmuja0Q==", - "license": "(MIT OR Apache-2.0)", - "dependencies": { - "@moq/qmux": "^0.0.6", - "@moq/signals": "^0.1.7", - "async-mutex": "^0.5.0" - }, - "peerDependencies": { - "zod": "^4.0.0" - } - }, - "node_modules/@moq/qmux": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/@moq/qmux/-/qmux-0.0.6.tgz", - "integrity": "sha512-ISuGz05lUvf1hzHW3Aw3VnsGRJe1w9Qdog3LQ66KS+l+5mzQsPANvW8yOioEe1Z9dJO2G3sAHoGPnzwnsY9SIQ==", - "license": "(MIT OR Apache-2.0)" - }, - "node_modules/@moq/signals": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/@moq/signals/-/signals-0.1.7.tgz", - "integrity": "sha512-Z45dKktZj1BLviWSXgZmtlePy02jx0H3cYp5+q9aYpDpb9k+v8shpjwBNNuTGxT4PUgxiQQFDY6YpKfLb54Xwg==", - "license": "(MIT OR Apache-2.0)", - "peerDependencies": { - "@types/react": "^19.1.8", - "react": "^19.0.0", - "solid-js": "^1.9.7" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "react": { - "optional": true - }, - "solid-js": { - "optional": true - } - } - }, - "node_modules/@moq/watch": { - "version": "0.2.13", - "resolved": "https://registry.npmjs.org/@moq/watch/-/watch-0.2.13.tgz", - "integrity": "sha512-U3nh58M6eUaJdaNYj2Mnyj+0j/wuxAGXHJq5jmZqf6yIRJyBAciL3P5pPP+hbbkmPG4AORGauKyRLDMRabQ4qQ==", - "license": "(MIT OR Apache-2.0)", - "dependencies": { - "@moq/hang": "^0.2.7", - "@moq/msf": "^0.1.1", - "@moq/net": "^0.1.2", - "@moq/signals": "^0.1.7" - } - }, - "node_modules/@napi-rs/wasm-runtime": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz", - "integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@tybys/wasm-util": "^0.10.1" + "node": ">= 12.0.0" }, "funding": { - "type": "github", - "url": "https://github.com/sponsors/Brooooooklyn" + "type": "opencollective", + "url": "https://opencollective.com/parcel" }, - "peerDependencies": { - "@emnapi/core": "^1.7.1", - "@emnapi/runtime": "^1.7.1" - } - }, - "node_modules/@oxc-project/types": { - "version": "0.132.0", - "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.132.0.tgz", - "integrity": "sha512-FESMOxil5Se014ui/Eq8fT5uHJo6nIRwH0PfJrZJXs6Gek3ZVFOrpUv3YIZT20m+extU98Hg1Ym72U58rlsxUQ==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/Boshen" + "optionalDependencies": { + "lightningcss-android-arm64": "1.33.0", + "lightningcss-darwin-arm64": "1.33.0", + "lightningcss-darwin-x64": "1.33.0", + "lightningcss-freebsd-x64": "1.33.0", + "lightningcss-linux-arm-gnueabihf": "1.33.0", + "lightningcss-linux-arm64-gnu": "1.33.0", + "lightningcss-linux-arm64-musl": "1.33.0", + "lightningcss-linux-x64-gnu": "1.33.0", + "lightningcss-linux-x64-musl": "1.33.0", + "lightningcss-win32-arm64-msvc": "1.33.0", + "lightningcss-win32-x64-msvc": "1.33.0" } }, - "node_modules/@rolldown/binding-android-arm64": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.2.tgz", - "integrity": "sha512-ZS4D1JPGn/MYQN/SYDWftIE/nVsM8j/AFOYEzAoOE2O3NktQOZru+/vYXGbR/qtdLdIfGCP0lcoJiYVzsEz+iQ==", + "node_modules/lightningcss-android-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz", + "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", "cpu": [ "arm64" ], "dev": true, - "license": "MIT", + "license": "MPL-2.0", "optional": true, "os": [ "android" ], "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/@rolldown/binding-darwin-arm64": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.2.tgz", - "integrity": "sha512-vdFA9+C/rekyGce7WqHs/xoT0ioZEWaOFyZLIV1mEeNFaFDUQrPIo8Vs2GvJ6eetb3rzDUtUBgzto3ExpXJB3w==", + "node_modules/lightningcss-darwin-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz", + "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==", "cpu": [ "arm64" ], "dev": true, - "license": "MIT", + "license": "MPL-2.0", "optional": true, "os": [ "darwin" ], "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/@rolldown/binding-darwin-x64": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.2.tgz", - "integrity": "sha512-BewSOwTHazv77DTYiAZXSqqKZ4KP/KonFisDMVU7PImxoWfB2aepnPhd2E4SWz3zDzYgDNbs6jBmTdgNnF02GA==", + "node_modules/lightningcss-darwin-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz", + "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", "cpu": [ "x64" ], "dev": true, - "license": "MIT", + "license": "MPL-2.0", "optional": true, "os": [ "darwin" ], "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/@rolldown/binding-freebsd-x64": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.2.tgz", - "integrity": "sha512-m41o7M0YWtUdqk61Tb+jnKb2rN++iRdIASlExkUoKfIAH30DOHCB8fVLzSUpbWHHU8esmEioY62PxzexE8MBuA==", + "node_modules/lightningcss-freebsd-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz", + "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", "cpu": [ "x64" ], "dev": true, - "license": "MIT", + "license": "MPL-2.0", "optional": true, "os": [ "freebsd" ], "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/@rolldown/binding-linux-arm-gnueabihf": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.2.tgz", - "integrity": "sha512-jcojB9H7W/jS29pMKWAK1N+fU99vXodHDTatS3b3y/XSOCiHo0kkA74pL3jJmkoQtYpOCxDvaKs1fo2Ij/1X5w==", + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz", + "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", "cpu": [ "arm" ], "dev": true, - "license": "MIT", + "license": "MPL-2.0", "optional": true, "os": [ "linux" ], "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-arm64-gnu": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.2.tgz", - "integrity": "sha512-1jn6qDU5iiOgFgygDzKUuKP0maTi0/f1+sBLgvij/76C77Nm3ts6ufz9Bjg5q5dduxiUIxtq86JIoBvo1xQ4Ig==", - "cpu": [ - "arm64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-arm64-musl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.2.tgz", - "integrity": "sha512-QVLO/czFMdoMFSqlX3bcswcJNm/23r+qoa/jgtmFc/qEp6/jXmIkDjF/XIo8dPfGaiwy1xfQn8o77L79GeXFgw==", - "cpu": [ - "arm64" - ], - "dev": true, - "libc": [ - "musl" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-ppc64-gnu": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.2.tgz", - "integrity": "sha512-hgO5Abm0w5UL6FEa2iFnZqo2KlK7TQ5QhV5x09hujBf7t5KzHQ1VmfPuTpqRy/rNlSxua3eWH374xxiVrP+lcA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-s390x-gnu": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.2.tgz", - "integrity": "sha512-fy8rXxuYEu602abC8MUNaPjYLIFzReOaEIEMKMUa0rFEUxNpVXhs15KSSQ4qlqSaM7B6rcj9rDZgADh/IGDzLQ==", - "cpu": [ - "s390x" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-x64-gnu": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.2.tgz", - "integrity": "sha512-0+bOkiQ779+r1WpoHOWHqncvyySci0vKph+myNDYb+im6meJAzHQXay6oEgnkHuUGouM1LKTZwqKpBow6Kj7CQ==", - "cpu": [ - "x64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-x64-musl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.2.tgz", - "integrity": "sha512-mjSkrzZK5Qsl0a9d1JgILOiuZOSDTVdKENcSXBoqbzSrspLR/4/IRVDo5wd2GgZjNss/viBFJdeq+j7qH2nypw==", - "cpu": [ - "x64" - ], - "dev": true, - "libc": [ - "musl" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-openharmony-arm64": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.2.tgz", - "integrity": "sha512-1v5vHasdfQAZoEHakBV72LIFAC9JjnymsiKxp+GEr/ma3+NJCPSaYK+qavInOovJkgwFrs7GccX2d6IgDA3Z5w==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-wasm32-wasi": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.2.tgz", - "integrity": "sha512-mb1VobWn6NheziTk5/WEaR6AKVbrwT5sOi6C7zk3gy/pD1qtJfU1j4PgTo2NJnOtbL9Dl3Aeei8w9jJ7qC2jZQ==", - "cpu": [ - "wasm32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/core": "1.10.0", - "@emnapi/runtime": "1.10.0", - "@napi-rs/wasm-runtime": "^1.1.4" - }, - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-win32-arm64-msvc": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.2.tgz", - "integrity": "sha512-SqKonF56vA/L2yHwHYcEp2P34URpOZ7d1fS635cTkpDnUtEGdUbhI6NzsPdqeSWvAAeGDrxjWjNmibDIdFf9/A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-win32-x64-msvc": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.2.tgz", - "integrity": "sha512-v7qRI7gXLRINcOGXt+7YmAZ6iFuyZVMIoXAxhd8oP+DR9dLfL9GfNIx7PLMxmhZdvq8waUJBQiWN9EKNy+TRBQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/pluginutils": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", - "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@svta/cml-iso-bmff": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@svta/cml-iso-bmff/-/cml-iso-bmff-1.0.2.tgz", - "integrity": "sha512-c9UgY1z16zgvTEa6fS++9E9zxLuPtLJ4Dw5ebOgEDtwIw+PIhbh3URGXjv30tyDU2QQTXstI6U1q6h/Q4SkxGQ==", - "license": "Apache-2.0", - "engines": { - "node": ">=20" - }, - "peerDependencies": { - "@svta/cml-utils": "1.5.0" - } - }, - "node_modules/@svta/cml-utils": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@svta/cml-utils/-/cml-utils-1.5.0.tgz", - "integrity": "sha512-JMqclD7Akd+GSJiuaYNUHOP2wNtf/nauKeszlYeivSHfi0Lp3pmSW5PXDvJ2dO4aPmmSOQbF0ztTsW9Vcs2Whw==", - "license": "Apache-2.0", - "peer": true, - "engines": { - "node": ">=20" - } - }, - "node_modules/@tybys/wasm-util": { - "version": "0.10.2", - "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz", - "integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@types/appdmg": { - "version": "0.5.5", - "resolved": "https://registry.npmjs.org/@types/appdmg/-/appdmg-0.5.5.tgz", - "integrity": "sha512-G+n6DgZTZFOteITE30LnWj+HRVIGr7wMlAiLWOO02uJFWVEitaPU9JVXm9wJokkgshBawb2O1OykdcsmkkZfgg==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/node": { - "version": "24.12.4", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.12.4.tgz", - "integrity": "sha512-GUUEShf+PBCGW2KaXwcIt3Yk+e3pkKwWKb9GSyM9WQVE+ep2jzmHdGsHzu4wgcZy5fN9FBdVzjpBQsYlpfpgLA==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~7.16.0" - } - }, - "node_modules/@types/react": { - "version": "19.2.15", - "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.15.tgz", - "integrity": "sha512-eRwcGNHve+E8qtEQSSRl6urh+rFop4v8gm6O8rGv25CodbvFdLjA1vVQ1KkiFE0w0UPOnb8tDiFKL5lp0rtY5Q==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "csstype": "^3.2.2" - } - }, - "node_modules/@types/react-dom": { - "version": "19.2.3", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", - "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "@types/react": "^19.2.0" - } - }, - "node_modules/@types/yauzl": { - "version": "2.10.3", - "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", - "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@ungap/global-this": { - "version": "0.4.4", - "resolved": "https://registry.npmjs.org/@ungap/global-this/-/global-this-0.4.4.tgz", - "integrity": "sha512-mHkm6FvepJECMNthFuIgpAEFmPOk71UyXuIxYfjytvFTnSDBIz7jmViO+LfHI/AjrazWije0PnSP3+/NlwzqtA==", - "license": "ISC" - }, - "node_modules/@vitejs/plugin-react": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.2.tgz", - "integrity": "sha512-DlSMqo4WhThw4vB8Mpn0Woe9J+Jfq1geJ61AKW0QEgLzGMNwtIMdxbDUzLxcun8W7NbJO0e2Jg/Nxm3cCSVzzg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@rolldown/pluginutils": "^1.0.0" - }, - "engines": { - "node": "^20.19.0 || >=22.12.0" - }, - "peerDependencies": { - "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", - "babel-plugin-react-compiler": "^1.0.0", - "vite": "^8.0.0" - }, - "peerDependenciesMeta": { - "@rolldown/plugin-babel": { - "optional": true - }, - "babel-plugin-react-compiler": { - "optional": true - } - } - }, - "node_modules/@xmldom/xmldom": { - "version": "0.9.10", - "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.9.10.tgz", - "integrity": "sha512-A9gOqLdi6cV4ibazAjcQufGj0B1y/vDqYrcuP6d/6x8P27gRS8643Dj9o1dEKtB6O7fwxb2FgBmJS2mX7gpvdw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.6" - } - }, - "node_modules/abbrev": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-4.0.0.tgz", - "integrity": "sha512-a1wflyaL0tHtJSmLSOVybYhy22vRih4eduhhrkcjgrWGnRfrZtovJ2FRjxuTtkkj47O/baf0R86QU5OuYpz8fA==", - "dev": true, - "license": "ISC", - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/appdmg": { - "version": "0.6.6", - "resolved": "https://registry.npmjs.org/appdmg/-/appdmg-0.6.6.tgz", - "integrity": "sha512-GRmFKlCG+PWbcYF4LUNonTYmy0GjguDy6Jh9WP8mpd0T6j80XIJyXBiWlD0U+MLNhqV9Nhx49Gl9GpVToulpLg==", - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "dependencies": { - "async": "^1.4.2", - "ds-store": "^0.1.5", - "execa": "^1.0.0", - "fs-temp": "^1.0.0", - "fs-xattr": "^0.3.0", - "image-size": "^0.7.4", - "is-my-json-valid": "^2.20.0", - "minimist": "^1.1.3", - "parse-color": "^1.0.0", - "path-exists": "^4.0.0", - "repeat-string": "^1.5.4" - }, - "bin": { - "appdmg": "bin/appdmg.js" - }, - "engines": { - "node": ">=8.5" - } - }, - "node_modules/async": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", - "integrity": "sha512-nSVgobk4rv61R9PUSDtYt7mPVB2olxNR5RWJcAsH676/ef11bUZwvu7+RGYrYauVdDPcO519v68wRhXQtxsV9w==", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/async-mutex": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/async-mutex/-/async-mutex-0.5.0.tgz", - "integrity": "sha512-1A94B18jkJ3DYq284ohPxoXbfTA5HsQ7/Mf4DEhcyLx3Bz27Rh59iScbB6EPiP+B+joue6YCxcMXSbFC1tZKwA==", - "license": "MIT", - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/author-regex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/author-regex/-/author-regex-1.0.0.tgz", - "integrity": "sha512-KbWgR8wOYRAPekEmMXrYYdc7BRyhn2Ftk7KWfMUnQ43hFdojWEFRxhhRUm3/OFEdPa1r0KAvTTg9YQK57xTe0g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8" - } - }, - "node_modules/balanced-match": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", - "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "18 || 20 || >=22" - } - }, - "node_modules/base32-encode": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/base32-encode/-/base32-encode-1.2.0.tgz", - "integrity": "sha512-cHFU8XeRyx0GgmoWi5qHMCVRiqU6J3MHWxVgun7jggCBUpVzm1Ir7M9dYr2whjSNc3tFeXfQ/oZjQu/4u55h9A==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "to-data-view": "^1.1.0" - } - }, - "node_modules/base64-js": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/bplist-creator": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/bplist-creator/-/bplist-creator-0.0.8.tgz", - "integrity": "sha512-Za9JKzD6fjLC16oX2wsXfc+qBEhJBJB1YPInoAQpMLhDuj5aVOv1baGeIQSq1Fr3OCqzvsoQcSBSwGId/Ja2PA==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "stream-buffers": "~2.2.0" - } - }, - "node_modules/brace-expansion": { - "version": "5.0.6", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", - "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^4.0.2" - }, - "engines": { - "node": "18 || 20 || >=22" - } - }, - "node_modules/buffer-crc32": { - "version": "0.2.13", - "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", - "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "*" - } - }, - "node_modules/chownr": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", - "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", - "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=18" - } - }, - "node_modules/chrome-trace-event": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", - "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.0" - } - }, - "node_modules/cli-cursor": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-4.0.0.tgz", - "integrity": "sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg==", - "dev": true, - "license": "MIT", - "dependencies": { - "restore-cursor": "^4.0.0" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/cli-truncate": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-3.1.0.tgz", - "integrity": "sha512-wfOBkjXteqSnI59oPcJkcPl/ZmwvMMOj340qUIY1SKZCv0B9Cf4D4fAucRkIKQmsIuYK3x1rrgU7MeGRruiuiA==", - "dev": true, - "license": "MIT", - "dependencies": { - "slice-ansi": "^5.0.0", - "string-width": "^5.0.0" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/colorette": { - "version": "2.0.20", - "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", - "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", - "dev": true, - "license": "MIT" - }, - "node_modules/commander": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz", - "integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=16" - } - }, - "node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "dev": true, - "license": "MIT", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/cross-spawn/node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/cross-zip": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/cross-zip/-/cross-zip-4.0.1.tgz", - "integrity": "sha512-n63i0lZ0rvQ6FXiGQ+/JFCKAUyPFhLQYJIqKaa+tSJtfKeULF/IDNDAbdnSIxgS4NTuw2b0+lj8LzfITuq+ZxQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "engines": { - "node": ">=12.10" - } - }, - "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==", - "devOptional": true, - "license": "MIT" - }, - "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/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/ds-store": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/ds-store/-/ds-store-0.1.6.tgz", - "integrity": "sha512-kY21M6Lz+76OS3bnCzjdsJSF7LBpLYGCVfavW8TgQD2XkcqIZ86W0y9qUDZu6fp7SIZzqosMDW2zi7zVFfv4hw==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "bplist-creator": "~0.0.3", - "macos-alias": "~0.2.5", - "tn1150": "^0.1.0" - } - }, - "node_modules/eastasianwidth": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", - "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", - "dev": true, - "license": "MIT" - }, - "node_modules/electron": { - "version": "42.3.0", - "resolved": "https://registry.npmjs.org/electron/-/electron-42.3.0.tgz", - "integrity": "sha512-9ZiLdRXk+WDxW1OgIUz8J2rIQ5TYU9o629gCOjU48Q3dQiOmym7osWsH5Ubs/Jh4uuFLn6m6SBD2rmRXLAPz9g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@electron/get": "^5.0.0", - "@types/node": "^24.9.0", - "extract-zip": "^2.0.1" - }, - "bin": { - "electron": "cli.js", - "install-electron": "install.js" - }, - "engines": { - "node": ">= 22.12.0" - } - }, - "node_modules/electron-installer-dmg": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/electron-installer-dmg/-/electron-installer-dmg-5.0.1.tgz", - "integrity": "sha512-qOa1aAQdX57C+vzhDk3549dd/PRlNL4F8y736MTD1a43qptD+PvHY97Bo9gSf+OZ8iUWE7BrYSpk/FgLUe40EA==", - "dev": true, - "license": "Apache-2.0", - "optional": true, - "dependencies": { - "@types/appdmg": "^0.5.5", - "debug": "^4.3.2", - "minimist": "^1.2.7" - }, - "bin": { - "electron-installer-dmg": "dist/electron-installer-dmg-bin.js" - }, - "engines": { - "node": ">= 16" - }, - "optionalDependencies": { - "appdmg": "^0.6.4" - } - }, - "node_modules/emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "dev": true, - "license": "MIT" - }, - "node_modules/encode-utf8": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/encode-utf8/-/encode-utf8-1.0.3.tgz", - "integrity": "sha512-ucAnuBEhUK4boH2HjVYG5Q2mQyPorvv0u/ocS+zhdw0S8AlHYY+GOFhP1Gio5z4icpP2ivFSvhtFjQi8+T9ppw==", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/end-of-stream": { - "version": "1.4.5", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", - "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", - "dev": true, - "license": "MIT", - "dependencies": { - "once": "^1.4.0" - } - }, - "node_modules/env-paths": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-3.0.0.tgz", - "integrity": "sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/err-code": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", - "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", - "dev": true, - "license": "MIT" - }, - "node_modules/esbuild": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.0.tgz", - "integrity": "sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.28.0", - "@esbuild/android-arm": "0.28.0", - "@esbuild/android-arm64": "0.28.0", - "@esbuild/android-x64": "0.28.0", - "@esbuild/darwin-arm64": "0.28.0", - "@esbuild/darwin-x64": "0.28.0", - "@esbuild/freebsd-arm64": "0.28.0", - "@esbuild/freebsd-x64": "0.28.0", - "@esbuild/linux-arm": "0.28.0", - "@esbuild/linux-arm64": "0.28.0", - "@esbuild/linux-ia32": "0.28.0", - "@esbuild/linux-loong64": "0.28.0", - "@esbuild/linux-mips64el": "0.28.0", - "@esbuild/linux-ppc64": "0.28.0", - "@esbuild/linux-riscv64": "0.28.0", - "@esbuild/linux-s390x": "0.28.0", - "@esbuild/linux-x64": "0.28.0", - "@esbuild/netbsd-arm64": "0.28.0", - "@esbuild/netbsd-x64": "0.28.0", - "@esbuild/openbsd-arm64": "0.28.0", - "@esbuild/openbsd-x64": "0.28.0", - "@esbuild/openharmony-arm64": "0.28.0", - "@esbuild/sunos-x64": "0.28.0", - "@esbuild/win32-arm64": "0.28.0", - "@esbuild/win32-ia32": "0.28.0", - "@esbuild/win32-x64": "0.28.0" - } - }, - "node_modules/eventemitter3": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", - "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", - "dev": true, - "license": "MIT" - }, - "node_modules/execa": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", - "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "cross-spawn": "^6.0.0", - "get-stream": "^4.0.0", - "is-stream": "^1.1.0", - "npm-run-path": "^2.0.0", - "p-finally": "^1.0.0", - "signal-exit": "^3.0.0", - "strip-eof": "^1.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/execa/node_modules/cross-spawn": { - "version": "6.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.6.tgz", - "integrity": "sha512-VqCUuhcd1iB+dsv8gxPttb5iZh/D0iubSP21g36KXdEuf6I5JiioesUVjpCdHV9MZRUfVFlvwtIUyPfxo5trtw==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "nice-try": "^1.0.4", - "path-key": "^2.0.1", - "semver": "^5.5.0", - "shebang-command": "^1.2.0", - "which": "^1.2.9" - }, - "engines": { - "node": ">=4.8" - } - }, - "node_modules/execa/node_modules/get-stream": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", - "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "pump": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/execa/node_modules/path-key": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", - "integrity": "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/execa/node_modules/semver": { - "version": "5.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", - "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", - "dev": true, - "license": "ISC", - "optional": true, - "bin": { - "semver": "bin/semver" - } - }, - "node_modules/execa/node_modules/shebang-command": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", - "integrity": "sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "shebang-regex": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/execa/node_modules/shebang-regex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", - "integrity": "sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/execa/node_modules/signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "dev": true, - "license": "ISC", - "optional": true - }, - "node_modules/execa/node_modules/which": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", - "dev": true, - "license": "ISC", - "optional": true, - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "which": "bin/which" - } - }, - "node_modules/exponential-backoff": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.3.tgz", - "integrity": "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==", - "dev": true, - "license": "Apache-2.0" - }, - "node_modules/extract-zip": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", - "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "debug": "^4.1.1", - "get-stream": "^5.1.0", - "yauzl": "^2.10.0" - }, - "bin": { - "extract-zip": "cli.js" - }, - "engines": { - "node": ">= 10.17.0" - }, - "optionalDependencies": { - "@types/yauzl": "^2.9.1" - } - }, - "node_modules/fd-slicer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", - "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", - "dev": true, - "license": "MIT", - "dependencies": { - "pend": "~1.2.0" - } - }, - "node_modules/fdir": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, - "node_modules/filename-reserved-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/filename-reserved-regex/-/filename-reserved-regex-3.0.0.tgz", - "integrity": "sha512-hn4cQfU6GOT/7cFHXBqeBg2TbrMBgdD0kcjLhvSQYYwm3s4B6cjvBfb7nBALJLAXqmU5xajSa7X2NnUud/VCdw==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/filenamify": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/filenamify/-/filenamify-6.0.0.tgz", - "integrity": "sha512-vqIlNogKeyD3yzrm0yhRMQg8hOVwYcYRfjEoODd49iCprMn4HL85gK3HcykQE53EPIpX3HcAbGA5ELQv216dAQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "filename-reserved-regex": "^3.0.0" - }, - "engines": { - "node": ">=16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/flora-colossus": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/flora-colossus/-/flora-colossus-3.0.2.tgz", - "integrity": "sha512-Jk78K/Tzt6saxQPGChlJw69xuFGpWyTSAS8EdU0h/FyXwD2K46yNOXmo6nRHcZ9ooekyBAzMkwmiGNt7wOC5zg==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^4.4.1" - }, - "engines": { - "node": ">=22.12.0" - } - }, - "node_modules/fmix": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/fmix/-/fmix-0.1.0.tgz", - "integrity": "sha512-Y6hyofImk9JdzU8k5INtTXX1cu8LDlePWDFU5sftm9H+zKCr5SGrVjdhkvsim646cw5zD0nADj8oHyXMZmCZ9w==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "imul": "^1.0.0" - } - }, - "node_modules/fs-extra": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", - "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/fs-temp": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/fs-temp/-/fs-temp-1.2.1.tgz", - "integrity": "sha512-okTwLB7/Qsq82G6iN5zZJFsOfZtx2/pqrA7Hk/9fvy+c+eJS9CvgGXT2uNxwnI14BDY9L/jQPkaBgSvlKfSW9w==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "random-path": "^0.1.0" - } - }, - "node_modules/fs-xattr": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/fs-xattr/-/fs-xattr-0.3.1.tgz", - "integrity": "sha512-UVqkrEW0GfDabw4C3HOrFlxKfx0eeigfRne69FxSBdHIP8Qt5Sq6Pu3RM9KmMlkygtC4pPKkj5CiPO5USnj2GA==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "!win32" - ], - "engines": { - "node": ">=8.6.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==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/galactus": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/galactus/-/galactus-2.0.2.tgz", - "integrity": "sha512-HmKyTFGomdAchz4umx8MwBnrnfFmdpwiTyGA4ZOF7rya2Lmgbc9qate4yweInL+0gUBVImhaz12SBGpW3SY4Yg==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^4.4.1", - "flora-colossus": "^3.0.2" - }, - "engines": { - "node": ">=22.12.0" - } - }, - "node_modules/generate-function": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz", - "integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "is-property": "^1.0.2" - } - }, - "node_modules/generate-object-property": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/generate-object-property/-/generate-object-property-1.2.0.tgz", - "integrity": "sha512-TuOwZWgJ2VAMEGJvAyPWvpqxSANF0LDpmyHauMjFYzaACvn+QTT/AZomvPCzVBV7yDN3OmwHQ5OvHaeLKre3JQ==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "is-property": "^1.0.0" - } - }, - "node_modules/get-stream": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", - "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", - "dev": true, - "license": "MIT", - "dependencies": { - "pump": "^3.0.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/glob": { - "version": "13.0.6", - "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", - "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "minimatch": "^10.2.2", - "minipass": "^7.1.3", - "path-scurry": "^2.0.2" - }, - "engines": { - "node": "18 || 20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/graceful-fs": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/image-size": { - "version": "0.7.5", - "resolved": "https://registry.npmjs.org/image-size/-/image-size-0.7.5.tgz", - "integrity": "sha512-Hiyv+mXHfFEP7LzUL/llg9RwFxxY+o9N3JVLIeG5E7iFIFAalxvRU9UZthBdYDEVnzHMgjnKJPPpay5BWf1g9g==", - "dev": true, - "license": "MIT", - "optional": true, - "bin": { - "image-size": "bin/image-size.js" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/imul": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/imul/-/imul-1.0.1.tgz", - "integrity": "sha512-WFAgfwPLAjU66EKt6vRdTlKj4nAgIDQzh29JonLa4Bqtl6D8JrIMvWjCnx7xEjVNmP3U0fM5o8ZObk7d0f62bA==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-fullwidth-code-point": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-4.0.0.tgz", - "integrity": "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-my-ip-valid": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-my-ip-valid/-/is-my-ip-valid-1.0.1.tgz", - "integrity": "sha512-jxc8cBcOWbNK2i2aTkCZP6i7wkHF1bqKFrwEHuN5Jtg5BSaZHUZQ/JTOJwoV41YvHnOaRyWWh72T/KvfNz9DJg==", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/is-my-json-valid": { - "version": "2.20.6", - "resolved": "https://registry.npmjs.org/is-my-json-valid/-/is-my-json-valid-2.20.6.tgz", - "integrity": "sha512-1JQwulVNjx8UqkPE/bqDaxtH4PXCe/2VRh/y3p99heOV87HG4Id5/VfDswd+YiAfHcRTfDlWgISycnHuhZq1aw==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "generate-function": "^2.0.0", - "generate-object-property": "^1.1.0", - "is-my-ip-valid": "^1.0.0", - "jsonpointer": "^5.0.0", - "xtend": "^4.0.0" - } - }, - "node_modules/is-property": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", - "integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/is-stream": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", - "integrity": "sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/isbinaryfile": { - "version": "4.0.10", - "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-4.0.10.tgz", - "integrity": "sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 8.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/gjtorikian/" - } - }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true, - "license": "ISC" - }, - "node_modules/jiti": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", - "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", - "dev": true, - "license": "MIT", - "bin": { - "jiti": "lib/jiti-cli.mjs" - } - }, - "node_modules/jsonfile": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", - "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/jsonpointer": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-5.0.1.tgz", - "integrity": "sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/junk": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/junk/-/junk-4.0.1.tgz", - "integrity": "sha512-Qush0uP+G8ZScpGMZvHUiRfI0YBWuB3gVBYlI0v0vvOJt5FLicco+IkP0a50LqTTQhmts/m6tP5SWE+USyIvcQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12.20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/lightningcss": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", - "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", - "dev": true, - "license": "MPL-2.0", - "dependencies": { - "detect-libc": "^2.0.3" - }, - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - }, - "optionalDependencies": { - "lightningcss-android-arm64": "1.32.0", - "lightningcss-darwin-arm64": "1.32.0", - "lightningcss-darwin-x64": "1.32.0", - "lightningcss-freebsd-x64": "1.32.0", - "lightningcss-linux-arm-gnueabihf": "1.32.0", - "lightningcss-linux-arm64-gnu": "1.32.0", - "lightningcss-linux-arm64-musl": "1.32.0", - "lightningcss-linux-x64-gnu": "1.32.0", - "lightningcss-linux-x64-musl": "1.32.0", - "lightningcss-win32-arm64-msvc": "1.32.0", - "lightningcss-win32-x64-msvc": "1.32.0" - } - }, - "node_modules/lightningcss-android-arm64": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", - "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-darwin-arm64": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", - "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-darwin-x64": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", - "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-freebsd-x64": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", - "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-arm-gnueabihf": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", - "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-arm64-gnu": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", - "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-arm64-musl": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", - "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", - "cpu": [ - "arm64" - ], - "dev": true, - "libc": [ - "musl" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-x64-gnu": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", - "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", - "cpu": [ - "x64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-x64-musl": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", - "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", - "cpu": [ - "x64" - ], - "dev": true, - "libc": [ - "musl" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-win32-arm64-msvc": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", - "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-win32-x64-msvc": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", - "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/listr2": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/listr2/-/listr2-7.0.2.tgz", - "integrity": "sha512-rJysbR9GKIalhTbVL2tYbF2hVyDnrf7pFUZBwjPaMIdadYHmeT+EVi/Bu3qd7ETQPahTotg2WRCatXwRBW554g==", - "dev": true, - "license": "MIT", - "dependencies": { - "cli-truncate": "^3.1.0", - "colorette": "^2.0.20", - "eventemitter3": "^5.0.1", - "log-update": "^5.0.1", - "rfdc": "^1.3.0", - "wrap-ansi": "^8.1.0" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/listr2/node_modules/ansi-regex": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", - "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/listr2/node_modules/ansi-styles": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", - "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/listr2/node_modules/strip-ansi": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", - "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.2.2" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/listr2/node_modules/wrap-ansi": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", - "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.1.0", - "string-width": "^5.0.1", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/log-update": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/log-update/-/log-update-5.0.1.tgz", - "integrity": "sha512-5UtUDQ/6edw4ofyljDNcOVJQ4c7OjDro4h3y8e1GQL5iYElYclVHJ3zeWchylvMaKnDbDilC8irOVyexnA/Slw==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-escapes": "^5.0.0", - "cli-cursor": "^4.0.0", - "slice-ansi": "^5.0.0", - "strip-ansi": "^7.0.1", - "wrap-ansi": "^8.0.1" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/log-update/node_modules/ansi-escapes": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-5.0.0.tgz", - "integrity": "sha512-5GFMVX8HqE/TB+FuBJGuO5XG0WrsA6ptUqoODaT/n9mmUaZFkqnBueB4leqGBCmrUHnCnC4PCZTCd0E7QQ83bA==", - "dev": true, - "license": "MIT", - "dependencies": { - "type-fest": "^1.0.2" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/log-update/node_modules/ansi-regex": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", - "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/log-update/node_modules/ansi-styles": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", - "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/log-update/node_modules/strip-ansi": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", - "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.2.2" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/log-update/node_modules/type-fest": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-1.4.0.tgz", - "integrity": "sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==", - "dev": true, - "license": "(MIT OR CC0-1.0)", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/log-update/node_modules/wrap-ansi": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", - "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.1.0", - "string-width": "^5.0.1", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/lru-cache": { - "version": "11.5.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.1.tgz", - "integrity": "sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==", - "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/macos-alias": { - "version": "0.2.12", - "resolved": "https://registry.npmjs.org/macos-alias/-/macos-alias-0.2.12.tgz", - "integrity": "sha512-yiLHa7cfJcGRFq4FrR4tMlpNHb4Vy4mWnpajlSSIFM5k4Lv8/7BbbDLzCAVogWNl0LlLhizRp1drXv0hK9h0Yw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "dependencies": { - "nan": "^2.4.0" - } - }, - "node_modules/mimic-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", - "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/minimatch": { - "version": "10.2.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", - "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "brace-expansion": "^5.0.5" - }, - "engines": { - "node": "18 || 20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/minimist": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "dev": true, - "license": "MIT", - "optional": true, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/minipass": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", - "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", - "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, - "node_modules/minizlib": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", - "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", - "dev": true, - "license": "MIT", - "dependencies": { - "minipass": "^7.1.2" - }, - "engines": { - "node": ">= 18" - } - }, - "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/murmur-32": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/murmur-32/-/murmur-32-0.2.0.tgz", - "integrity": "sha512-ZkcWZudylwF+ir3Ld1n7gL6bI2mQAzXvSobPwVtu8aYi2sbXeipeSkdcanRLzIofLcM5F53lGaKm2dk7orBi7Q==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "encode-utf8": "^1.0.3", - "fmix": "^0.1.0", - "imul": "^1.0.0" - } - }, - "node_modules/nan": { - "version": "2.27.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.27.0.tgz", - "integrity": "sha512-hC+0LidcL3XE4rp1C4H54KujgXKzbfyTngZTwBByQxsOxCEKZT0MPQ4hOKUH2jU1OYstqdDH4onyHPDzcV0XdQ==", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/nanoid": { - "version": "3.3.12", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", - "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, - "node_modules/nice-try": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", - "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/node-abi": { - "version": "4.31.0", - "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-4.31.0.tgz", - "integrity": "sha512-Erq5w/t3syw3s4sDsUaX4QttIdBPsGKTT1DTRsCkTonGggczhlDKm/wDX3o+HPJpQ41EjXCbcmXf0tgr5YZJXw==", - "dev": true, - "license": "MIT", - "dependencies": { - "semver": "^7.6.3" - }, - "engines": { - "node": ">=22.12.0" - } - }, - "node_modules/node-api-version": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/node-api-version/-/node-api-version-0.2.1.tgz", - "integrity": "sha512-2xP/IGGMmmSQpI1+O/k72jF/ykvZ89JeuKX3TLJAYPDVLUalrshrLHkeVcCCZqG/eEa635cr8IBYzgnDvM2O8Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "semver": "^7.3.5" - } - }, - "node_modules/node-gyp": { - "version": "12.3.0", - "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-12.3.0.tgz", - "integrity": "sha512-QNcUWM+HgJplcPzBvFBZ9VXacyGZ4+VTOb80PwWR+TlVzoHbRKULNEzpRsnaoxG3Wzr7Qh7BYxGDU3CbKib2Yg==", - "dev": true, - "license": "MIT", - "dependencies": { - "env-paths": "^2.2.0", - "exponential-backoff": "^3.1.1", - "graceful-fs": "^4.2.6", - "nopt": "^9.0.0", - "proc-log": "^6.0.0", - "semver": "^7.3.5", - "tar": "^7.5.4", - "tinyglobby": "^0.2.12", - "undici": "^6.25.0", - "which": "^6.0.0" - }, - "bin": { - "node-gyp": "bin/node-gyp.js" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/node-gyp/node_modules/env-paths": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", - "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/node-gyp/node_modules/undici": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-6.26.0.tgz", - "integrity": "sha512-4yqz8a3n5HmGTlsbADNtr/dJlhkh/55Rq798G6ibiULcXbDtaLpTl1pvdqcbFfeoj3iSi52lePFM7h9H21cw/A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18.17" - } - }, - "node_modules/nopt": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-9.0.0.tgz", - "integrity": "sha512-Zhq3a+yFKrYwSBluL4H9XP3m3y5uvQkB/09CwDruCiRmR/UJYnn9W4R48ry0uGC70aeTPKLynBtscP9efFFcPw==", - "dev": true, - "license": "ISC", - "dependencies": { - "abbrev": "^4.0.0" - }, - "bin": { - "nopt": "bin/nopt.js" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/npm-run-path": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", - "integrity": "sha512-lJxZYlT4DW/bRUtFh1MQIWqmLwQfAxnqWG4HhEdjMlkrJYnJn0Jrr2u3mgxqaWsdiBc76TYkTG/mhrnYTuzfHw==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "path-key": "^2.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/npm-run-path/node_modules/path-key": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", - "integrity": "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, - "license": "ISC", - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/onetime": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", - "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "mimic-fn": "^2.1.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-finally": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", - "integrity": "sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/parse-author": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/parse-author/-/parse-author-2.0.0.tgz", - "integrity": "sha512-yx5DfvkN8JsHL2xk2Os9oTia467qnvRgey4ahSm2X8epehBLx/gWLcy5KI+Y36ful5DzGbCS6RazqZGgy1gHNw==", - "dev": true, - "license": "MIT", - "dependencies": { - "author-regex": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/parse-color": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/parse-color/-/parse-color-1.0.0.tgz", - "integrity": "sha512-fuDHYgFHJGbpGMgw9skY/bj3HL/Jrn4l/5rSspy00DoT4RyLnDcRvPxdZ+r6OFwIsgAuhDh4I09tAId4mI12bw==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "color-convert": "~0.5.0" - } - }, - "node_modules/parse-color/node_modules/color-convert": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-0.5.3.tgz", - "integrity": "sha512-RwBeO/B/vZR3dfKL1ye/vx8MHZ40ugzpyfeVG5GsiuGnrlMWe2o8wxBbLCpw9CsxV+wHuzYlCiWnybrIA0ling==", - "dev": true, - "optional": true - }, - "node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-scurry": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", - "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "lru-cache": "^11.0.0", - "minipass": "^7.1.2" - }, - "engines": { - "node": "18 || 20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/pe-library": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/pe-library/-/pe-library-1.0.1.tgz", - "integrity": "sha512-nh39Mo1eGWmZS7y+mK/dQIqg7S1lp38DpRxkyoHf0ZcUs/HDc+yyTjuOtTvSMZHmfSLuSQaX945u05Y2Q6UWZg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14", - "npm": ">=7" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/jet2jet" - } - }, - "node_modules/pend": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", - "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", - "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==", - "dev": true, - "license": "ISC" - }, - "node_modules/picomatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", - "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/plist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/plist/-/plist-3.1.1.tgz", - "integrity": "sha512-ZIfcLJC+7E7FBFnDxm9MPmt7D+DidyQ26lewieO75AdhA2ayMtsJSES0iWzqJQbcVRSrTufQoy0DR94xHue0oA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@xmldom/xmldom": "^0.9.10", - "base64-js": "^1.5.1", - "xmlbuilder": "^15.1.1" - }, - "engines": { - "node": ">=10.4.0" - } - }, - "node_modules/postcss": { - "version": "8.5.15", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", - "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "nanoid": "^3.3.12", - "picocolors": "^1.1.1", - "source-map-js": "^1.2.1" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, - "node_modules/postject": { - "version": "1.0.0-alpha.6", - "resolved": "https://registry.npmjs.org/postject/-/postject-1.0.0-alpha.6.tgz", - "integrity": "sha512-b9Eb8h2eVqNE8edvKdwqkrY6O7kAwmI8kcnBv1NScolYJbo59XUF0noFq+lxbC1yN20bmC0WBEbDC5H/7ASb0A==", - "dev": true, - "license": "MIT", - "dependencies": { - "commander": "^9.4.0" - }, - "bin": { - "postject": "dist/cli.js" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/postject/node_modules/commander": { - "version": "9.5.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", - "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.20.0 || >=14" - } - }, - "node_modules/proc-log": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-6.1.0.tgz", - "integrity": "sha512-iG+GYldRf2BQ0UDUAd6JQ/RwzaQy6mXmsk/IzlYyal4A4SNFw54MeH4/tLkF4I5WoWG9SQwuqWzS99jaFQHBuQ==", - "dev": true, - "license": "ISC", - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/progress": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", - "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/promise-retry": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", - "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", - "dev": true, - "license": "MIT", - "dependencies": { - "err-code": "^2.0.2", - "retry": "^0.12.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/pump": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", - "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", - "dev": true, - "license": "MIT", - "dependencies": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, - "node_modules/random-path": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/random-path/-/random-path-0.1.2.tgz", - "integrity": "sha512-4jY0yoEaQ5v9StCl5kZbNIQlg1QheIDBrdkDn53EynpPb9FgO6//p3X/tgMnrC45XN6QZCzU1Xz/+pSSsJBpRw==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "base32-encode": "^0.1.0 || ^1.0.0", - "murmur-32": "^0.1.0 || ^0.2.0" - } - }, - "node_modules/react": { - "version": "19.2.6", - "resolved": "https://registry.npmjs.org/react/-/react-19.2.6.tgz", - "integrity": "sha512-sfWGGfavi0xr8Pg0sVsyHMAOziVYKgPLNrS7ig+ivMNb3wbCBw3KxtflsGBAwD3gYQlE/AEZsTLgToRrSCjb0Q==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/react-dom": { - "version": "19.2.6", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.6.tgz", - "integrity": "sha512-0prMI+hvBbPjsWnxDLxlCGyM8PN6UuWjEUCYmZhO67xIV9Xasa/r/vDnq+Xyq4Lo27g8QSbO5YzARu0D1Sps3g==", - "license": "MIT", - "dependencies": { - "scheduler": "^0.27.0" - }, - "peerDependencies": { - "react": "^19.2.6" - } - }, - "node_modules/read-binary-file-arch": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/read-binary-file-arch/-/read-binary-file-arch-1.0.6.tgz", - "integrity": "sha512-BNg9EN3DD3GsDXX7Aa8O4p92sryjkmzYYgmgTAc6CA4uGLEDzFfxOxugu21akOxpcXHiEgsYkC6nPsQvLLLmEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^4.3.4" - }, - "bin": { - "read-binary-file-arch": "cli.js" - } - }, - "node_modules/repeat-string": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", - "integrity": "sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/resedit": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/resedit/-/resedit-2.0.3.tgz", - "integrity": "sha512-oTeemxwoMuxxTYxXUwjkrOPfngTQehlv0/HoYFNkB4uzsP1Un1A9nI8JQKGOFkxpqkC7qkMs0lUsGrvUlbLNUA==", - "dev": true, - "license": "MIT", - "dependencies": { - "pe-library": "^1.0.1" - }, - "engines": { - "node": ">=14", - "npm": ">=7" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/jet2jet" - } - }, - "node_modules/restore-cursor": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-4.0.0.tgz", - "integrity": "sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg==", - "dev": true, - "license": "MIT", - "dependencies": { - "onetime": "^5.1.0", - "signal-exit": "^3.0.2" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + "node": ">= 12.0.0" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/restore-cursor/node_modules/signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/retry": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", - "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "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/rolldown": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.2.tgz", - "integrity": "sha512-oZx5zVDtVB44AW3eaifgDml1gWRDZGvjcfdxonE4swNPG98PrrXjaO/KrnUjzlMnztCCRVlUueA1kCXhARGk6g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@oxc-project/types": "=0.132.0", - "@rolldown/pluginutils": "^1.0.0" - }, - "bin": { - "rolldown": "bin/cli.mjs" - }, - "engines": { - "node": "^20.19.0 || >=22.12.0" - }, - "optionalDependencies": { - "@rolldown/binding-android-arm64": "1.0.2", - "@rolldown/binding-darwin-arm64": "1.0.2", - "@rolldown/binding-darwin-x64": "1.0.2", - "@rolldown/binding-freebsd-x64": "1.0.2", - "@rolldown/binding-linux-arm-gnueabihf": "1.0.2", - "@rolldown/binding-linux-arm64-gnu": "1.0.2", - "@rolldown/binding-linux-arm64-musl": "1.0.2", - "@rolldown/binding-linux-ppc64-gnu": "1.0.2", - "@rolldown/binding-linux-s390x-gnu": "1.0.2", - "@rolldown/binding-linux-x64-gnu": "1.0.2", - "@rolldown/binding-linux-x64-musl": "1.0.2", - "@rolldown/binding-openharmony-arm64": "1.0.2", - "@rolldown/binding-wasm32-wasi": "1.0.2", - "@rolldown/binding-win32-arm64-msvc": "1.0.2", - "@rolldown/binding-win32-x64-msvc": "1.0.2" - } - }, - "node_modules/scheduler": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", - "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", - "license": "MIT" - }, - "node_modules/semver": { - "version": "7.8.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.1.tgz", - "integrity": "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz", + "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "MIT", - "dependencies": { - "shebang-regex": "^3.0.0" - }, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=8" + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz", + "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "MIT", + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=8" + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/slice-ansi": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-5.0.0.tgz", - "integrity": "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==", + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz", + "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", + "cpu": [ + "x64" + ], "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.0.0", - "is-fullwidth-code-point": "^4.0.0" - }, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=12" + "node": ">= 12.0.0" }, "funding": { - "url": "https://github.com/chalk/slice-ansi?sponsor=1" + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/slice-ansi/node_modules/ansi-styles": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", - "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz", + "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", + "cpu": [ + "x64" + ], "dev": true, - "license": "MIT", + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=12" + "node": ">= 12.0.0" }, "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/source-map-js": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz", + "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "BSD-3-Clause", + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": ">=0.10.0" + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/stream-buffers": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/stream-buffers/-/stream-buffers-2.2.0.tgz", - "integrity": "sha512-uyQK/mx5QjHun80FLJTfaWE7JtwfRMKBLkMne6udYOmvH0CawotVa7TfgYHzAnpphn4+TweIx1QKMnRIbipmUg==", + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz", + "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==", + "cpu": [ + "x64" + ], "dev": true, - "license": "Unlicense", + "license": "MPL-2.0", "optional": true, + "os": [ + "win32" + ], "engines": { - "node": ">= 0.10.0" + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "node_modules/nanoid": { + "version": "3.3.18", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz", + "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==", "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], "license": "MIT", - "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" + "bin": { + "nanoid": "bin/nanoid.cjs" }, "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, - "node_modules/string-width/node_modules/ansi-regex": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", - "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "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, + "license": "ISC" + }, + "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" }, "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" + "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/string-width/node_modules/strip-ansi": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", - "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "node_modules/postcss": { + "version": "8.5.26", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz", + "integrity": "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==", "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], "license": "MIT", "dependencies": { - "ansi-regex": "^6.2.2" + "nanoid": "^3.3.17", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" }, "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" + "node": "^10 || ^12 || >=14" } }, - "node_modules/strip-eof": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", - "integrity": "sha512-7FCwGGmx8mD5xQd3RPUvnSpUXHM3BWuzjtpD4TXsfcZ9EL4azvVVUscFYwD9nx8Kh+uCBC00XBtAykoMHwTh8Q==", + "node_modules/rolldown": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.6.tgz", + "integrity": "sha512-vMM4q3aixf46GiF1Kok8jDPFsEpXgFWGjUHXNkNHNm+Y2adXAG2dbX91jkti3i0ZRsOlcmbuzAz1poObSHCmUA==", "dev": true, "license": "MIT", - "optional": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/sumchecker": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/sumchecker/-/sumchecker-3.0.1.tgz", - "integrity": "sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg==", - "dev": true, - "license": "Apache-2.0", "dependencies": { - "debug": "^4.1.0" + "@oxc-project/types": "=0.147.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" }, "engines": { - "node": ">= 8.0" + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm-eabi": "1.2.6", + "@rolldown/binding-android-arm64": "1.2.6", + "@rolldown/binding-darwin-arm64": "1.2.6", + "@rolldown/binding-darwin-x64": "1.2.6", + "@rolldown/binding-freebsd-x64": "1.2.6", + "@rolldown/binding-linux-arm-gnueabihf": "1.2.6", + "@rolldown/binding-linux-arm64-gnu": "1.2.6", + "@rolldown/binding-linux-arm64-musl": "1.2.6", + "@rolldown/binding-linux-ppc64-gnu": "1.2.6", + "@rolldown/binding-linux-s390x-gnu": "1.2.6", + "@rolldown/binding-linux-x64-gnu": "1.2.6", + "@rolldown/binding-linux-x64-musl": "1.2.6", + "@rolldown/binding-openharmony-arm64": "1.2.6", + "@rolldown/binding-win32-arm64-msvc": "1.2.6", + "@rolldown/binding-win32-x64-msvc": "1.2.6" } }, - "node_modules/tar": { - "version": "7.5.15", - "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.15.tgz", - "integrity": "sha512-dzGK0boVlC4W5QFuQN1EFSl3bIDYsk7Tj40U6eIBnK2k/8ml7TZ5agbI5j5+qnoVcAA+rNtBml8SEiLxZpNqRQ==", + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/fs-minipass": "^4.0.0", - "chownr": "^3.0.0", - "minipass": "^7.1.2", - "minizlib": "^3.1.0", - "yallist": "^5.0.0" - }, + "license": "BSD-3-Clause", "engines": { - "node": ">=18" + "node": ">=0.10.0" } }, "node_modules/tinyglobby": { @@ -3743,34 +747,6 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, - "node_modules/tn1150": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/tn1150/-/tn1150-0.1.0.tgz", - "integrity": "sha512-DbplOfQFkqG5IHcDyyrs/lkvSr3mPUVsFf/RbDppOshs22yTPnSJWEe6FkYd1txAwU/zcnR905ar2fi4kwF29w==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "unorm": "^1.4.1" - }, - "engines": { - "node": ">=0.12" - } - }, - "node_modules/to-data-view": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/to-data-view/-/to-data-view-1.1.0.tgz", - "integrity": "sha512-1eAdufMg6mwgmlojAx3QeMnzB/BTVp7Tbndi3U7ftcT2zCZadjxkkmLmd97zmaxWi+sgGcgWrokmpEoy0Dn0vQ==", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "license": "0BSD" - }, "node_modules/typescript": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", @@ -3785,70 +761,18 @@ "node": ">=14.17" } }, - "node_modules/undici": { - "version": "7.26.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-7.26.0.tgz", - "integrity": "sha512-3O9Tf67pGhgOv9jM35AbhkXAKi13f3oy3aE4CSgr+TckGeY+/iu97ZXN+J7DpHPzLbVApFd1IFhcnBjREYXYcg==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=20.18.1" - } - }, - "node_modules/undici-types": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", - "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", - "dev": true, - "license": "MIT" - }, - "node_modules/universalify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/unorm": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/unorm/-/unorm-1.6.0.tgz", - "integrity": "sha512-b2/KCUlYZUeA7JFUuRJZPUtr4gZvBh7tavtv4fvk4+KV9pfGiR6CQAQAWl49ZpR3ts2dk4FYkP7EIgDJoiOLDA==", - "dev": true, - "license": "MIT or GPL-2.0", - "optional": true, - "engines": { - "node": ">= 0.4.0" - } - }, - "node_modules/uuid": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-14.0.0.tgz", - "integrity": "sha512-Qo+uWgilfSmAhXCMav1uYFynlQO7fMFiMVZsQqZRMIXp0O7rR7qjkj+cPvBHLgBqi960QCoo/PH2/6ZtVqKvrg==", - "funding": [ - "https://github.com/sponsors/broofa", - "https://github.com/sponsors/ctavan" - ], - "license": "MIT", - "bin": { - "uuid": "dist-node/bin/uuid" - } - }, "node_modules/vite": { - "version": "8.0.14", - "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.14.tgz", - "integrity": "sha512-s4BJJ+5y1pYL6Otw51FHhVJQhPnuRinKig64g/1+EUNaJsd3gCKdD31IPFvswUgW9/60QT9oFHbZHbQK5imcxw==", + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.2.2.tgz", + "integrity": "sha512-cFKLV/PRgAUlIRm5WjMjJ86jrftzpqcgH+Us+DS8mI3CDNiH30Whrz8uHL3+MOLPAgqbMBAqWdAHAphOAM+z/Q==", "dev": true, "license": "MIT", "dependencies": { - "lightningcss": "^1.32.0", - "picomatch": "^4.0.4", - "postcss": "^8.5.15", - "rolldown": "1.0.2", - "tinyglobby": "^0.2.16" + "lightningcss": "^1.33.0", + "picomatch": "^4.0.5", + "postcss": "^8.5.26", + "rolldown": "~1.2.4", + "tinyglobby": "^0.2.17" }, "bin": { "vite": "bin/vite.js" @@ -3864,7 +788,7 @@ }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", - "@vitejs/devtools": "^0.1.18", + "@vitejs/devtools": "^0.4.0 || ^0.5.0", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", @@ -3914,100 +838,6 @@ "optional": true } } - }, - "node_modules/which": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/which/-/which-6.0.1.tgz", - "integrity": "sha512-oGLe46MIrCRqX7ytPUf66EAYvdeMIZYn3WaocqqKZAxrBpkqHfL/qvTyJ/bTk5+AqHCjXmrv3CEWgy368zhRUg==", - "dev": true, - "license": "ISC", - "dependencies": { - "isexe": "^4.0.0" - }, - "bin": { - "node-which": "bin/which.js" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/which/node_modules/isexe": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-4.0.0.tgz", - "integrity": "sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw==", - "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=20" - } - }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/xmlbuilder": { - "version": "15.1.1", - "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-15.1.1.tgz", - "integrity": "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.0" - } - }, - "node_modules/xtend": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", - "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=0.4" - } - }, - "node_modules/yallist": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", - "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", - "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=18" - } - }, - "node_modules/yargs-parser": { - "version": "22.0.0", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-22.0.0.tgz", - "integrity": "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==", - "dev": true, - "license": "ISC", - "engines": { - "node": "^20.19.0 || ^22.12.0 || >=23" - } - }, - "node_modules/yauzl": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", - "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", - "dev": true, - "license": "MIT", - "dependencies": { - "buffer-crc32": "~0.2.3", - "fd-slicer": "~1.1.0" - } - }, - "node_modules/zod": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", - "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/colinhacks" - } } } } diff --git a/package.json b/package.json index 46069a3..8558029 100644 --- a/package.json +++ b/package.json @@ -1,37 +1,15 @@ { "name": "distance-client", "private": true, - "version": "0.1.0", - "description": "Remote Desktop Client for Distance", - "main": "dist/main/main.js", + "version": "0.2.0", + "description": "Remote Desktop Client for Distance (pure web)", + "type": "module", "scripts": { "dev": "vite --host", "build": "vite build", - "dev-app": "node scripts/build-main.mjs && electron .", - "build-app": "vite build && node scripts/build-main.mjs", - "start": "electron-forge start", - "package": "npm run build-app && electron-forge package", - "make": "npm run build-app && electron-forge make", - "make:arm64": "npm run build-app && electron-forge make --arch arm64", - "make:x64": "npm run build-app && electron-forge make --arch x64" - }, - "dependencies": { - "@moq/hang": "^0.2.7", - "@moq/net": "^0.1.2", - "@moq/watch": "^0.2.13", - "react": "^19.2.0", - "react-dom": "^19.2.0", - "uuid": "^14.0.0" + "preview": "vite preview" }, "devDependencies": { - "@electron-forge/cli": "^8.0.0-alpha.9", - "@electron-forge/maker-dmg": "^8.0.0-alpha.9", - "@electron-forge/maker-zip": "^8.0.0-alpha.9", - "@types/react": "^19.2.0", - "@types/react-dom": "^19.2.0", - "@vitejs/plugin-react": "^6.0.0", - "electron": "^42.3.0", - "esbuild": "^0.28.0", "typescript": "^6.0.0", "vite": "^8.0.0" } diff --git a/scripts/build-main.mjs b/scripts/build-main.mjs deleted file mode 100644 index 59d552f..0000000 --- a/scripts/build-main.mjs +++ /dev/null @@ -1,12 +0,0 @@ -import { build } from 'esbuild' - -await build({ - entryPoints: ['src/main/main.ts', 'src/main/preload.ts'], - outdir: 'dist/main', - bundle: true, - platform: 'node', - format: 'cjs', - external: ['electron'], - sourcemap: true, - minify: false, -}) diff --git a/src/decoder.ts b/src/decoder.ts new file mode 100644 index 0000000..2a9b933 --- /dev/null +++ b/src/decoder.ts @@ -0,0 +1,184 @@ +// H264 (Annex B) decoder on the platform WebCodecs VideoDecoder, drawing onto a +// . The agent sends raw ffmpeg H.264 Annex B bytes (00 00 00 01 / +// 00 00 01 start codes) on the unidirectional video stream, so the byte stream +// is split into NAL units and each is fed to the decoder. +export class Decoder { + private videoDecoder: VideoDecoder | null = null + private canvas: HTMLCanvasElement + private ctx: CanvasRenderingContext2D + private buf: Uint8Array = new Uint8Array(0) + private dts = 0 + private sps: Uint8Array | null = null + private pps: Uint8Array | null = null + + private frameCount = 0 + private fpsWindowStart = performance.now() + private fps = 0 + + public onFps: ((fps: number) => void) | null = null + public onFirstFrame: (() => void) | null = null + private firstFrameDrawn = false + + constructor(canvas: HTMLCanvasElement) { + this.canvas = canvas + const ctx = canvas.getContext('2d', { alpha: false }) + if (!ctx) throw new Error('2d canvas context unavailable') + this.ctx = ctx + if (typeof VideoDecoder === 'undefined') { + throw new Error('WebCodecs VideoDecoder is not available in this browser') + } + } + + // The agent encodes exclusively as H.264, so the codec string it reports in + // `started` is informational only. + configure(_codec: string, width?: number, height?: number): void { + if (this.videoDecoder && this.videoDecoder.state !== 'closed') { + try { this.videoDecoder.reset() } catch { /* ignore */ } + } + this.videoDecoder = new VideoDecoder({ + output: (frame) => this.onFrame(frame), + error: (e) => console.error('[decoder] error', e) + }) + const cfg: VideoDecoderConfig = { codec: 'avc1.42E01E' } + if (width && height) { + cfg.codedWidth = width + cfg.codedHeight = height + } + this.videoDecoder.configure(cfg) + } + + private onFrame(frame: VideoFrame): void { + const w = frame.displayWidth + const h = frame.displayHeight + if (this.canvas.width !== w || this.canvas.height !== h) { + this.canvas.width = w + this.canvas.height = h + } + this.ctx.drawImage(frame as unknown as CanvasImageSource, 0, 0, w, h) + frame.close() + + this.frameCount++ + if (!this.firstFrameDrawn) { + this.firstFrameDrawn = true + this.onFirstFrame?.() + } + } + + feed(chunk: Uint8Array): void { + if (!this.videoDecoder || this.videoDecoder.state !== 'configured') return + this.buf = concat(this.buf, chunk) + let start = 0 + let i = 0 + const buf = this.buf + while (i + 2 < buf.length) { + if (buf[i] === 0 && buf[i + 1] === 0) { + if (i + 3 < buf.length && buf[i + 2] === 0 && buf[i + 3] === 1) { + if (start < i) { + this.decodeNal(buf.subarray(start, i)) + } + start = i + 4 + i = start + continue + } else if (buf[i + 2] === 1) { + if (start < i) { + this.decodeNal(buf.subarray(start, i)) + } + start = i + 3 + i = start + continue + } + } + i++ + } + this.buf = buf.subarray(start) + this.tickFps() + } + + private decodeNal(nal: Uint8Array): void { + if (nal.length === 0) return + const nalType = nal[0] & 0x1f + if (nalType === 7) { + this.sps = nal.slice() + return + } + if (nalType === 8) { + this.pps = nal.slice() + return + } + let data: Uint8Array + let type: 'key' | 'delta' + if (nalType === 5) { + type = 'key' + if (this.sps && this.pps) { + data = annexBConcat([this.sps, this.pps, nal]) + } else { + data = nal.slice() + } + } else if (nalType === 6) { + return + } else { + type = 'delta' + data = nal.slice() + } + this.dts += 33333 + try { + this.videoDecoder!.decode( + new EncodedVideoChunk({ type, timestamp: this.dts, data: data as unknown as BufferSource }) + ) + } catch (e) { + if (!(e instanceof DOMException && e.name === 'InvalidStateError')) { + console.warn('[decoder] decode threw', e) + } + } + } + + private tickFps(): void { + const now = performance.now() + const dt = (now - this.fpsWindowStart) / 1000 + if (dt >= 1) { + this.fps = Math.round(this.frameCount / dt) + this.frameCount = 0 + this.fpsWindowStart = now + this.onFps?.(this.fps) + } + } + + get currentFps(): number { + return this.fps + } + + reset(): void { + this.buf = new Uint8Array(0) + this.dts = 0 + this.sps = null + this.pps = null + try { this.videoDecoder?.reset() } catch { /* ignore */ } + } + + close(): void { + try { this.videoDecoder?.close() } catch { /* ignore */ } + this.videoDecoder = null + } +} + +function concat(a: Uint8Array, b: Uint8Array): Uint8Array { + const out = new Uint8Array(a.length + b.length) + out.set(a, 0) + out.set(b, a.length) + return out +} + +function annexBConcat(nals: Uint8Array[]): Uint8Array { + const sc = new Uint8Array([0, 0, 0, 1]) + let total = 0 + for (const n of nals) total += 4 + n.length + const out = new Uint8Array(total) + let off = 0 + for (const n of nals) { + out.set(sc, off) + off += 4 + out.set(n, off) + off += n.length + } + return out +} diff --git a/src/input.ts b/src/input.ts new file mode 100644 index 0000000..76a067e --- /dev/null +++ b/src/input.ts @@ -0,0 +1,143 @@ +import type { InputMessage } from './types' + +type Send = (msg: InputMessage) => void + +/** + * Captures local input and serializes it to the control bidi stream. + * + * - Pointer Lock gives relative mouse motion (movementX/Y) with no clipping. + * - Keyboard Lock captures key events the browser would otherwise consume + * (Tab, F-keys, ...). + * - Wheel and multi-touch (normalized 0..1) are forwarded for remote injection. + * + * The agent does NOT implement an `input` handler yet (session.go answers with + * `{"type":"error","message":"unknown type: input"}`), so the stream panel keeps + * this off by default. + */ +export class InputController { + private send: Send + private target: HTMLElement | null = null + private locked = false + private bound: Array<[EventTarget, string, EventListener, AddEventListenerOptions | boolean]> = [] + + constructor(send: Send) { + this.send = send + } + + attach(target: HTMLElement): void { + if (this.target) this.detach() + this.target = target + + this.on(target, 'click', () => { + if (!this.locked && target.requestPointerLock) { + target.requestPointerLock() + } + }) + + this.on(document, 'pointerlockchange', () => { + this.locked = document.pointerLockElement === target + target.classList.toggle('locked', this.locked) + if (this.locked) this.tryKeyboardLock() + }) + + this.on(target, 'mousemove', (e) => { + if (!this.locked) return + const ev = e as MouseEvent + this.send({ type: 'input', kind: 'mouse', dx: ev.movementX, dy: ev.movementY, buttons: ev.buttons }) + }) + + this.on(target, 'mousedown', (e) => { + if (!this.locked) return + const ev = e as MouseEvent + this.send({ type: 'input', kind: 'mousedown', button: ev.button }) + }) + + this.on(target, 'mouseup', (e) => { + if (!this.locked) return + const ev = e as MouseEvent + this.send({ type: 'input', kind: 'mouseup', button: ev.button }) + }) + + this.on(target, 'wheel', (e) => { + if (!this.locked) return + e.preventDefault() + const ev = e as WheelEvent + this.send({ type: 'input', kind: 'wheel', dx: ev.deltaX, dy: ev.deltaY }) + }, { passive: false }) + + // Suppress the context menu so right-click can be sent to the remote. + this.on(target, 'contextmenu', (e) => e.preventDefault()) + + this.on(window, 'keydown', (e) => this.onKey(e as KeyboardEvent, true)) + this.on(window, 'keyup', (e) => this.onKey(e as KeyboardEvent, false)) + + this.on(target, 'touchstart', (e) => this.onTouch(e as TouchEvent, 'start'), { passive: false }) + this.on(target, 'touchmove', (e) => this.onTouch(e as TouchEvent, 'move'), { passive: false }) + this.on(target, 'touchend', (e) => this.onTouch(e as TouchEvent, 'end'), { passive: false }) + } + + get isLocked(): boolean { + return this.locked + } + + release(): void { + if (this.locked && document.exitPointerLock) document.exitPointerLock() + const kb = (navigator as Navigator & { keyboard?: { unlock?: () => void } }).keyboard + if (kb?.unlock) kb.unlock() + } + + detach(): void { + this.release() + for (const [t, type, fn, opts] of this.bound) t.removeEventListener(type, fn, opts) + this.bound = [] + this.target?.classList.remove('locked') + this.target = null + this.locked = false + } + + private on( + t: EventTarget, + type: string, + fn: (e: Event) => void, + opts?: AddEventListenerOptions | boolean + ): void { + t.addEventListener(type, fn, opts) + this.bound.push([t, type, fn as EventListener, opts ?? false]) + } + + private tryKeyboardLock(): void { + const kb = (navigator as Navigator & { keyboard?: { lock?: (codes?: string[]) => Promise } }).keyboard + if (!kb?.lock) return + // Best-effort: capture a broad set of keys. Failures are non-fatal. + kb.lock(LOCK_KEYS).catch(() => { + /* keyboard lock may require a transient activation; ignore */ + }) + } + + private onKey(e: KeyboardEvent, down: boolean): void { + if (!this.locked) return + // Stop the page from acting on keys we forward (scroll, find, ...). + if (SWALLOW_KEYS.includes(e.key)) e.preventDefault() + this.send({ type: 'input', kind: 'key', code: e.code, down }) + } + + private onTouch(e: TouchEvent, phase: 'start' | 'move' | 'end'): void { + const target = this.target + if (!target) return + e.preventDefault() + const rect = target.getBoundingClientRect() + const touches = e.changedTouches + for (let i = 0; i < touches.length; i++) { + const t = touches[i] + const x = rect.width ? (t.clientX - rect.left) / rect.width : 0 + const y = rect.height ? (t.clientY - rect.top) / rect.height : 0 + this.send({ type: 'input', kind: 'touch', id: t.identifier, x, y, phase }) + } + } +} + +const FN_KEYS = ['F1', 'F2', 'F3', 'F4', 'F5', 'F6', 'F7', 'F8', 'F9', 'F10', 'F11', 'F12'] +const ARROW_KEYS = ['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'] + +const LOCK_KEYS = ['Tab', 'Space', ...ARROW_KEYS, ...FN_KEYS] +const SWALLOW_KEYS = ['Tab', 'Space', ...ARROW_KEYS, "'", '/', ...FN_KEYS] diff --git a/src/main.ts b/src/main.ts new file mode 100644 index 0000000..9e43ab8 --- /dev/null +++ b/src/main.ts @@ -0,0 +1,77 @@ +import './style.css' +import type { ConnectionConfig } from './types' +import { Sidebar, loadConnections, saveConnections } from './ui/sidebar' +import { StreamPanel } from './ui/stream' +import { DebugOverlay } from './ui/debug' +import { $, debugLog } from './util' + +const sidebarEl = $('#sidebar') +const placeholderEl = $('#stream-placeholder') +const streamPanelEl = $('#stream-panel') +const debugEl = $('#debug-overlay') + +const debugOverlay = new DebugOverlay(debugEl) +void debugOverlay + +let activePanel: StreamPanel | null = null +let activeId: string | null = null + +const sidebar = new Sidebar(sidebarEl, { + onSelect(config: ConnectionConfig) { + if (activeId === config.id) return + destroyPanel() + activeId = config.id + placeholderEl.classList.add('hidden') + streamPanelEl.classList.remove('hidden') + sidebar.updateStatus(config.id, 'connecting') + activePanel = new StreamPanel(streamPanelEl, config, { + onStatusChange(status: string) { + sidebar.updateStatus(config.id, status) + }, + onError(message: string) { + debugLog('Error:', message) + }, + onConfigChange() { + sidebar.reload() + }, + onBack() { + destroyPanel() + activeId = null + streamPanelEl.classList.add('hidden') + placeholderEl.classList.remove('hidden') + } + }) + }, + onDelete(id: string) { + const conns = loadConnections().filter(c => c.id !== id) + saveConnections(conns) + if (activeId === id) { + destroyPanel() + activeId = null + streamPanelEl.classList.add('hidden') + placeholderEl.classList.remove('hidden') + } + sidebar.setConnections(conns) + }, + onAdd(config: ConnectionConfig) { + const conns = loadConnections() + conns.push(config) + saveConnections(conns) + sidebar.setConnections(conns) + } +}) + +sidebar.render() + +function destroyPanel(): void { + if (activePanel) { + activePanel.destroy() + activePanel = null + } +} + +window.addEventListener('keydown', (e) => { + if ((e.key === '~' || e.key === '`') && activePanel) { + activePanel.toggleStats() + } +}) diff --git a/src/main/main.ts b/src/main/main.ts deleted file mode 100644 index 8eba828..0000000 --- a/src/main/main.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { app, BrowserWindow, screen } from 'electron' -import path from 'path' - -const isDev = process.env.NODE_ENV !== 'production' || !app.isPackaged - -function createWindow(): void { - const { width: screenWidth, height: screenHeight } = screen.getPrimaryDisplay().workAreaSize - - const mainWindow = new BrowserWindow({ - width: Math.min(1280, Math.round(screenWidth * 0.8)), - height: Math.min(800, Math.round(screenHeight * 0.8)), - minWidth: 800, - minHeight: 600, - title: 'Distance Client', - backgroundColor: '#0f0f0f', - webPreferences: { - preload: path.join(__dirname, 'preload.js'), - contextIsolation: true, - nodeIntegration: false, - webgl: true, - }, - }) - - if (isDev) { - mainWindow.webContents.openDevTools({ mode: 'detach' }) - } - mainWindow.loadFile(path.join(__dirname, '../renderer/index.html')) -} - -app.whenReady().then(createWindow) - -app.on('window-all-closed', () => { - app.quit() -}) - -app.on('activate', () => { - if (BrowserWindow.getAllWindows().length === 0) { - createWindow() - } -}) diff --git a/src/main/preload.ts b/src/main/preload.ts deleted file mode 100644 index 3ad94f6..0000000 --- a/src/main/preload.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { contextBridge } from 'electron' - -contextBridge.exposeInMainWorld('electronAPI', { - platform: process.platform, -}) diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx deleted file mode 100644 index 013d138..0000000 --- a/src/renderer/App.tsx +++ /dev/null @@ -1,136 +0,0 @@ -import { useState, useCallback } from 'react' -import type { ConnectionConfig, ConnectionStatus } from './lib/types' -import { AddConnection } from './components/AddConnection' -import { ConnectionList } from './components/ConnectionList' -import { StreamPanel } from './components/StreamPanel' -import { DebugOverlay } from './components/DebugOverlay' -import './App.css' - -const STORAGE_KEY = 'distance-connections' - -function loadConnections(): ConnectionConfig[] { - try { - const raw = localStorage.getItem(STORAGE_KEY) - return raw ? JSON.parse(raw) : [] - } catch { - return [] - } -} - -function saveConnections(conns: ConnectionConfig[]): void { - localStorage.setItem(STORAGE_KEY, JSON.stringify(conns)) -} - -export default function App() { - const [connections, setConnections] = useState(loadConnections) - const [activeId, setActiveId] = useState(null) - const [statuses, setStatuses] = useState>({}) - const [errors, setErrors] = useState>({}) - - const activeConfig = connections.find((c) => c.id === activeId) ?? null - - const handleAdd = useCallback((config: ConnectionConfig) => { - const updated = [...connections, config] - setConnections(updated) - saveConnections(updated) - }, [connections]) - - const handleDelete = useCallback((id: string) => { - const updated = connections.filter((c) => c.id !== id) - setConnections(updated) - saveConnections(updated) - if (activeId === id) { - setActiveId(null) - } - }, [connections, activeId]) - - const handleSelect = useCallback((config: ConnectionConfig) => { - setActiveId(config.id) - setErrors((prev) => { - const next = { ...prev } - delete next[config.id] - return next - }) - }, []) - - const handleStatusChange = useCallback((id: string, status: ConnectionStatus) => { - setStatuses((prev) => ({ ...prev, [id]: status })) - }, []) - - const handleError = useCallback((id: string, message: string) => { - setErrors((prev) => ({ ...prev, [id]: message })) - setStatuses((prev) => ({ ...prev, [id]: 'disconnected' })) - }, []) - - const handleUpdateFingerprint = useCallback((id: string, fingerprint: string) => { - setConnections((prev) => { - const updated = prev.map((c) => { - if (c.id !== id) return c - if (c.fingerprints.includes(fingerprint)) return c - return { ...c, fingerprints: [...c.fingerprints, fingerprint] } - }) - saveConnections(updated) - return updated - }) - }, []) - - const handleBack = useCallback(() => { - setActiveId(null) - }, []) - - return ( -
- {activeConfig ? ( -
-
- -

{activeConfig.name}

- {activeConfig.host}:{activeConfig.port} -
- {errors[activeConfig.id] && ( -
- {errors[activeConfig.id]} - -
- )} - -
- ) : ( -
-
-

Distance Client

-

Remote Desktop Viewer

-
- - - {Object.entries(errors).length > 0 && ( -
- {Object.entries(errors).map(([id, msg]) => { - const conn = connections.find((c) => c.id === id) - return ( -
- {conn?.name ?? id}: {msg} -
- ) - })} -
- )} -
- )} - -
- ) -} diff --git a/src/renderer/components/AddConnection.tsx b/src/renderer/components/AddConnection.tsx deleted file mode 100644 index f98613c..0000000 --- a/src/renderer/components/AddConnection.tsx +++ /dev/null @@ -1,78 +0,0 @@ -import { useState } from 'react' -import { v4 as uuid } from 'uuid' -import type { ConnectionConfig } from '../lib/types' - -interface Props { - onAdd: (config: ConnectionConfig) => void -} - -export function AddConnection({ onAdd }: Props) { - const [name, setName] = useState('') - const [host, setHost] = useState('') - const [port, setPort] = useState('52020') - const [fingerprint, setFingerprint] = useState('') - const [show, setShow] = useState(false) - - const canSave = name.trim() && host.trim() && port.trim() - - const handleSubmit = (e: React.FormEvent) => { - e.preventDefault() - if (!canSave) return - onAdd({ - id: uuid(), - name: name.trim(), - host: host.trim(), - port: parseInt(port, 10) || 52020, - fingerprints: fingerprint.trim() ? [fingerprint.trim()] : [], - }) - setName('') - setHost('') - setPort('52020') - setFingerprint('') - setShow(false) - } - - if (!show) { - return ( - - ) - } - - return ( -
- setName(e.target.value)} - autoFocus - /> - setHost(e.target.value)} - /> - setPort(e.target.value)} - min={1} - max={65535} - /> - setFingerprint(e.target.value)} - /> -
- - -
-
- ) -} diff --git a/src/renderer/components/ConnectionList.tsx b/src/renderer/components/ConnectionList.tsx deleted file mode 100644 index be44285..0000000 --- a/src/renderer/components/ConnectionList.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import type { ConnectionConfig, ConnectionStatus } from '../lib/types' - -interface Props { - connections: ConnectionConfig[] - activeId: string | null - statuses: Record - onSelect: (config: ConnectionConfig) => void - onDelete: (id: string) => void -} - -const statusLabels: Record = { - disconnected: '', - connecting: 'Connecting…', - connected: 'Connected', - streaming: 'Streaming', -} - -export function ConnectionList({ connections, activeId, statuses, onSelect, onDelete }: Props) { - if (connections.length === 0) { - return
No connections yet
- } - - return ( -
    - {connections.map((conn) => { - const status = statuses[conn.id] - const isActive = conn.id === activeId - return ( -
  • - - -
  • - ) - })} -
- ) -} diff --git a/src/renderer/components/DebugOverlay.tsx b/src/renderer/components/DebugOverlay.tsx deleted file mode 100644 index 9227e14..0000000 --- a/src/renderer/components/DebugOverlay.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import { useState, useEffect } from 'react' -import { getDebugLogs, onDebugLogUpdate } from '../lib/debug' - -export function DebugOverlay() { - const [visible, setVisible] = useState(false) - const [logs, setLogs] = useState(getDebugLogs) - - useEffect(() => { - const unsub = onDebugLogUpdate(() => { - setLogs([...getDebugLogs()]) - }) - return unsub - }, []) - - useEffect(() => { - const handler = (e: KeyboardEvent) => { - if (e.key === 'd' && e.metaKey && e.shiftKey) { - setVisible(v => !v) - } - } - window.addEventListener('keydown', handler) - return () => window.removeEventListener('keydown', handler) - }, []) - - if (!visible) return null - - return ( -
-
- Debug Logs - - -
-
- {logs.length === 0 && No logs yet. Press Cmd+Shift+D to toggle.} - {logs.map((line, i) =>
{line}
)} -
-
- ) -} diff --git a/src/renderer/components/StreamPanel.tsx b/src/renderer/components/StreamPanel.tsx deleted file mode 100644 index b7f92bf..0000000 --- a/src/renderer/components/StreamPanel.tsx +++ /dev/null @@ -1,148 +0,0 @@ -import { useRef, useEffect, useState } from 'react' -import type { DisplayInfo, ConnectionConfig } from '../lib/types' -import { WebTransportClient, type StreamEvent } from '../lib/webtransport-client' -import { MoqIntegration } from '../lib/moq-integration' -import { debugLog } from '../lib/debug' - -interface Props { - config: ConnectionConfig - onStatusChange: (id: string, status: 'disconnected' | 'connecting' | 'connected' | 'streaming') => void - onError: (id: string, message: string) => void - onUpdateFingerprint?: (id: string, fingerprint: string) => void -} - -export function StreamPanel({ config, onStatusChange, onError, onUpdateFingerprint }: Props) { - const canvasRef = useRef(null) - const streamRef = useRef(null) - const moqRef = useRef(null) - const [displays, setDisplays] = useState([]) - const [selectedDisplayId, setSelectedDisplayId] = useState(0) - const [streamInfo, setStreamInfo] = useState<{ width: number; height: number; codec: string } | null>(null) - - useEffect(() => { - const moq = new MoqIntegration() - moqRef.current = moq - - const client = new WebTransportClient(config, (event: StreamEvent) => { - debugLog('StreamPanel: event:', event.type, event) - switch (event.type) { - case 'connecting': - onStatusChange(config.id, 'connecting') - break - case 'connected': { - debugLog('StreamPanel: connected, requesting display list') - onStatusChange(config.id, 'connected') - client.listDisplays() - - moq.connect(config.host, config.port, config.fingerprints).catch((err) => { - debugLog('StreamPanel: MoQ connect error:', err) - }) - break - } - case 'displays': - debugLog('StreamPanel: received', event.displays.length, 'displays') - setDisplays(event.displays) - if (event.displays.length > 0) { - setSelectedDisplayId(event.displays[0].id) - } - break - case 'started': { - debugLog('StreamPanel: stream started', event.width, 'x', event.height, event.codec) - setStreamInfo({ width: event.width, height: event.height, codec: event.codec }) - onStatusChange(config.id, 'streaming') - - const canvas = canvasRef.current - if (canvas && moq.connected) { - moq.startDisplay(canvas, selectedDisplayId) - } else { - debugLog('StreamPanel: MoQ not ready yet, will retry in 500ms') - setTimeout(() => { - if (canvas && moqRef.current?.connected) { - moqRef.current.startDisplay(canvas, selectedDisplayId) - } - }, 500) - } - break - } - case 'stopped': - case 'stream-ended': - debugLog('StreamPanel: stream ended') - setStreamInfo(null) - setDisplays([]) - moq.stopDisplay() - onStatusChange(config.id, 'connected') - break - case 'error': - debugLog('StreamPanel: error event:', event.message) - onError(config.id, event.message) - break - case 'disconnected': - debugLog('StreamPanel: disconnected') - setStreamInfo(null) - setDisplays([]) - moq.close() - onStatusChange(config.id, 'disconnected') - break - case 'fingerprint-refresh': - debugLog('StreamPanel: fingerprint refresh:', event.fingerprint) - onUpdateFingerprint?.(config.id, event.fingerprint) - break - } - }) - streamRef.current = client - client.connect() - - return () => { - moq.close() - client.disconnect() - } - }, [config, onStatusChange, onError, onUpdateFingerprint]) - - const handleStart = () => { - streamRef.current?.startStream(selectedDisplayId) - } - - const handleStop = () => { - streamRef.current?.stopStream() - moqRef.current?.stopDisplay() - setStreamInfo(null) - } - - return ( -
- {displays.length > 0 && !streamInfo && ( -
- - - -
- )} - - {streamInfo && ( -
- Streaming: {streamInfo.width}x{streamInfo.height} ({streamInfo.codec}) - -
- )} - -
- - {!streamInfo && !displays.length && ( -
-

Connected to {config.name}

-

Loading displays…

-
- )} -
-
- ) -} diff --git a/src/renderer/lib/debug.ts b/src/renderer/lib/debug.ts deleted file mode 100644 index c86cdf1..0000000 --- a/src/renderer/lib/debug.ts +++ /dev/null @@ -1,25 +0,0 @@ -const logs: string[] = [] -const MAX_LOGS = 100 -const listeners: Array<() => void> = [] - -export function debugLog(...args: unknown[]): void { - const ts = new Date().toISOString().slice(11, 23) - const msg = args.map(a => typeof a === 'object' ? JSON.stringify(a) : String(a)).join(' ') - const entry = `[${ts}] ${msg}` - console.debug('[Distance]', ...args) - logs.push(entry) - if (logs.length > MAX_LOGS) logs.shift() - listeners.forEach(fn => fn()) -} - -export function getDebugLogs(): string[] { - return [...logs] -} - -export function onDebugLogUpdate(fn: () => void): () => void { - listeners.push(fn) - return () => { - const idx = listeners.indexOf(fn) - if (idx >= 0) listeners.splice(idx, 1) - } -} diff --git a/src/renderer/lib/moq-integration.ts b/src/renderer/lib/moq-integration.ts deleted file mode 100644 index 9a1f4bf..0000000 --- a/src/renderer/lib/moq-integration.ts +++ /dev/null @@ -1,98 +0,0 @@ -import * as Moq from '@moq/net' -import * as Watch from '@moq/watch' -import { Signal } from '@moq/signals' -import { debugLog } from './debug' - -function parseFingerprint(hex: string): Uint8Array { - const clean = hex.replace(/[^0-9a-fA-F]/g, '') - const bytes = new Uint8Array(clean.length / 2) - for (let i = 0; i < bytes.length; i++) { - bytes[i] = parseInt(clean.substring(i * 2, i * 2 + 2), 16) - } - return bytes -} - -export class MoqIntegration { - private established: Moq.Connection.Established | null = null - private broadcast: Watch.Broadcast | null = null - private backend: Watch.MultiBackend | null = null - private establishedSig: Signal | null = null - private _connected = false - - get connected(): boolean { - return this._connected - } - - async connect(host: string, port: number, fingerprints: string[]): Promise { - if (this.established) return - debugLog('MoqIntegration: connecting MoQ layer to /moq...') - try { - const url = `https://${host}:${port}/moq` - const wt = new WebTransport(url, { - protocols: ['moq-lite-04', 'moq-lite-03', 'moql', 'moqt-18', 'moqt-17', 'moqt-16', 'moqt-15'], - serverCertificateHashes: fingerprints.map(fp => ({ - algorithm: 'sha-256', - value: parseFingerprint(fp) as BufferSource, - })), - }) - await wt.ready - const parsedUrl = new URL(url) - this.established = await Moq.Connection.connect(parsedUrl, { transport: wt }) - this.establishedSig = new Signal(this.established) - this._connected = true - debugLog('MoqIntegration: MoQ connected, version:', this.established.version) - } catch (err) { - debugLog('MoqIntegration: MoQ connect failed:', err) - this._connected = false - throw err - } - } - - startDisplay(canvas: HTMLCanvasElement, displayId: number): void { - if (!this.established || !this.establishedSig) { - debugLog('MoqIntegration: cannot start display, MoQ not connected') - return - } - - const path = Moq.Path.from('display', String(displayId)) - debugLog('MoqIntegration: subscribing to broadcast:', path) - - this.broadcast = new Watch.Broadcast({ - connection: this.establishedSig, - name: path, - enabled: true, - }) - - this.backend = new Watch.MultiBackend({ - element: canvas, - broadcast: this.broadcast, - }) - - debugLog('MoqIntegration: display started') - } - - stopDisplay(): void { - debugLog('MoqIntegration: stopping display') - if (this.backend) { - this.backend.close() - this.backend = null - } - if (this.broadcast) { - this.broadcast.close() - this.broadcast = null - } - debugLog('MoqIntegration: display stopped') - } - - close(): void { - debugLog('MoqIntegration: closing') - this.stopDisplay() - if (this.established) { - this.established.close() - this.established = null - this.establishedSig = null - this._connected = false - } - debugLog('MoqIntegration: closed') - } -} diff --git a/src/renderer/lib/types.ts b/src/renderer/lib/types.ts deleted file mode 100644 index 0b2937b..0000000 --- a/src/renderer/lib/types.ts +++ /dev/null @@ -1,45 +0,0 @@ -export interface ConnectionConfig { - id: string - name: string - host: string - port: number - fingerprints: string[] -} - -export interface DisplayInfo { - id: number - width: number - height: number - x: number - y: number - refresh_rate: number -} - -export type ClientMessage = - | { type: 'list-displays' } - | { type: 'start'; display_id: number; fps?: number; codec?: string; bitrate?: number } - | { type: 'stop' } - -export type ServerMessage = - | { type: 'displays'; displays: DisplayInfo[] } - | { type: 'started'; width: number; height: number; codec: string } - | { type: 'stopped' } - | { type: 'error'; message: string } - | { type: 'stream-ended' } - | { type: 'fingerprint-refresh'; algorithm: string; fingerprint: string } - -export type ConnectionStatus = - | 'disconnected' - | 'connecting' - | 'connected' - | 'streaming' - -export interface StreamState { - status: ConnectionStatus - displays: DisplayInfo[] - width: number - height: number - codec: string - error: string - selectedDisplayId: number -} diff --git a/src/renderer/lib/webtransport-client.ts b/src/renderer/lib/webtransport-client.ts deleted file mode 100644 index c1f2475..0000000 --- a/src/renderer/lib/webtransport-client.ts +++ /dev/null @@ -1,230 +0,0 @@ -import type { ConnectionConfig, ServerMessage, ClientMessage, DisplayInfo } from './types' -import { debugLog } from './debug' - -export type StreamEvent = - | { type: 'displays'; displays: DisplayInfo[] } - | { type: 'started'; width: number; height: number; codec: string } - | { type: 'stopped' } - | { type: 'error'; message: string } - | { type: 'stream-ended' } - | { type: 'disconnected' } - | { type: 'connecting' } - | { type: 'connected' } - | { type: 'fingerprint-refresh'; algorithm: string; fingerprint: string } - -function parseFingerprint(hex: string): Uint8Array { - const clean = hex.replace(/[^0-9a-fA-F]/g, '') - const bytes = new Uint8Array(clean.length / 2) - for (let i = 0; i < bytes.length; i++) { - bytes[i] = parseInt(clean.substring(i * 2, i * 2 + 2), 16) - } - return bytes -} - -export class WebTransportClient { - private transport: WebTransport | null = null - private config: ConnectionConfig - private onEvent: (event: StreamEvent) => void - private writer: WritableStreamDefaultWriter | null = null - private aborted = false - private abortController: AbortController | null = null - - constructor(config: ConnectionConfig, onEvent: (event: StreamEvent) => void) { - this.config = config - this.onEvent = onEvent - } - - getTransport(): WebTransport | null { - return this.transport - } - - getUrl(): string { - return `https://${this.config.host}:${this.config.port}/wt` - } - - connect(): void { - this.aborted = false - this.abortController = new AbortController() - debugLog(`connect() called for ${this.config.host}:${this.config.port}`) - this.onEvent({ type: 'connecting' }) - this.start().catch((err) => { - if (this.aborted) return - const msg = `Connection failed: ${err instanceof Error ? err.message : String(err)}` - debugLog('connect() failed:', msg) - this.onEvent({ type: 'error', message: msg }) - this.onEvent({ type: 'disconnected' }) - }) - } - - private async start(): Promise { - const url = this.getUrl() - debugLog(`start(): connecting to ${url}`) - debugLog(`start(): fingerprints:`, this.config.fingerprints) - - const options: WebTransportOptions = { - protocols: ['moq-lite-04', 'moq-lite-03', 'moql', 'moqt-18', 'moqt-17', 'moqt-16', 'moqt-15'], - } - if (this.config.fingerprints && this.config.fingerprints.length > 0) { - options.serverCertificateHashes = this.config.fingerprints.map((fp) => ({ - algorithm: 'sha-256', - value: parseFingerprint(fp) as BufferSource, - })) - debugLog(`start(): using ${this.config.fingerprints.length} cert fingerprint(s)`) - } - - this.transport = new WebTransport(url, options) - debugLog('start(): WebTransport instance created, waiting for ready...') - - this.transport.closed.then(() => { - debugLog('transport.closed resolved (clean close)') - if (!this.aborted) { - this.onEvent({ type: 'disconnected' }) - } - }).catch((err) => { - debugLog('transport.closed rejected:', err) - if (!this.aborted) { - this.onEvent({ type: 'disconnected' }) - } - }) - - try { - await this.transport.ready - debugLog('start(): transport.ready resolved - WebTransport connected!') - } catch (err) { - debugLog('start(): transport.ready REJECTED:', err) - throw err - } - if (this.aborted) return - - debugLog('start(): creating bidirectional stream...') - const bidiStream = await this.transport.createBidirectionalStream() - debugLog('start(): bidirectional stream created') - if (this.aborted) return - this.writer = bidiStream.writable.getWriter() - - this.onEvent({ type: 'connected' }) - - const signal = this.abortController!.signal - this.readControlStream(bidiStream.readable, signal) - } - - private async readControlStream( - readable: ReadableStream, - signal: AbortSignal, - ): Promise { - const reader = readable.getReader() - const decoder = new TextDecoder() - - try { - while (true) { - if (signal.aborted) { - debugLog('readControlStream: signal aborted, exiting') - break - } - const { value, done } = await reader.read() - if (done) { - debugLog('readControlStream: stream done (closed by server)') - break - } - - let text: string - try { - text = decoder.decode(value as BufferSource, { stream: true }) - } catch (e) { - debugLog('readControlStream: decode error:', e) - continue - } - - debugLog('readControlStream: raw data received:', text) - const lines = text.split('\n').filter(Boolean) - for (const line of lines) { - try { - const parsed = JSON.parse(line) as ServerMessage - debugLog('readControlStream: parsed message:', parsed) - this.handleControlMessage(parsed) - } catch (e) { - debugLog('readControlStream: invalid JSON:', line, e) - this.onEvent({ type: 'error', message: 'Invalid JSON from server' }) - } - } - } - } catch (err) { - debugLog('readControlStream: error:', err) - if (!this.aborted && !signal.aborted) { - this.onEvent({ type: 'error', message: `Control stream error: ${err instanceof Error ? err.message : String(err)}` }) - } - } - } - - private handleControlMessage(msg: ServerMessage): void { - debugLog('handleControlMessage:', msg.type, msg) - switch (msg.type) { - case 'displays': - this.onEvent({ type: 'displays', displays: msg.displays }) - break - case 'started': - this.onEvent({ type: 'started', width: msg.width, height: msg.height, codec: msg.codec }) - break - case 'stopped': - this.onEvent({ type: 'stopped' }) - break - case 'error': - debugLog('server error:', msg.message) - this.onEvent({ type: 'error', message: msg.message }) - break - case 'stream-ended': - this.onEvent({ type: 'stream-ended' }) - break - case 'fingerprint-refresh': - debugLog('fingerprint refresh:', msg.algorithm, msg.fingerprint) - this.onEvent({ - type: 'fingerprint-refresh', - algorithm: msg.algorithm, - fingerprint: msg.fingerprint, - }) - break - default: - debugLog('unhandled message type:', (msg as { type: string }).type) - } - } - - private async send(msg: ClientMessage): Promise { - if (!this.writer) { - debugLog('send(): no writer, dropping message:', msg.type) - return - } - try { - const encoder = new TextEncoder() - const data = encoder.encode(JSON.stringify(msg) + '\n') - debugLog('send():', JSON.stringify(msg)) - await this.writer.write(data) - } catch (e) { - debugLog('send(): error:', e) - } - } - - listDisplays(): void { - debugLog('listDisplays() called') - this.send({ type: 'list-displays' }) - } - - startStream(displayId: number, fps = 60, codec = 'h264', bitrate?: number): void { - debugLog(`startStream(): display=${displayId} fps=${fps} codec=${codec} bitrate=${bitrate}`) - this.send({ type: 'start', display_id: displayId, fps, codec, bitrate }) - } - - stopStream(): void { - debugLog('stopStream() called') - this.send({ type: 'stop' }) - } - - disconnect(): void { - debugLog('disconnect() called') - this.aborted = true - this.writer = null - if (this.transport) { - this.transport.close() - this.transport = null - } - } -} diff --git a/src/renderer/main.tsx b/src/renderer/main.tsx deleted file mode 100644 index feac8ee..0000000 --- a/src/renderer/main.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import { StrictMode } from 'react' -import { createRoot } from 'react-dom/client' -import App from './App' - -createRoot(document.getElementById('root')!).render( - - - , -) diff --git a/src/renderer/vite-env.d.ts b/src/renderer/vite-env.d.ts deleted file mode 100644 index 2027c77..0000000 --- a/src/renderer/vite-env.d.ts +++ /dev/null @@ -1,9 +0,0 @@ -/// - -interface ElectronAPI { - platform: string -} - -interface Window { - electronAPI: ElectronAPI -} diff --git a/src/renderer/App.css b/src/style.css similarity index 71% rename from src/renderer/App.css rename to src/style.css index 70fff1f..044130e 100644 --- a/src/renderer/App.css +++ b/src/style.css @@ -20,7 +20,7 @@ --radius-sm: 4px; } -html, body, #root { +html, body { height: 100%; overflow: hidden; } @@ -34,19 +34,25 @@ body { -webkit-font-smoothing: antialiased; } -.app { +#app { display: flex; height: 100%; + position: relative; +} + +.hidden { + display: none !important; } -/* Sidebar (connection list) */ -.sidebar-view { +/* Sidebar */ +#sidebar { width: 340px; min-width: 340px; display: flex; flex-direction: column; border-right: 1px solid var(--border); background: var(--surface); + overflow-y: auto; } .sidebar-header { @@ -65,7 +71,6 @@ body { margin-top: 2px; } -/* Add Connection form */ .add-btn { margin: 12px 16px; padding: 10px 16px; @@ -106,6 +111,35 @@ body { border-color: var(--accent); } +.add-connection input[type="checkbox"] { + width: 14px; + height: 14px; + padding: 0; + flex: none; + accent-color: var(--accent); +} + +.form-row { + display: flex; + flex-direction: column; +} + +.form-check { + display: flex; + align-items: center; + gap: 8px; + font-size: 12px; + color: var(--text-muted); + cursor: pointer; + user-select: none; +} + +.form-hint { + font-size: 11px; + line-height: 1.4; + color: var(--text-muted); +} + .add-actions { display: flex; gap: 8px; @@ -145,14 +179,6 @@ body { background: var(--surface-hover); } -/* Connection list */ -.connection-list { - list-style: none; - overflow-y: auto; - flex: 1; - padding: 0 8px; -} - .empty-state { padding: 32px 16px; text-align: center; @@ -160,6 +186,13 @@ body { font-size: 13px; } +.connection-list { + list-style: none; + overflow-y: auto; + flex: 1; + padding: 0 8px; +} + .connection-item { display: flex; align-items: stretch; @@ -172,10 +205,6 @@ body { background: var(--surface-hover); } -.connection-item.active { - background: rgba(79, 140, 255, 0.1); -} - .connection-item.connecting { opacity: 0.7; } @@ -211,7 +240,8 @@ body { border-radius: 3px; } -.conn-status[data-status="connecting"] { +.conn-status[data-status="connecting"], +.conn-status[data-status="reconnecting"] { color: var(--warning); } @@ -220,6 +250,16 @@ body { color: var(--success); } +.conn-tag { + align-self: flex-start; + margin-top: 3px; + padding: 1px 6px; + border: 1px solid var(--border); + border-radius: 3px; + font-size: 10px; + color: var(--text-muted); +} + .conn-delete { padding: 0 12px; border: none; @@ -240,28 +280,28 @@ body { opacity: 1 !important; } -/* Error summary */ -.error-summary { - padding: 12px 16px; - border-top: 1px solid var(--border); -} - -.error-item { - padding: 6px 0; - font-size: 12px; - color: var(--danger); +/* Stream area */ +#stream-area { + flex: 1; + display: flex; + flex-direction: column; + background: var(--bg); + min-width: 0; } -.error-item strong { - margin-right: 4px; +.stream-placeholder { + flex: 1; + display: flex; + align-items: center; + justify-content: center; + color: var(--text-muted); + font-size: 14px; } -/* Stream view */ -.stream-view { +#stream-panel { flex: 1; display: flex; flex-direction: column; - background: var(--bg); } .stream-header { @@ -299,34 +339,6 @@ body { font-family: 'SF Mono', 'Menlo', 'Monaco', monospace; } -.error-banner { - display: flex; - align-items: center; - justify-content: space-between; - padding: 8px 16px; - background: rgba(231, 76, 76, 0.12); - color: var(--danger); - font-size: 13px; - border-bottom: 1px solid rgba(231, 76, 76, 0.2); -} - -.error-banner button { - border: none; - background: none; - color: var(--danger); - font-size: 18px; - cursor: pointer; - padding: 0 4px; -} - -/* Stream panel */ -.stream-panel { - flex: 1; - display: flex; - flex-direction: column; - position: relative; -} - .display-selector { display: flex; align-items: center; @@ -366,23 +378,20 @@ body { background: var(--accent-hover); } -.stream-info-bar { +.input-toggle { display: flex; align-items: center; - gap: 12px; - padding: 8px 16px; - background: var(--surface); - border-bottom: 1px solid var(--border); + gap: 6px; font-size: 12px; + color: var(--text-muted); + cursor: pointer; + user-select: none; } -.stream-info-bar .decoder-badge { - padding: 1px 6px; - border-radius: 3px; - background: rgba(63, 184, 122, 0.15); - color: var(--success); - font-family: 'SF Mono', 'Menlo', 'Monaco', monospace; - font-size: 11px; +.input-toggle input { + width: 13px; + height: 13px; + accent-color: var(--accent); } .stop-btn { @@ -418,17 +427,120 @@ body { display: block; } -.stream-placeholder { +.canvas-container canvas.locked { + cursor: none; + outline: 1px solid var(--accent); +} + +/* Stats overlay */ +.stats { position: absolute; - text-align: center; + top: 12px; + right: 12px; + min-width: 150px; + background: rgba(12, 13, 16, 0.72); + border: 1px solid var(--border); + border-radius: 10px; + padding: 10px 12px; + font-family: 'SF Mono', 'Menlo', 'Monaco', monospace; + font-size: 0.78rem; + backdrop-filter: blur(8px); + user-select: none; + z-index: 10; +} + +.stats .stat { + display: flex; + justify-content: space-between; + gap: 16px; + padding: 2px 0; +} + +.stats .stat .k { color: var(--text-muted); } -.stream-placeholder p { - font-size: 14px; +.stats .stat .v { + color: var(--success); } -.stream-placeholder .subtle { - font-size: 12px; - margin-top: 4px; +.stats .stat .v.bad { + color: var(--warning); +} + +.stats .stat .v.crit { + color: var(--danger); +} + +/* Toast */ +.toast { + position: fixed; + left: 50%; + top: 16px; + transform: translateX(-50%); + background: var(--surface); + border: 1px solid var(--border); + color: var(--text); + padding: 10px 16px; + border-radius: 10px; + font-size: 0.85rem; + box-shadow: 0 12px 40px rgba(0, 0, 0, 0.5); + z-index: 50; + max-width: 80%; +} + +.toast.err { + border-color: var(--danger); + color: #ffd9dd; +} + +.toast.ok { + border-color: var(--success); +} + +/* Debug overlay */ +.debug-overlay { + position: fixed; + bottom: 0; + left: 0; + right: 0; + height: 35vh; + background: rgba(0, 0, 0, 0.92); + color: #0f0; + font-family: monospace; + font-size: 11px; + line-height: 1.5; + padding: 8px; + overflow-y: auto; + z-index: 9999; + border-top: 2px solid #0f0; +} + +.debug-bar { + position: sticky; + top: 0; + background: #000; + padding-bottom: 4px; + display: flex; + gap: 8px; + align-items: center; +} + +.debug-bar span { + color: #0f0; + font-weight: bold; +} + +.debug-bar button { + background: none; + border: 1px solid #0f0; + color: #0f0; + cursor: pointer; + font-size: 11px; + padding: 2px 8px; +} + +.debug-log-container { + white-space: pre-wrap; + word-break: break-all; } diff --git a/src/transport.ts b/src/transport.ts new file mode 100644 index 0000000..101fdf2 --- /dev/null +++ b/src/transport.ts @@ -0,0 +1,258 @@ +import type { ClientMessage, ControlMessage } from './types' + +export interface ConnectOptions { + url: string + // SHA-256 fingerprints (hex) to pin. Empty means the agent serves a trusted + // certificate (--cert/--key, or a reverse proxy in front) and the browser + // validates it normally. + fingerprints: string[] +} + +export interface TransportStats { + bitrate: number + rtt: number + rxBytes: number +} + +// Advertised for forward compatibility only: the agent's upgrader treats +// ApplicationProtocols as an allow-list, and omitting the wt-protocol header +// negotiates fine (which is what the agent's own viewer does). Sending it costs +// nothing and keeps us working if the agent ever starts requiring a protocol. +const WT_PROTOCOLS = ['moq-lite-04'] + +type MessageHandler = (msg: ControlMessage) => void +type VideoHandler = (chunk: Uint8Array) => void +type StatsHandler = (stats: TransportStats) => void + +export class Transport { + private wt: WebTransport | null = null + private ctrlWriter: WritableStreamDefaultWriter | null = null + private ctrlReader: ReadableStreamDefaultReader | null = null + private msgHandler: MessageHandler | null = null + private videoHandler: VideoHandler | null = null + private statsHandler: StatsHandler | null = null + + private ctrlBuf: Uint8Array = new Uint8Array(0) + private enc = new TextEncoder() + private dec = new TextDecoder() + + private rxBytes = 0 + private windowBytes = 0 + private lastWindow = performance.now() + private bitrate = 0 + + private pendingSince = 0 + private rtt = 0 + + private statsTimer: number | undefined + private closed = false + + onMessage(h: MessageHandler) { + this.msgHandler = h + } + setVideoHandler(h: VideoHandler) { + this.videoHandler = h + } + onStats(h: StatsHandler) { + this.statsHandler = h + } + + get connected(): boolean { + return this.wt !== null + } + + async connect(opts: ConnectOptions): Promise { + this.closed = false + const wt = new WebTransport(opts.url, buildOptions(opts.fingerprints)) + this.wt = wt + + wt.closed.then( + () => { + if (!this.closed) this.msgHandler?.({ type: 'stream-ended' }) + }, + () => { + if (!this.closed) this.msgHandler?.({ type: 'stream-ended' }) + } + ) + + await wt.ready + + const bidi = await wt.createBidirectionalStream() + this.ctrlWriter = bidi.writable.getWriter() + this.ctrlReader = bidi.readable.getReader() + this.readControlLoop() + + this.readVideoLoop() + + this.statsTimer = window.setInterval(() => this.tickStats(), 1000) + } + + send(msg: ClientMessage): void { + if (!this.ctrlWriter) throw new Error('not connected') + if (msg.type === 'list-displays' || msg.type === 'start') { + this.pendingSince = performance.now() + } + const bytes = this.enc.encode(JSON.stringify(msg) + '\n') + this.ctrlWriter.write(bytes).catch(() => { + if (!this.closed) this.msgHandler?.({ type: 'stream-ended' }) + }) + } + + listDisplays(): void { + this.send({ type: 'list-displays' }) + } + + start(displayId: number, opts: { fps?: number; codec?: string; bitrate?: number } = {}): void { + this.send({ type: 'start', display_id: displayId, ...opts }) + } + + stop(): void { + this.send({ type: 'stop' }) + } + + getStats(): TransportStats { + return { bitrate: this.bitrate, rtt: this.rtt, rxBytes: this.rxBytes } + } + + close(): void { + this.closed = true + if (this.statsTimer) window.clearInterval(this.statsTimer) + this.statsTimer = undefined + try { this.ctrlWriter?.close() } catch { /* ignore */ } + try { this.wt?.close() } catch { /* ignore */ } + this.wt = null + this.ctrlWriter = null + this.ctrlReader = null + this.ctrlBuf = new Uint8Array(0) + this.rxBytes = 0 + this.windowBytes = 0 + this.bitrate = 0 + this.rtt = 0 + this.pendingSince = 0 + } + + private async readControlLoop(): Promise { + const reader = this.ctrlReader + if (!reader) return + try { + while (true) { + const { value, done } = await reader.read() + if (done) break + if (!value) continue + this.ctrlBuf = concat(this.ctrlBuf, value) + let nl: number + while ((nl = findNL(this.ctrlBuf)) !== -1) { + const line = this.dec.decode(this.ctrlBuf.subarray(0, nl)) + this.ctrlBuf = this.ctrlBuf.subarray(nl + 1) + const text = line.trim() + if (!text) continue + let msg: ControlMessage + try { msg = JSON.parse(text) } catch { continue } + if ((msg.type === 'displays' || msg.type === 'started') && this.pendingSince) { + this.rtt = Math.round(performance.now() - this.pendingSince) + this.pendingSince = 0 + } + this.msgHandler?.(msg) + } + } + } catch { /* stream closed */ } + } + + private async readVideoLoop(): Promise { + const wt = this.wt + if (!wt) return + try { + const streamReader = wt.incomingUnidirectionalStreams.getReader() + while (true) { + const { value: recv, done: sdone } = await streamReader.read() + if (sdone) break + if (!recv) continue + const reader = recv.readable.getReader() + while (true) { + const { value, done } = await reader.read() + if (done) break + if (!value) continue + this.rxBytes += value.byteLength + this.windowBytes += value.byteLength + this.videoHandler?.(value) + } + } + } catch { /* stream closed */ } + } + + private tickStats(): void { + const now = performance.now() + const dt = (now - this.lastWindow) / 1000 + if (dt > 0) { + this.bitrate = Math.round((this.windowBytes * 8) / dt) + this.windowBytes = 0 + this.lastWindow = now + } + this.statsHandler?.(this.getStats()) + } +} + +// serverCertificateHashes / protocols are missing from some lib.dom releases, +// so the option bag is assembled and cast once here. +function buildOptions(fingerprints: string[]): WebTransportOptions { + const opts: Record = { protocols: WT_PROTOCOLS } + if (fingerprints.length > 0) { + opts.serverCertificateHashes = fingerprints.map(fp => ({ + algorithm: 'sha-256', + value: parseFingerprint(fp) + })) + } + return opts as WebTransportOptions +} + +// Parse a SHA-256 fingerprint, tolerating colon/space separators. +export function parseFingerprint(hex: string): Uint8Array { + const bytes = hexToBytes(hex) + if (bytes.length !== 32) { + throw new Error('fingerprint must be a 32-byte SHA-256 hex string') + } + return bytes +} + +export function isValidFingerprint(hex: string): boolean { + try { + parseFingerprint(hex) + return true + } catch { + return false + } +} + +// Normalized lowercase hex, so dedupe survives formatting differences. +export function normalizeFingerprint(hex: string): string { + return hex.replace(/[^0-9a-fA-F]/g, '').toLowerCase() +} + +function concat(a: Uint8Array, b: Uint8Array): Uint8Array { + const out = new Uint8Array(a.length + b.length) + out.set(a, 0) + out.set(b, a.length) + return out +} + +function findNL(buf: Uint8Array): number { + for (let i = 0; i < buf.length; i++) { + if (buf[i] === 0x0a) return i + } + return -1 +} + +function hexToBytes(hex: string): Uint8Array { + const clean = normalizeFingerprint(hex) + if (clean.length % 2 !== 0) throw new Error('invalid hex length') + const out = new Uint8Array(clean.length / 2) + for (let i = 0; i < out.length; i++) { + out[i] = parseInt(clean.substring(i * 2, i * 2 + 2), 16) + } + return out +} + +export function wtUrl(host: string, port: number): string { + const h = host.replace(/^https?:\/\//, '').replace(/\/$/, '') + return `https://${h}:${port}/wt` +} diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..7fc7904 --- /dev/null +++ b/src/types.ts @@ -0,0 +1,81 @@ +// Control protocol shared with distancedesktop/agent (src/session.go). +// All control messages are newline-delimited JSON on the bidirectional stream. + +export interface ConnectionConfig { + id: string + name: string + host: string + port: number + // SHA-256 certificate fingerprints (hex). Multiple entries are kept so a + // rotated agent cert stays reachable; all of them are pinned at connect time. + // Empty when trustedCert is set. + fingerprints: string[] + // Agent is behind a reverse proxy or serves a publicly trusted cert + // (agent run with --cert/--key). No fingerprint pinning, no cert manager, + // so no fingerprint-refresh push either. + trustedCert?: boolean +} + +export interface DisplayInfo { + id: number + width: number + height: number + x: number + y: number + refresh_rate: number +} + +// Input messages. NOT IMPLEMENTED SERVER-SIDE: session.go has no `input` case, +// so these fall through to `default:` and come back as +// {"type":"error","message":"unknown type: input"}. Kept behind an +// off-by-default toggle in the stream panel until the agent grows a handler. +export type InputMessage = + | { type: 'input'; kind: 'mouse'; dx: number; dy: number; buttons: number } + | { type: 'input'; kind: 'mousedown'; button: number } + | { type: 'input'; kind: 'mouseup'; button: number } + | { type: 'input'; kind: 'wheel'; dx: number; dy: number } + | { type: 'input'; kind: 'key'; code: string; down: boolean } + | { type: 'input'; kind: 'touch'; id: number; x: number; y: number; phase: 'start' | 'move' | 'end' } + +// Client -> Server. `codec` and `bitrate` are accepted by the agent's `start` +// handler even though AGENTS.md documents only display_id/fps. +export type ClientMessage = + | { type: 'list-displays' } + | { type: 'start'; display_id: number; fps?: number; codec?: string; bitrate?: number } + | { type: 'stop' } + | InputMessage + +// Server -> Client. +// - `fingerprint-refresh` is pushed unconditionally on connect when the agent +// runs its own cert manager, and broadcast on rotation (rotation only reaches +// sessions subscribed to a live stream). +// - `displays` also arrives unsolicited right after the connect-time +// fingerprint, before any `list-displays` request. +// - `stream-ended` is synthesized locally when the WebTransport session drops; +// the agent never sends it. +// - `pong` is NOT IMPLEMENTED SERVER-SIDE: session.go has no `ping` case, so a +// ping is answered with {"type":"error","message":"unknown type: ping"}. +export type ServerMessage = + | { type: 'displays'; displays: DisplayInfo[] } + | { type: 'started'; width: number; height: number; codec: string } + | { type: 'stopped' } + | { type: 'stream-ended' } + | { type: 'error'; message: string } + | { type: 'fingerprint-refresh'; algorithm: string; fingerprint: string } + | { type: 'pong'; t: number } + +export type ControlMessage = ServerMessage + +// Connect payload as encoded in a QR / paste blob by the agent's web UI. +export interface ConnectPayload { + host: string + port?: number + fingerprint: string + label?: string +} + +export type ConnectionStatus = + | 'disconnected' + | 'connecting' + | 'connected' + | 'streaming' diff --git a/src/ui/debug.ts b/src/ui/debug.ts new file mode 100644 index 0000000..c407ccd --- /dev/null +++ b/src/ui/debug.ts @@ -0,0 +1,65 @@ +import { getDebugLogs, onDebugLogUpdate } from '../util' + +export class DebugOverlay { + private root: HTMLElement + private visible = false + + constructor(root: HTMLElement) { + this.root = root + this.root.className = 'debug-overlay hidden' + + window.addEventListener('keydown', (e) => { + if (e.key === 'D' && e.shiftKey && (e.metaKey || e.ctrlKey)) { + this.toggle() + } + }) + + onDebugLogUpdate(() => { + if (this.visible) this.renderLogs() + }) + } + + toggle(): void { + this.visible = !this.visible + this.root.classList.toggle('hidden', !this.visible) + if (this.visible) this.render() + } + + private render(): void { + this.root.innerHTML = '' + const bar = document.createElement('div') + bar.className = 'debug-bar' + + const title = document.createElement('span') + title.textContent = 'Debug Logs' + + const closeBtn = document.createElement('button') + closeBtn.textContent = 'Close' + closeBtn.addEventListener('click', () => this.toggle()) + + bar.append(title, closeBtn) + this.root.append(bar) + + const container = document.createElement('div') + container.className = 'debug-log-container' + this.root.append(container) + + this.renderLogs() + } + + private renderLogs(): void { + const container = this.root.querySelector('.debug-log-container') + if (!container) return + const logs = getDebugLogs() + container.innerHTML = '' + if (logs.length === 0) { + container.textContent = 'No logs yet.' + return + } + for (const line of logs) { + const div = document.createElement('div') + div.textContent = line + container.append(div) + } + } +} diff --git a/src/ui/sidebar.ts b/src/ui/sidebar.ts new file mode 100644 index 0000000..d742143 --- /dev/null +++ b/src/ui/sidebar.ts @@ -0,0 +1,224 @@ +import type { ConnectionConfig } from '../types' +import { el } from '../util' +import { isValidFingerprint, normalizeFingerprint } from '../transport' + +const STORAGE_KEY = 'distance-connections' + +// How many rotated fingerprints to keep per connection. The agent rotates when +// fewer than 7 days remain on a 14-day cert, so a handful covers any realistic +// gap between client sessions. +const MAX_FINGERPRINTS = 4 + +export function loadConnections(): ConnectionConfig[] { + try { + const raw = localStorage.getItem(STORAGE_KEY) + return raw ? JSON.parse(raw) : [] + } catch { + return [] + } +} + +export function saveConnections(conns: ConnectionConfig[]): void { + localStorage.setItem(STORAGE_KEY, JSON.stringify(conns)) +} + +export interface AppendResult { + changed: boolean + fingerprints: string[] +} + +/** + * Append a fingerprint to a stored connection, newest first, deduped and capped. + * Used for the connect-time push and for mid-stream rotation broadcasts. + */ +export function appendFingerprint(id: string, fingerprint: string): AppendResult { + const normalized = normalizeFingerprint(fingerprint) + const conns = loadConnections() + const conn = conns.find(c => c.id === id) + if (!conn) return { changed: false, fingerprints: [] } + + const existing = (conn.fingerprints ?? []).map(normalizeFingerprint).filter(fp => fp.length > 0) + if (existing[0] === normalized) return { changed: false, fingerprints: existing } + + const next = [normalized, ...existing.filter(fp => fp !== normalized)].slice(0, MAX_FINGERPRINTS) + conn.fingerprints = next + saveConnections(conns) + return { changed: true, fingerprints: next } +} + +export interface SidebarCallbacks { + onSelect: (config: ConnectionConfig) => void + onDelete: (id: string) => void + onAdd: (config: ConnectionConfig) => void +} + +const STATUS_LABELS: Record = { + connecting: 'Connecting\u2026', + reconnecting: 'Reconnecting\u2026', + connected: 'Connected', + streaming: 'Streaming' +} + +export class Sidebar { + private root: HTMLElement + private cbs: SidebarCallbacks + private connections: ConnectionConfig[] + private statuses: Record = {} + private showingForm = false + + constructor(root: HTMLElement, cbs: SidebarCallbacks) { + this.root = root + this.cbs = cbs + this.connections = loadConnections() + } + + updateStatus(id: string, status: string): void { + this.statuses[id] = status + this.render() + } + + setConnections(conns: ConnectionConfig[]): void { + this.connections = conns + saveConnections(conns) + this.render() + } + + // Re-read from storage without clobbering it, for changes made elsewhere + // (a cached fingerprint push, for instance). + reload(): void { + this.connections = loadConnections() + this.render() + } + + render(): void { + this.root.innerHTML = '' + + const header = el('div', { class: 'sidebar-header' }, [ + el('h1', { text: 'Distance Client' }), + el('p', { class: 'subtle', text: 'Remote Desktop Viewer' }) + ]) + this.root.append(header) + + if (this.showingForm) { + this.renderForm() + } else { + const addBtn = el('button', { class: 'add-btn', text: '+ Add Connection' }) as HTMLButtonElement + addBtn.addEventListener('click', () => { + this.showingForm = true + this.render() + }) + this.root.append(addBtn) + } + + if (this.connections.length === 0) { + this.root.append(el('div', { class: 'empty-state', text: 'No connections yet' })) + } else { + const list = el('ul', { class: 'connection-list' }) + for (const conn of this.connections) { + const status = this.statuses[conn.id] + const item = el('li', { class: 'connection-item' }) + if (status === 'connecting' || status === 'reconnecting') item.classList.add('connecting') + + const main = el('button', { class: 'connection-main' }) + main.append( + el('span', { class: 'conn-name', text: conn.name }), + el('span', { class: 'conn-host', text: `${conn.host}:${conn.port}` }) + ) + if (conn.trustedCert) { + main.append(el('span', { class: 'conn-tag', text: 'trusted cert' })) + } else if (conn.fingerprints.length > 1) { + main.append(el('span', { class: 'conn-tag', text: `${conn.fingerprints.length} fingerprints` })) + } + if (status && status !== 'disconnected') { + const badge = el('span', { class: 'conn-status' }) + badge.setAttribute('data-status', status) + badge.textContent = STATUS_LABELS[status] ?? status + main.append(badge) + } + main.addEventListener('click', () => this.cbs.onSelect(conn)) + + const del = el('button', { class: 'conn-delete', text: '\u00d7' }) as HTMLButtonElement + del.title = 'Remove connection' + del.addEventListener('click', (e) => { + e.stopPropagation() + this.cbs.onDelete(conn.id) + }) + + item.append(main, del) + list.append(item) + } + this.root.append(list) + } + } + + private renderForm(): void { + const form = el('form', { class: 'add-connection' }) + const nameInput = el('input', { type: 'text', placeholder: 'Name (e.g. Home PC)' }) as HTMLInputElement + const hostInput = el('input', { type: 'text', placeholder: 'Host (IP or hostname)' }) as HTMLInputElement + const portInput = el('input', { type: 'number', placeholder: 'Port' }) as HTMLInputElement + portInput.value = '52020' + + const fpRow = el('div', { class: 'form-row' }) + const fpInput = el('input', { type: 'text', placeholder: 'SHA-256 Fingerprint (64 hex chars)' }) as HTMLInputElement + fpRow.append(fpInput) + + // Agents started with --cert/--key (or fronted by a reverse proxy) serve a + // certificate the browser already trusts. There is no cert manager in that + // mode, so there is no fingerprint to pin and none gets pushed. + const trustedRow = el('label', { class: 'form-check' }) + const trustedBox = el('input', { type: 'checkbox' }) as HTMLInputElement + trustedRow.append( + trustedBox, + el('span', { text: 'Trusted certificate / behind reverse proxy' }) + ) + const trustedHint = el('p', { + class: 'form-hint hidden', + text: 'No fingerprint pinning \u2014 the browser validates the certificate itself.' + }) + trustedBox.addEventListener('change', () => { + fpRow.classList.toggle('hidden', trustedBox.checked) + trustedHint.classList.toggle('hidden', !trustedBox.checked) + if (trustedBox.checked) fpInput.value = '' + }) + + const actions = el('div', { class: 'add-actions' }) + const saveBtn = el('button', { type: 'submit', text: 'Save' }) as HTMLButtonElement + const cancelBtn = el('button', { type: 'button', text: 'Cancel' }) as HTMLButtonElement + cancelBtn.addEventListener('click', () => { + this.showingForm = false + this.render() + }) + actions.append(saveBtn, cancelBtn) + + form.append(nameInput, hostInput, portInput, trustedRow, trustedHint, fpRow, actions) + form.addEventListener('submit', (e) => { + e.preventDefault() + const name = nameInput.value.trim() + const host = hostInput.value.trim() + const port = parseInt(portInput.value, 10) || 52020 + const trustedCert = trustedBox.checked + const fp = normalizeFingerprint(fpInput.value) + if (!name || !host) return + if (!trustedCert && fp && !isValidFingerprint(fp)) { + fpInput.setCustomValidity('Expected 64 hex characters (SHA-256)') + fpInput.reportValidity() + return + } + fpInput.setCustomValidity('') + const config: ConnectionConfig = { + id: crypto.randomUUID(), + name, + host, + port, + fingerprints: !trustedCert && fp ? [fp] : [], + trustedCert + } + this.cbs.onAdd(config) + this.showingForm = false + this.reload() + }) + + this.root.append(form) + nameInput.focus() + } +} diff --git a/src/ui/stats.ts b/src/ui/stats.ts new file mode 100644 index 0000000..50141f1 --- /dev/null +++ b/src/ui/stats.ts @@ -0,0 +1,55 @@ +import { formatBitrate } from '../util' + +export interface StatsView { + fps: number + rtt: number + bitrate: number + width: number + height: number + online: boolean +} + +export class StatsOverlay { + public el: HTMLElement + private visible = false + private last: StatsView = { fps: 0, rtt: 0, bitrate: 0, width: 0, height: 0, online: false } + + constructor() { + this.el = document.createElement('div') + this.el.className = 'stats hidden' + } + + toggle(): void { + this.visible = !this.visible + this.el.classList.toggle('hidden', !this.visible) + if (this.visible) this.render() + } + + show(): void { + this.visible = true + this.el.classList.remove('hidden') + this.render() + } + + update(v: Partial): void { + this.last = { ...this.last, ...v } + if (this.visible) this.render() + } + + private row(k: string, v: string, cls = ''): string { + return `
${k}${v}
` + } + + private render(): void { + const v = this.last + const rttCls = v.rtt > 150 ? 'crit' : v.rtt > 60 ? 'bad' : '' + const fpsCls = v.fps === 0 ? 'bad' : '' + const bitrateCls = v.bitrate === 0 ? 'bad' : '' + this.el.innerHTML = + this.row('fps', v.fps ? `${v.fps}` : '\u2014', fpsCls) + + this.row('rtt', v.rtt ? `${v.rtt} ms` : '\u2014', rttCls) + + this.row('bitrate', v.bitrate ? formatBitrate(v.bitrate) : '\u2014', bitrateCls) + + this.row('res', v.width ? `${v.width}\u00d7${v.height}` : '\u2014') + + this.row('link', v.online ? 'up' : 'down', v.online ? '' : 'crit') + } +} diff --git a/src/ui/stream.ts b/src/ui/stream.ts new file mode 100644 index 0000000..f8ebc3c --- /dev/null +++ b/src/ui/stream.ts @@ -0,0 +1,328 @@ +import type { ConnectionConfig, DisplayInfo, ControlMessage } from '../types' +import { Transport, wtUrl, isValidFingerprint } from '../transport' +import { Decoder } from '../decoder' +import { InputController } from '../input' +import { el, debugLog, toast } from '../util' +import { StatsOverlay } from './stats' +import { appendFingerprint } from './sidebar' + +// Reconnect backoff: exponential with full jitter, capped. +const RECONNECT_BASE_MS = 500 +const RECONNECT_CAP_MS = 15_000 +const RECONNECT_MAX_ATTEMPTS = 8 + +export interface StreamPanelCallbacks { + onStatusChange: (status: string) => void + onError: (message: string) => void + onBack: () => void + // Fired when the stored connection changed (e.g. a rotated fingerprint was + // cached) so the sidebar can pick the new value up. + onConfigChange: () => void +} + +export class StreamPanel { + private root: HTMLElement + private config: ConnectionConfig + private transport: Transport + private decoder: Decoder + private stats: StatsOverlay + private input: InputController + private canvas: HTMLCanvasElement + + private displays: DisplayInfo[] = [] + private selectedDisplayId = 0 + private streaming = false + private inputEnabled = false + + private attempt = 0 + private reconnectTimer: number | undefined + private connecting = false + private destroyed = false + + private cbs: StreamPanelCallbacks + + constructor(root: HTMLElement, config: ConnectionConfig, cbs: StreamPanelCallbacks) { + this.root = root + this.config = config + this.cbs = cbs + + this.transport = new Transport() + this.canvas = el('canvas') as HTMLCanvasElement + this.decoder = new Decoder(this.canvas) + this.stats = new StatsOverlay() + this.input = new InputController((msg) => { + if (this.inputEnabled) this.transport.send(msg) + }) + + this.transport.onMessage((msg: ControlMessage) => this.handleMessage(msg)) + this.transport.setVideoHandler((chunk) => this.decoder.feed(chunk)) + this.transport.onStats((s) => { + this.stats.update({ bitrate: s.bitrate, rtt: s.rtt, online: this.transport.connected }) + }) + this.decoder.onFps = (fps) => this.stats.update({ fps }) + this.decoder.onFirstFrame = () => this.stats.show() + + this.render() + void this.connect() + } + + private render(): void { + this.root.innerHTML = '' + + const header = el('div', { class: 'stream-header' }) + const backBtn = el('button', { class: 'back-btn', text: '\u2190 Back' }) as HTMLButtonElement + backBtn.addEventListener('click', () => this.cbs.onBack()) + header.append( + backBtn, + el('h1', { text: this.config.name }), + el('span', { class: 'header-host', text: `${this.config.host}:${this.config.port}` }) + ) + this.root.append(header) + + const toolbar = el('div', { class: 'display-selector' }) + toolbar.id = 'stream-toolbar' + this.root.append(toolbar) + + const container = el('div', { class: 'canvas-container' }) + container.append(this.canvas) + this.root.append(container) + + this.root.append(this.stats.el) + } + + private updateToolbar(): void { + const toolbar = this.root.querySelector('#stream-toolbar') + if (!toolbar) return + toolbar.innerHTML = '' + + if (this.streaming) { + const info = el('span', { text: `Streaming (${this.decoder.currentFps} fps)` }) + const stopBtn = el('button', { class: 'stop-btn', text: 'Stop' }) as HTMLButtonElement + stopBtn.addEventListener('click', () => this.stopStream()) + toolbar.append(info, this.inputToggle(), stopBtn) + return + } + + if (this.displays.length > 0) { + const label = el('label', { text: 'Select display to stream:' }) + const select = el('select') as HTMLSelectElement + for (const d of this.displays) { + const opt = el('option', { + value: String(d.id), + text: `Display ${d.id} \u2014 ${d.width}x${d.height}${d.refresh_rate ? ` @ ${Math.round(d.refresh_rate)}Hz` : ''}` + }) as HTMLOptionElement + if (d.id === this.selectedDisplayId) opt.selected = true + select.append(opt) + } + select.addEventListener('change', () => { + this.selectedDisplayId = Number(select.value) + }) + const startBtn = el('button', { class: 'start-btn', text: 'Start Stream' }) as HTMLButtonElement + startBtn.addEventListener('click', () => this.startStream()) + toolbar.append(label, select, startBtn) + } + } + + // Remote input is off by default: the agent has no `input` handler yet, so + // enabling it only produces `unknown type: input` errors until it does. + private inputToggle(): HTMLElement { + const wrap = el('label', { class: 'input-toggle' }) + const box = el('input', { type: 'checkbox' }) as HTMLInputElement + box.checked = this.inputEnabled + box.addEventListener('change', () => this.setInputEnabled(box.checked)) + wrap.title = 'Experimental: the agent does not implement an input handler yet' + wrap.append(box, el('span', { text: 'Send input (experimental)' })) + return wrap + } + + private setInputEnabled(on: boolean): void { + this.inputEnabled = on + if (on) { + this.input.attach(this.canvas) + toast('Remote input enabled \u2014 click the canvas to capture pointer', 'info') + } else { + this.input.detach() + } + this.updateToolbar() + } + + private async connect(): Promise { + if (this.destroyed || this.connecting || this.reconnectTimer !== undefined) return + this.connecting = true + this.cbs.onStatusChange(this.attempt > 0 ? 'reconnecting' : 'connecting') + + const fingerprints = this.activeFingerprints() + if (fingerprints.length === 0 && !this.config.trustedCert) { + this.connecting = false + this.cbs.onError('No fingerprint configured, and this connection is not marked as using a trusted certificate') + this.cbs.onStatusChange('disconnected') + return + } + + const url = wtUrl(this.config.host, this.config.port) + debugLog('Connecting to', url, fingerprints.length ? `(${fingerprints.length} pinned fingerprint(s))` : '(trusted certificate)') + try { + await this.transport.connect({ url, fingerprints }) + this.connecting = false + if (this.destroyed) { + this.transport.close() + return + } + this.attempt = 0 + this.cbs.onStatusChange('connected') + // The agent pushes `displays` unprompted right after connecting; this + // request is a fallback for agents that do not (or when the push failed). + this.transport.listDisplays() + } catch (e) { + this.connecting = false + const msg = `Connection failed: ${(e as Error).message}` + debugLog(msg) + this.scheduleReconnect(msg) + } + } + + // Pinned hashes for this connection. Empty means "let the browser validate + // the certificate normally" — the agent run with --cert/--key, or behind a + // reverse proxy, has no cert manager and no fingerprint to pin. + private activeFingerprints(): string[] { + if (this.config.trustedCert) return [] + return this.config.fingerprints.filter(isValidFingerprint) + } + + // Idempotent: `wt.closed` rejecting and the failed `connect()` await both + // report the same drop, so only the first one gets to arm a timer. + private scheduleReconnect(reason: string): void { + if (this.destroyed || this.connecting || this.reconnectTimer !== undefined) return + this.transport.close() + this.decoder.reset() + if (this.streaming) { + this.streaming = false + this.setInputEnabled(false) + } + this.displays = [] + this.updateToolbar() + + if (this.attempt >= RECONNECT_MAX_ATTEMPTS) { + debugLog('Giving up after', this.attempt, 'reconnect attempts') + this.cbs.onError(`${reason} \u2014 gave up after ${this.attempt} reconnect attempts`) + this.cbs.onStatusChange('disconnected') + toast('Connection lost \u2014 reconnect attempts exhausted', 'err') + return + } + + // Exponential backoff with full jitter. + const ceiling = Math.min(RECONNECT_CAP_MS, RECONNECT_BASE_MS * 2 ** this.attempt) + const delay = Math.round(Math.random() * ceiling) + this.attempt++ + debugLog(`Reconnecting in ${delay}ms (attempt ${this.attempt}/${RECONNECT_MAX_ATTEMPTS})`) + this.cbs.onStatusChange('reconnecting') + this.reconnectTimer = window.setTimeout(() => { + this.reconnectTimer = undefined + void this.connect() + }, delay) + } + + private handleMessage(msg: ControlMessage): void { + debugLog('Control message:', msg.type) + switch (msg.type) { + case 'displays': + this.displays = msg.displays + if (msg.displays.length > 0 && !this.displays.some(d => d.id === this.selectedDisplayId)) { + this.selectedDisplayId = msg.displays[0].id + } + this.updateToolbar() + break + case 'started': + this.streaming = true + this.decoder.configure(msg.codec, msg.width, msg.height) + this.stats.update({ width: msg.width, height: msg.height }) + this.cbs.onStatusChange('streaming') + this.updateToolbar() + toast(`Streaming ${msg.width}\u00d7${msg.height}`, 'ok') + break + case 'stopped': + this.streaming = false + this.displays = [] + this.decoder.reset() + this.setInputEnabled(false) + this.cbs.onStatusChange('connected') + this.updateToolbar() + // Ask for displays again so the panel returns to a usable state. + this.transport.listDisplays() + break + case 'stream-ended': + this.scheduleReconnect('Session ended') + break + case 'error': + this.handleError(msg.message) + break + case 'fingerprint-refresh': + this.cacheFingerprint(msg.fingerprint) + break + case 'pong': + // Server-side `ping` is unimplemented; kept for protocol completeness. + debugLog('pong', msg.t) + break + } + } + + // The agent answers any message it does not know with + // `{"type":"error","message":"unknown type: "}`. That is a protocol + // capability gap, not a session failure, so it must never tear the panel down. + private handleError(message: string): void { + const unknown = /^unknown type: /.test(message) + if (!unknown) { + this.cbs.onError(message) + toast(`Agent: ${message}`, 'err') + return + } + debugLog('Agent does not support this message:', message) + if (message === 'unknown type: input' && this.inputEnabled) { + // Every forwarded event would bounce back as an error; stop sending. + this.setInputEnabled(false) + toast('Agent has no input handler \u2014 remote input disabled', 'info') + } + } + + // Cache a rotated fingerprint so the next connect still validates. The agent + // pushes this unconditionally at connect time, and broadcasts it on rotation + // to sessions attached to a live stream. + private cacheFingerprint(fingerprint: string): void { + if (!isValidFingerprint(fingerprint)) { + debugLog('Ignoring malformed fingerprint push:', fingerprint) + return + } + const result = appendFingerprint(this.config.id, fingerprint) + if (!result.changed) return + this.config.fingerprints = result.fingerprints + this.cbs.onConfigChange() + debugLog('Cached agent fingerprint:', fingerprint) + toast('Agent certificate fingerprint cached', 'ok') + } + + private startStream(): void { + this.transport.start(this.selectedDisplayId) + } + + toggleStats(): void { + this.stats.toggle() + } + + private stopStream(): void { + this.transport.stop() + this.streaming = false + this.decoder.reset() + this.setInputEnabled(false) + this.updateToolbar() + } + + destroy(): void { + this.destroyed = true + if (this.reconnectTimer) window.clearTimeout(this.reconnectTimer) + this.reconnectTimer = undefined + this.input.detach() + this.transport.close() + this.decoder.close() + this.root.innerHTML = '' + } +} diff --git a/src/util.ts b/src/util.ts new file mode 100644 index 0000000..579307d --- /dev/null +++ b/src/util.ts @@ -0,0 +1,63 @@ +export function $(sel: string, root: ParentNode = document): HTMLElement { + const el = root.querySelector(sel) + if (!el) throw new Error(`missing element: ${sel}`) + return el as HTMLElement +} + +export function el( + tag: K, + attrs: Partial> = {}, + children: (Node | string)[] = [] +): HTMLElementTagNameMap[K] { + const node = document.createElement(tag) + for (const [k, v] of Object.entries(attrs)) { + if (v === undefined) continue + if (k === 'class') node.className = v + else if (k === 'text') node.textContent = v + else node.setAttribute(k, v) + } + for (const c of children) node.append(c) + return node +} + +export function formatBitrate(bps: number): string { + if (bps < 1000) return `${bps.toFixed(0)} bps` + if (bps < 1_000_000) return `${(bps / 1000).toFixed(0)} kbps` + return `${(bps / 1_000_000).toFixed(2)} Mbps` +} + +let toastTimer: number | undefined +export function toast(msg: string, kind: 'info' | 'err' | 'ok' = 'info', ms = 3200): void { + const t = document.getElementById('toast') + if (!t) return + t.textContent = msg + t.className = `toast ${kind === 'info' ? '' : kind}` + if (toastTimer) window.clearTimeout(toastTimer) + toastTimer = window.setTimeout(() => t.classList.add('hidden'), ms) +} + +const logs: string[] = [] +const MAX_LOGS = 100 +const listeners: Array<() => void> = [] + +export function debugLog(...args: unknown[]): void { + const ts = new Date().toISOString().slice(11, 23) + const msg = args.map(a => typeof a === 'object' ? JSON.stringify(a) : String(a)).join(' ') + const entry = `[${ts}] ${msg}` + console.debug('[Distance]', ...args) + logs.push(entry) + if (logs.length > MAX_LOGS) logs.shift() + listeners.forEach(fn => fn()) +} + +export function getDebugLogs(): string[] { + return [...logs] +} + +export function onDebugLogUpdate(fn: () => void): () => void { + listeners.push(fn) + return () => { + const idx = listeners.indexOf(fn) + if (idx >= 0) listeners.splice(idx, 1) + } +} diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts new file mode 100644 index 0000000..8f97dca --- /dev/null +++ b/src/vite-env.d.ts @@ -0,0 +1,3 @@ +/// + +declare module '*.css' {} diff --git a/tsconfig.json b/tsconfig.json index eeb8a62..48f02a2 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,9 +1,8 @@ { "compilerOptions": { - "target": "ES2020", + "target": "ES2022", "module": "ESNext", - "lib": ["ES2020", "DOM", "DOM.Iterable"], - "jsx": "react-jsx", + "lib": ["ES2022", "DOM", "DOM.Iterable"], "moduleResolution": "bundler", "strict": true, "noUnusedLocals": true, @@ -15,6 +14,5 @@ "isolatedModules": true, "esModuleInterop": true }, - "include": ["src/renderer"], - "exclude": ["src/main"] + "include": ["src"] } diff --git a/tsconfig.main.json b/tsconfig.main.json deleted file mode 100644 index 3326b16..0000000 --- a/tsconfig.main.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2020", - "module": "CommonJS", - "lib": ["ES2020"], - "moduleResolution": "node", - "strict": true, - "noUnusedLocals": true, - "noUnusedParameters": true, - "skipLibCheck": true, - "forceConsistentCasingInFileNames": true, - "esModuleInterop": true, - "outDir": "dist/main", - "rootDir": "src/main", - "sourceMap": true - }, - "include": ["src/main"], - "exclude": [] -} diff --git a/vite.config.ts b/vite.config.ts index 40c0b9c..e500e3f 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,13 +1,10 @@ import { defineConfig } from 'vite' -import react from '@vitejs/plugin-react' -import path from 'path' export default defineConfig({ - plugins: [react()], base: './', root: '.', build: { - outDir: 'dist/renderer', + outDir: 'dist', emptyOutDir: true, }, server: { @@ -15,9 +12,4 @@ export default defineConfig({ strictPort: true, host: true, }, - resolve: { - alias: { - '@': path.resolve(__dirname, 'src/renderer'), - }, - }, }) From b06437899bc62c581f2edb77f36166af71d34415 Mon Sep 17 00:00:00 2001 From: Hermet Date: Tue, 1 Sep 2026 09:44:34 -0400 Subject: [PATCH 2/5] fix: decode H.264 via avcC description instead of Annex B guesswork The viewer received video but decoded nothing: 0 fps, no VideoDecoder error. Two defects, both fatal on their own. - The codec string was hardcoded to avc1.42E01E (Baseline 3.0) while the agent's ffmpeg emits Main 3.2 (SPS 67 4d 40 20). Profile and level are now derived from the SPS, giving avc1.4d4020 for this stream. - Only IDR NALs were given start codes; P-frames were passed bare. With no `description` set, VideoDecoder is in Annex B mode and needs start codes on every NAL, so all 145 delta frames per keyframe were malformed. Rather than patch both, adopt the approach the agent's own embedded viewer uses: build an AVCDecoderConfigurationRecord from the SPS/PPS, pass it as `description`, and convert each access unit to AVCC with 4-byte length prefixes. Also fixes framing and timing: - NALs are grouped into access units (new AU at the first VCL NAL after a non-VCL NAL) and emitted as one EncodedVideoChunk per frame, instead of one chunk per NAL. Verified against a real 1277483-byte capture: 297 NALs -> 146 access units, 1 keyframe + 145 delta, 0 without a VCL NAL. - Timestamps derive from a frame index at the negotiated rate rather than a fixed 33333us tick, which described 30fps for a 60fps stream. The requested fps is now passed from the start request into the decoder so the two cannot disagree. - Access units arriving before the SPS/PPS are queued and flushed on configure, so a mid-stream join doesn't drop its first frames. --- src/decoder.ts | 361 ++++++++++++++++++++++++++++++++--------------- src/ui/stream.ts | 8 +- 2 files changed, 257 insertions(+), 112 deletions(-) diff --git a/src/decoder.ts b/src/decoder.ts index 2a9b933..dbf4dd4 100644 --- a/src/decoder.ts +++ b/src/decoder.ts @@ -1,19 +1,107 @@ -// H264 (Annex B) decoder on the platform WebCodecs VideoDecoder, drawing onto a -// . The agent sends raw ffmpeg H.264 Annex B bytes (00 00 00 01 / -// 00 00 01 start codes) on the unidirectional video stream, so the byte stream -// is split into NAL units and each is fed to the decoder. +/** + * H264 (Annex B) decoder on the platform WebCodecs `VideoDecoder`, drawing onto + * a . + * + * The agent streams raw ffmpeg H.264 Annex B bytes over a WebTransport + * unidirectional stream — one contiguous byte stream with no framing. So: + * 1. buffer incoming bytes and split them on Annex B start codes + * (00 00 00 01 / 00 00 01); + * 2. group NALs into access units (one per frame) using the standard rule + * "a new access unit starts at the first VCL NAL following a non-VCL NAL", + * keeping the SPS/PPS/SEI that precede a frame attached to it; + * 3. build the AVCDecoderConfigurationRecord (avcC `description`) from the + * SPS/PPS and derive the RFC 6381 `avc1.PPCCLL` codec string from the SPS — + * the profile and level must come from the bitstream, not a constant, or + * `VideoDecoder` silently decodes nothing; + * 4. convert each access unit from Annex B to AVCC (4-byte length prefixes) + * and feed it as one `EncodedVideoChunk`. + */ + +const NAL_IDR = 5 +const NAL_SPS = 7 +const NAL_PPS = 8 + +function nalType(nal: Uint8Array): number { + return nal[0] & 0x1f +} + +function isVCL(t: number): boolean { + return t >= 1 && t <= 5 +} + +/** + * Build the AVCDecoderConfigurationRecord (avcC) from SPS/PPS NALs. + * Both are passed without start codes but with their 1-byte NAL header. + */ +function buildAvcC(sps: Uint8Array, pps: Uint8Array): Uint8Array { + const out = new Uint8Array(5 + 1 + 2 + sps.length + 1 + 2 + pps.length) + let o = 0 + out[o++] = 1 // configurationVersion + out[o++] = sps[1] // AVCProfileIndication = profile_idc + out[o++] = sps[2] // profile_compatibility = constraint flags + out[o++] = sps[3] // AVCLevelIndication = level_idc + out[o++] = 0xff // 6 bits reserved + lengthSizeMinusOne = 3 (4-byte lengths) + out[o++] = 0xe1 // 3 bits reserved + numOfSequenceParameterSets = 1 + out[o++] = (sps.length >> 8) & 0xff + out[o++] = sps.length & 0xff + out.set(sps, o) + o += sps.length + out[o++] = 1 // numOfPictureParameterSets + out[o++] = (pps.length >> 8) & 0xff + out[o++] = pps.length & 0xff + out.set(pps, o) + o += pps.length + return out.subarray(0, o) +} + +/** Derive the RFC 6381 codec string (e.g. `avc1.4d4020`) from an SPS NAL. */ +function codecStringFromSps(sps: Uint8Array): string { + const h = (n: number) => n.toString(16).padStart(2, '0') + return `avc1.${h(sps[1])}${h(sps[2])}${h(sps[3])}` +} + +function concat(a: Uint8Array, b: Uint8Array): Uint8Array { + const out = new Uint8Array(a.length + b.length) + out.set(a, 0) + out.set(b, a.length) + return out +} + +/** Index of the first Annex B start code at or after `from`, else -1. */ +function findStartCode(buf: Uint8Array, from: number): number { + for (let i = from; i + 3 <= buf.length; i++) { + if (buf[i] === 0x00 && buf[i + 1] === 0x00) { + if (buf[i + 2] === 0x01) return i + if (buf[i + 2] === 0x00 && i + 3 < buf.length && buf[i + 3] === 0x01) return i + } + } + return -1 +} + +/** Length of the start code at index `i` (3 or 4). */ +function startCodeLen(buf: Uint8Array, i: number): number { + return i + 3 < buf.length && buf[i + 3] === 0x01 ? 4 : 3 +} + export class Decoder { private videoDecoder: VideoDecoder | null = null private canvas: HTMLCanvasElement private ctx: CanvasRenderingContext2D + private buf: Uint8Array = new Uint8Array(0) - private dts = 0 + private pendingAU: Uint8Array[] = [] + private pendingBeforeConfig: Uint8Array[][] = [] + private sps: Uint8Array | null = null private pps: Uint8Array | null = null + private configured = false + private codec = '' + private fps = 60 + private frameIndex = 0 private frameCount = 0 private fpsWindowStart = performance.now() - private fps = 0 + private measuredFps = 0 public onFps: ((fps: number) => void) | null = null public onFirstFrame: (() => void) | null = null @@ -29,27 +117,159 @@ export class Decoder { } } - // The agent encodes exclusively as H.264, so the codec string it reports in - // `started` is informational only. - configure(_codec: string, width?: number, height?: number): void { + /** + * Prepare for a new stream. The agent encodes H.264 only, and the real codec + * string plus dimensions are taken from the SPS once it arrives, so the + * `started` message is used just for the frame-rate hint. + */ + configure(_codec: string, _width?: number, _height?: number, fps?: number): void { + this.reset() + if (fps && fps > 0) this.fps = fps + } + + /** Feed a raw chunk of video bytes; may contain any number of NAL units. */ + feed(chunk: Uint8Array): void { + this.buf = concat(this.buf, chunk) + while (true) { + const nal = this.nextNal() + if (!nal) break + this.ingestNal(nal) + } + } + + /** + * Extract the next complete NAL. A NAL's end is only known once the following + * start code appears, so an incomplete trailing NAL stays buffered. + */ + private nextNal(): Uint8Array | null { + const start = findStartCode(this.buf, 0) + if (start === -1) { + // Keep up to 3 trailing bytes that could be the head of a start code. + const keep = Math.max(0, this.buf.length - 3) + this.buf = this.buf.subarray(keep) + return null + } + const dataStart = start + startCodeLen(this.buf, start) + const next = findStartCode(this.buf, dataStart) + if (next === -1) { + this.buf = this.buf.subarray(start) + return null + } + const nal = this.buf.subarray(dataStart, next) + this.buf = this.buf.subarray(next) + return nal + } + + private ingestNal(nal: Uint8Array): void { + if (nal.length === 0) return + const t = nalType(nal) + if (t === NAL_SPS) this.sps = nal.slice() + else if (t === NAL_PPS) this.pps = nal.slice() + + // Access-unit boundary: a VCL NAL that follows a non-VCL NAL begins a new + // frame, and the run of non-VCL NALs before it belongs to that new frame. + if (isVCL(t) && this.pendingAU.length > 0) { + const lastT = nalType(this.pendingAU[this.pendingAU.length - 1]) + if (!isVCL(lastT)) { + let split = this.pendingAU.length + while (split > 0 && !isVCL(nalType(this.pendingAU[split - 1]))) split-- + const au = this.pendingAU.slice(0, split) + if (au.length) this.emitAU(au) + this.pendingAU = this.pendingAU.slice(split) + } + } + this.pendingAU.push(nal) + } + + private emitAU(au: Uint8Array[]): void { + const carriesParams = au.some((n) => { + const t = nalType(n) + return t === NAL_SPS || t === NAL_PPS + }) + if (carriesParams && this.sps && this.pps) { + this.configureDecoder(this.sps, this.pps) + } + + if (!this.configured) { + // Hold access units until the SPS/PPS arrive and the decoder is set up. + if (this.pendingBeforeConfig.length < 300) this.pendingBeforeConfig.push(au) + return + } + if (!this.videoDecoder || this.videoDecoder.state !== 'configured') return + + // Annex B -> AVCC: replace start codes with 4-byte lengths. + let size = 0 + for (const n of au) size += 4 + n.length + const avcc = new Uint8Array(size) + let o = 0 + for (const n of au) { + avcc[o++] = (n.length >> 24) & 0xff + avcc[o++] = (n.length >> 16) & 0xff + avcc[o++] = (n.length >> 8) & 0xff + avcc[o++] = n.length & 0xff + avcc.set(n, o) + o += n.length + } + + const isKey = au.some((n) => nalType(n) === NAL_IDR) + const timestamp = Math.round((this.frameIndex * 1_000_000) / this.fps) + const duration = Math.round(1_000_000 / this.fps) + try { + this.videoDecoder.decode( + new EncodedVideoChunk({ + type: isKey ? 'key' : 'delta', + timestamp, + duration, + data: avcc as unknown as BufferSource + }) + ) + this.frameIndex++ + } catch (e) { + if (!(e instanceof DOMException && e.name === 'InvalidStateError')) { + console.warn('[decoder] decode threw', e) + } + } + } + + private configureDecoder(sps: Uint8Array, pps: Uint8Array): void { + const codec = codecStringFromSps(sps) + if (this.configured && this.codec === codec) return + if (this.videoDecoder && this.videoDecoder.state !== 'closed') { try { this.videoDecoder.reset() } catch { /* ignore */ } } + this.videoDecoder = new VideoDecoder({ output: (frame) => this.onFrame(frame), error: (e) => console.error('[decoder] error', e) }) - const cfg: VideoDecoderConfig = { codec: 'avc1.42E01E' } - if (width && height) { - cfg.codedWidth = width - cfg.codedHeight = height + + try { + this.videoDecoder.configure({ + codec, + // codedWidth/codedHeight come from the SPS inside `description`. + description: buildAvcC(sps, pps) as unknown as BufferSource, + optimizeForLatency: true + }) + } catch (e) { + console.error('[decoder] configure failed', codec, e) + return + } + + this.codec = codec + this.configured = true + console.info('[decoder] configured', codec) + + if (this.pendingBeforeConfig.length) { + const queued = this.pendingBeforeConfig + this.pendingBeforeConfig = [] + for (const au of queued) this.emitAU(au) } - this.videoDecoder.configure(cfg) } private onFrame(frame: VideoFrame): void { - const w = frame.displayWidth - const h = frame.displayHeight + const w = frame.displayWidth || frame.codedWidth + const h = frame.displayHeight || frame.codedHeight if (this.canvas.width !== w || this.canvas.height !== h) { this.canvas.width = w this.canvas.height = h @@ -58,127 +278,48 @@ export class Decoder { frame.close() this.frameCount++ + this.tickFps() if (!this.firstFrameDrawn) { this.firstFrameDrawn = true this.onFirstFrame?.() } } - feed(chunk: Uint8Array): void { - if (!this.videoDecoder || this.videoDecoder.state !== 'configured') return - this.buf = concat(this.buf, chunk) - let start = 0 - let i = 0 - const buf = this.buf - while (i + 2 < buf.length) { - if (buf[i] === 0 && buf[i + 1] === 0) { - if (i + 3 < buf.length && buf[i + 2] === 0 && buf[i + 3] === 1) { - if (start < i) { - this.decodeNal(buf.subarray(start, i)) - } - start = i + 4 - i = start - continue - } else if (buf[i + 2] === 1) { - if (start < i) { - this.decodeNal(buf.subarray(start, i)) - } - start = i + 3 - i = start - continue - } - } - i++ - } - this.buf = buf.subarray(start) - this.tickFps() - } - - private decodeNal(nal: Uint8Array): void { - if (nal.length === 0) return - const nalType = nal[0] & 0x1f - if (nalType === 7) { - this.sps = nal.slice() - return - } - if (nalType === 8) { - this.pps = nal.slice() - return - } - let data: Uint8Array - let type: 'key' | 'delta' - if (nalType === 5) { - type = 'key' - if (this.sps && this.pps) { - data = annexBConcat([this.sps, this.pps, nal]) - } else { - data = nal.slice() - } - } else if (nalType === 6) { - return - } else { - type = 'delta' - data = nal.slice() - } - this.dts += 33333 - try { - this.videoDecoder!.decode( - new EncodedVideoChunk({ type, timestamp: this.dts, data: data as unknown as BufferSource }) - ) - } catch (e) { - if (!(e instanceof DOMException && e.name === 'InvalidStateError')) { - console.warn('[decoder] decode threw', e) - } - } - } - + /** Rolling fps over decoded frames (not received bytes). */ private tickFps(): void { const now = performance.now() const dt = (now - this.fpsWindowStart) / 1000 if (dt >= 1) { - this.fps = Math.round(this.frameCount / dt) + this.measuredFps = Math.round(this.frameCount / dt) this.frameCount = 0 this.fpsWindowStart = now - this.onFps?.(this.fps) + this.onFps?.(this.measuredFps) } } get currentFps(): number { - return this.fps + return this.measuredFps } reset(): void { this.buf = new Uint8Array(0) - this.dts = 0 + this.pendingAU = [] + this.pendingBeforeConfig = [] this.sps = null this.pps = null + this.configured = false + this.codec = '' + this.frameIndex = 0 + this.frameCount = 0 + this.measuredFps = 0 + this.firstFrameDrawn = false try { this.videoDecoder?.reset() } catch { /* ignore */ } + this.videoDecoder = null } close(): void { try { this.videoDecoder?.close() } catch { /* ignore */ } this.videoDecoder = null + this.configured = false } } - -function concat(a: Uint8Array, b: Uint8Array): Uint8Array { - const out = new Uint8Array(a.length + b.length) - out.set(a, 0) - out.set(b, a.length) - return out -} - -function annexBConcat(nals: Uint8Array[]): Uint8Array { - const sc = new Uint8Array([0, 0, 0, 1]) - let total = 0 - for (const n of nals) total += 4 + n.length - const out = new Uint8Array(total) - let off = 0 - for (const n of nals) { - out.set(sc, off) - off += 4 - out.set(n, off) - off += n.length - } - return out -} diff --git a/src/ui/stream.ts b/src/ui/stream.ts index f8ebc3c..8ace984 100644 --- a/src/ui/stream.ts +++ b/src/ui/stream.ts @@ -6,6 +6,10 @@ import { el, debugLog, toast } from '../util' import { StatsOverlay } from './stats' import { appendFingerprint } from './sidebar' +// Frame rate requested from the agent. Also used as the decoder's timestamp +// base, so the two must agree or presentation timestamps drift from real time. +const REQUESTED_FPS = 60 + // Reconnect backoff: exponential with full jitter, capped. const RECONNECT_BASE_MS = 500 const RECONNECT_CAP_MS = 15_000 @@ -234,7 +238,7 @@ export class StreamPanel { break case 'started': this.streaming = true - this.decoder.configure(msg.codec, msg.width, msg.height) + this.decoder.configure(msg.codec, msg.width, msg.height, REQUESTED_FPS) this.stats.update({ width: msg.width, height: msg.height }) this.cbs.onStatusChange('streaming') this.updateToolbar() @@ -301,7 +305,7 @@ export class StreamPanel { } private startStream(): void { - this.transport.start(this.selectedDisplayId) + this.transport.start(this.selectedDisplayId, { fps: REQUESTED_FPS }) } toggleStats(): void { From af283316be8ebfbfdd1836a09054f6b1074aee42 Mon Sep 17 00:00:00 2001 From: Hermet Date: Tue, 1 Sep 2026 10:17:35 -0400 Subject: [PATCH 3/5] fix: detect access-unit boundaries from slice headers, drop pre-keyframe data MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two decoding failures, both exposed by switching captured to a PipeWire source (ffmpeg High 4.0) after only ever testing against KMS (Main 3.2). Access-unit grouping keyed off "a VCL NAL following a non-VCL NAL", which silently depends on the encoder emitting SEI or parameter sets between frames. The Main-profile stream does, so it worked; the High-profile stream emits none, and 294 consecutive P-frame slices collapsed into a single access unit. Boundaries now come from the slice header's first_mb_in_slice == 0, which is the actual picture-start signal. Verified on both real captures: 295 access units for PipeWire (was 3), 146 for KMS (unchanged), zero access units containing more than one VCL NAL in either. Access units arriving before the first keyframe are now discarded. A viewer always joins mid-GOP because the agent's encoder is already running, so the first data received references frames and a PPS that were never sent — ffprobe on a capture reports "non-existing PPS 0 referenced" / "no frame!". Feeding those to VideoDecoder raises a fatal error, which moves it to 'closed' and kills the IDR immediately behind them, so nothing ever decoded. The previous commit's pre-config queue made this worse by replaying them on configure; that queue is now bounded to 8 and only holds post-keyframe data. Also recover from fatal decoder errors instead of leaving a closed decoder in place: clear the SPS/PPS and keyframe state so the next parameter sets reconfigure from scratch. --- src/decoder.ts | 92 +++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 76 insertions(+), 16 deletions(-) diff --git a/src/decoder.ts b/src/decoder.ts index dbf4dd4..5f632b4 100644 --- a/src/decoder.ts +++ b/src/decoder.ts @@ -6,15 +6,19 @@ * unidirectional stream — one contiguous byte stream with no framing. So: * 1. buffer incoming bytes and split them on Annex B start codes * (00 00 00 01 / 00 00 01); - * 2. group NALs into access units (one per frame) using the standard rule - * "a new access unit starts at the first VCL NAL following a non-VCL NAL", - * keeping the SPS/PPS/SEI that precede a frame attached to it; + * 2. group NALs into access units (one per frame), starting a new unit at each + * VCL NAL whose slice header reports first_mb_in_slice == 0, so the + * SPS/PPS/SEI preceding a frame stay attached to it; * 3. build the AVCDecoderConfigurationRecord (avcC `description`) from the * SPS/PPS and derive the RFC 6381 `avc1.PPCCLL` codec string from the SPS — * the profile and level must come from the bitstream, not a constant, or * `VideoDecoder` silently decodes nothing; * 4. convert each access unit from Annex B to AVCC (4-byte length prefixes) * and feed it as one `EncodedVideoChunk`. + * + * Streams are always joined mid-GOP, since the agent's encoder is already + * running when a viewer connects, so access units before the first keyframe are + * discarded (see `emitAU`). */ const NAL_IDR = 5 @@ -29,6 +33,25 @@ function isVCL(t: number): boolean { return t >= 1 && t <= 5 } +/** + * True when a VCL NAL starts a new picture, i.e. its slice header's + * `first_mb_in_slice` is 0. + * + * That field is the first ue(v) Exp-Golomb value after the 1-byte NAL header, + * and ue(v) == 0 is encoded as a single set bit, so a high bit in the first + * payload byte means "this slice covers macroblock 0" — a new picture. With + * multiple slices per picture only the first has first_mb_in_slice == 0, so this + * still identifies exactly one boundary per frame. + * + * This is the reliable boundary test. Keying off "a VCL NAL following a non-VCL + * NAL" only works when the encoder emits SEI/parameter sets between frames: + * ffmpeg's Main-profile output does, but its High-profile output does not, and + * there consecutive slices would otherwise collapse into a single access unit. + */ +function startsNewPicture(nal: Uint8Array): boolean { + return nal.length > 1 && (nal[1] & 0x80) !== 0 +} + /** * Build the AVCDecoderConfigurationRecord (avcC) from SPS/PPS NALs. * Both are passed without start codes but with their 1-byte NAL header. @@ -98,6 +121,8 @@ export class Decoder { private codec = '' private fps = 60 private frameIndex = 0 + private seenKeyframe = false + private droppedBeforeKeyframe = 0 private frameCount = 0 private fpsWindowStart = performance.now() @@ -166,15 +191,14 @@ export class Decoder { if (t === NAL_SPS) this.sps = nal.slice() else if (t === NAL_PPS) this.pps = nal.slice() - // Access-unit boundary: a VCL NAL that follows a non-VCL NAL begins a new - // frame, and the run of non-VCL NALs before it belongs to that new frame. - if (isVCL(t) && this.pendingAU.length > 0) { - const lastT = nalType(this.pendingAU[this.pendingAU.length - 1]) - if (!isVCL(lastT)) { - let split = this.pendingAU.length - while (split > 0 && !isVCL(nalType(this.pendingAU[split - 1]))) split-- - const au = this.pendingAU.slice(0, split) - if (au.length) this.emitAU(au) + // A VCL NAL that starts a new picture closes the previous access unit. Any + // trailing non-VCL NALs already buffered (SPS/PPS/SEI) are parameter sets + // for the *new* picture, so they stay with it rather than being emitted. + if (isVCL(t) && startsNewPicture(nal) && this.pendingAU.length > 0) { + let split = this.pendingAU.length + while (split > 0 && !isVCL(nalType(this.pendingAU[split - 1]))) split-- + if (split > 0) { + this.emitAU(this.pendingAU.slice(0, split)) this.pendingAU = this.pendingAU.slice(split) } } @@ -190,9 +214,29 @@ export class Decoder { this.configureDecoder(this.sps, this.pps) } + const isKey = au.some((n) => nalType(n) === NAL_IDR) + + // A stream is joined mid-GOP: the agent's ffmpeg is already running, so the + // first access units received are the tail of the previous GOP and reference + // frames (and a PPS) that were never sent. Feeding those to VideoDecoder + // raises a fatal decode error, which moves it to `closed` and kills the + // IDR that follows. So everything before the first keyframe is dropped. + if (!this.seenKeyframe) { + if (!isKey) { + this.droppedBeforeKeyframe++ + return + } + this.seenKeyframe = true + if (this.droppedBeforeKeyframe > 0) { + console.info( + `[decoder] dropped ${this.droppedBeforeKeyframe} access unit(s) before the first keyframe` + ) + } + } + if (!this.configured) { - // Hold access units until the SPS/PPS arrive and the decoder is set up. - if (this.pendingBeforeConfig.length < 300) this.pendingBeforeConfig.push(au) + // Keyframe arrived but SPS/PPS have not: hold it so the GOP is not lost. + if (this.pendingBeforeConfig.length < 8) this.pendingBeforeConfig.push(au) return } if (!this.videoDecoder || this.videoDecoder.state !== 'configured') return @@ -211,7 +255,6 @@ export class Decoder { o += n.length } - const isKey = au.some((n) => nalType(n) === NAL_IDR) const timestamp = Math.round((this.frameIndex * 1_000_000) / this.fps) const duration = Math.round(1_000_000 / this.fps) try { @@ -231,6 +274,21 @@ export class Decoder { } } + /** + * A fatal VideoDecoder error moves it to `closed`, after which every decode + * throws. Recover by tearing the decoder down and waiting for the next + * SPS/PPS + keyframe, rather than leaving a dead decoder in place. + */ + private onDecoderError(e: DOMException): void { + console.error('[decoder] error', e) + this.configured = false + this.seenKeyframe = false + this.sps = null + this.pps = null + this.pendingBeforeConfig = [] + this.videoDecoder = null + } + private configureDecoder(sps: Uint8Array, pps: Uint8Array): void { const codec = codecStringFromSps(sps) if (this.configured && this.codec === codec) return @@ -241,7 +299,7 @@ export class Decoder { this.videoDecoder = new VideoDecoder({ output: (frame) => this.onFrame(frame), - error: (e) => console.error('[decoder] error', e) + error: (e) => this.onDecoderError(e) }) try { @@ -313,6 +371,8 @@ export class Decoder { this.frameCount = 0 this.measuredFps = 0 this.firstFrameDrawn = false + this.seenKeyframe = false + this.droppedBeforeKeyframe = 0 try { this.videoDecoder?.reset() } catch { /* ignore */ } this.videoDecoder = null } From b03680b586f0b5d8ef1165828823ee7eee269f1d Mon Sep 17 00:00:00 2001 From: Hermet Date: Tue, 1 Sep 2026 11:03:19 -0400 Subject: [PATCH 4/5] fix: address CodeRabbit review on the web viewer - Guard stale WebTransport.closed callbacks. The shared `closed` flag does not identify which transport settled, so a transport whose `closed` promise resolved after a reconnect had already replaced `this.wt` would emit stream-ended and make scheduleReconnect tear down the *new* transport. Both callbacks now verify they still own `this.wt`. - Require a fingerprint when trusted-certificate mode is off. The form accepted an empty fingerprint and saved `fingerprints: []`, producing a connection that StreamPanel.connect then always rejected. Anything isValidFingerprint does not accept is now refused at entry, with a distinct message for empty versus malformed. - Scope 'Trusted certificate' to CA-trusted certificates in the README and the form hint. That mode omits serverCertificateHashes and relies on browser PKI, so a self-signed certificate passed via --cert/--key is rejected; document computing its SHA-256 from the DER bytes, since --fingerprint does not print one in custom-certificate mode. - Correct the Safari requirements: WebTransport needs 26.4+, WebCodecs H.264 needs 16.4+, rather than 'no WebTransport support'. CodeRabbit's remaining finding, that each EncodedVideoChunk must be a complete access unit, it marked as already addressed in b064378. --- README.md | 28 +++++++++++++++++++++++----- src/transport.ts | 17 +++++++++-------- src/ui/sidebar.ts | 17 ++++++++++++----- 3 files changed, 44 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index 4cc8f02..dfd0d3b 100644 --- a/README.md +++ b/README.md @@ -18,9 +18,22 @@ Serve `dist/` from any static host. WebTransport requires a secure context, so use `https://` (or `localhost`) when serving it. Add a connection with the agent's host, port (`52020` by default) and the -SHA-256 certificate fingerprint the agent prints on startup. If the agent runs -with `--cert`/`--key` or sits behind a reverse proxy, tick **Trusted -certificate** instead and leave the fingerprint blank. +SHA-256 certificate fingerprint the agent prints on startup. + +Tick **Trusted certificate** and leave the fingerprint blank only when the agent +presents a **CA-trusted** certificate — served through a reverse proxy, or run +with `--cert`/`--key` pointing at a real certificate for that hostname. That mode +omits `serverCertificateHashes` entirely and relies on normal browser PKI +validation, so a self-signed certificate supplied via `--cert`/`--key` will be +rejected. + +For a self-signed certificate, keep the fingerprint. The agent only prints it +with `--fingerprint` when it manages its own certificate; for a custom one, +compute it from the DER bytes: + +```bash +openssl x509 -in cert.pem -outform der | openssl dgst -sha256 +``` ## Protocol @@ -30,5 +43,10 @@ is raw H.264 Annex B on server-initiated unidirectional streams. See ## Requirements -A browser with WebTransport and WebCodecs: Chrome/Edge 97+, or Firefox 114+ -with WebCodecs enabled. Safari does not support WebTransport yet. +A browser with both WebTransport and WebCodecs: + +| Browser | Required | +|---------|----------| +| Chrome / Edge | 97+ | +| Firefox | 114+, with WebCodecs enabled | +| Safari | 26.4+ (WebTransport); WebCodecs H.264 needs 16.4+ | diff --git a/src/transport.ts b/src/transport.ts index 101fdf2..388fb2a 100644 --- a/src/transport.ts +++ b/src/transport.ts @@ -66,14 +66,15 @@ export class Transport { const wt = new WebTransport(opts.url, buildOptions(opts.fingerprints)) this.wt = wt - wt.closed.then( - () => { - if (!this.closed) this.msgHandler?.({ type: 'stream-ended' }) - }, - () => { - if (!this.closed) this.msgHandler?.({ type: 'stream-ended' }) - } - ) + // `closed` can settle long after a reconnect has already replaced this.wt. + // Notifying then would tear down the *new* transport, so a stale callback + // must stay silent -- the shared `closed` flag alone does not distinguish + // which transport is reporting. + const notifyIfCurrent = () => { + if (this.closed || this.wt !== wt) return + this.msgHandler?.({ type: 'stream-ended' }) + } + wt.closed.then(notifyIfCurrent, notifyIfCurrent) await wt.ready diff --git a/src/ui/sidebar.ts b/src/ui/sidebar.ts index d742143..71b4404 100644 --- a/src/ui/sidebar.ts +++ b/src/ui/sidebar.ts @@ -173,7 +173,7 @@ export class Sidebar { ) const trustedHint = el('p', { class: 'form-hint hidden', - text: 'No fingerprint pinning \u2014 the browser validates the certificate itself.' + text: 'No fingerprint pinning \u2014 the browser validates the certificate itself, so this requires a CA-trusted certificate.' }) trustedBox.addEventListener('change', () => { fpRow.classList.toggle('hidden', trustedBox.checked) @@ -198,19 +198,26 @@ export class Sidebar { const port = parseInt(portInput.value, 10) || 52020 const trustedCert = trustedBox.checked const fp = normalizeFingerprint(fpInput.value) + fpInput.setCustomValidity('') if (!name || !host) return - if (!trustedCert && fp && !isValidFingerprint(fp)) { - fpInput.setCustomValidity('Expected 64 hex characters (SHA-256)') + // Without trusted-cert mode the fingerprint is mandatory: pinning is the + // only way to reach the agent's self-signed cert, and saving an empty list + // would produce a connection that StreamPanel.connect always rejects. + if (!trustedCert && !isValidFingerprint(fp)) { + fpInput.setCustomValidity( + fp + ? 'Expected 64 hex characters (SHA-256)' + : 'A fingerprint is required unless the agent has a trusted certificate' + ) fpInput.reportValidity() return } - fpInput.setCustomValidity('') const config: ConnectionConfig = { id: crypto.randomUUID(), name, host, port, - fingerprints: !trustedCert && fp ? [fp] : [], + fingerprints: trustedCert ? [] : [fp], trustedCert } this.cbs.onAdd(config) From 421453a2b089bb5810df218e90de73c9cc511922 Mon Sep 17 00:00:00 2001 From: Hermet Date: Wed, 2 Sep 2026 09:43:42 -0400 Subject: [PATCH 5/5] fix: read WebTransportReceiveStream directly, not through .readable Video never reached the decoder. incomingUnidirectionalStreams yields a WebTransportReceiveStream, which *is* a ReadableStream and has no `.readable` property, so `recv.readable.getReader()` threw a TypeError on the first video stream. readVideoLoop's outer catch swallowed it as 'stream closed', which is why there was no console error: the control plane stayed healthy, the UI reported 'Streaming', and the canvas stayed blank at its default 300x150 while zero bytes were ever handed to Decoder.feed. Verified the stream shape against a live agent from a real browser: constructor WebTransportReceiveStream, instanceof ReadableStream true, 'readable' in stream false, and reading it directly yields 179 chunks / 40494 bytes opening with 00 00 00 01 67 64 00 28. Each stream is now drained in its own task rather than inline, so a second video stream after a stop/start cycle is picked up promptly instead of waiting for the previous one to end, and per-stream read errors are reported instead of silently ending the whole loop. End to end in headless Chromium against the deployed bundle: 779 chunks submitted, 777 frames output, decoder configured avc1.640028, canvas resized to 1360x768 with 344 distinct colours sampled, and the stats overlay reporting 60 fps / 170 ms rtt / 423 kbps. --- src/transport.ts | 38 ++++++++++++++++++++++++++++---------- 1 file changed, 28 insertions(+), 10 deletions(-) diff --git a/src/transport.ts b/src/transport.ts index 388fb2a..b0f8892 100644 --- a/src/transport.ts +++ b/src/transport.ts @@ -168,17 +168,35 @@ export class Transport { const { value: recv, done: sdone } = await streamReader.read() if (sdone) break if (!recv) continue - const reader = recv.readable.getReader() - while (true) { - const { value, done } = await reader.read() - if (done) break - if (!value) continue - this.rxBytes += value.byteLength - this.windowBytes += value.byteLength - this.videoHandler?.(value) - } + // Each value is a WebTransportReceiveStream, which *is* a ReadableStream. + // It has no `.readable` property: reaching for one throws a TypeError + // that the outer catch would swallow as "stream closed", leaving the + // control plane healthy while no video ever arrives. + void this.pumpVideoStream(recv as unknown as ReadableStream) } - } catch { /* stream closed */ } + } catch { + /* session closed */ + } + } + + // Drained in its own task so a second video stream (after stop/start) is + // picked up promptly instead of waiting on the previous one to end. + private async pumpVideoStream(stream: ReadableStream): Promise { + const reader = stream.getReader() + try { + while (true) { + const { value, done } = await reader.read() + if (done) break + if (!value || value.byteLength === 0) continue + this.rxBytes += value.byteLength + this.windowBytes += value.byteLength + this.videoHandler?.(value) + } + } catch (err) { + if (!this.closed) console.warn('[transport] video stream error', err) + } finally { + try { reader.releaseLock() } catch { /* already released */ } + } } private tickStats(): void {