diff --git a/README.md b/README.md index 5049237..dfd0d3b 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,52 @@ # 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. + +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 + +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 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/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..5f632b4 --- /dev/null +++ b/src/decoder.ts @@ -0,0 +1,385 @@ +/** + * 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), 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 +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 +} + +/** + * 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. + */ +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 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 seenKeyframe = false + private droppedBeforeKeyframe = 0 + + private frameCount = 0 + private fpsWindowStart = performance.now() + private measuredFps = 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') + } + } + + /** + * 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() + + // 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) + } + } + 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) + } + + 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) { + // 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 + + // 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 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) + } + } + } + + /** + * 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 + + if (this.videoDecoder && this.videoDecoder.state !== 'closed') { + try { this.videoDecoder.reset() } catch { /* ignore */ } + } + + this.videoDecoder = new VideoDecoder({ + output: (frame) => this.onFrame(frame), + error: (e) => this.onDecoderError(e) + }) + + 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) + } + } + + private onFrame(frame: VideoFrame): void { + 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 + } + this.ctx.drawImage(frame as unknown as CanvasImageSource, 0, 0, w, h) + frame.close() + + this.frameCount++ + this.tickFps() + if (!this.firstFrameDrawn) { + this.firstFrameDrawn = true + this.onFirstFrame?.() + } + } + + /** 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.measuredFps = Math.round(this.frameCount / dt) + this.frameCount = 0 + this.fpsWindowStart = now + this.onFps?.(this.measuredFps) + } + } + + get currentFps(): number { + return this.measuredFps + } + + reset(): void { + this.buf = new Uint8Array(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 + this.seenKeyframe = false + this.droppedBeforeKeyframe = 0 + try { this.videoDecoder?.reset() } catch { /* ignore */ } + this.videoDecoder = null + } + + close(): void { + try { this.videoDecoder?.close() } catch { /* ignore */ } + this.videoDecoder = null + this.configured = false + } +} 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..b0f8892 --- /dev/null +++ b/src/transport.ts @@ -0,0 +1,277 @@ +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 + + // `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 + + 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 + // 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 { + /* 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 { + 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..71b4404 --- /dev/null +++ b/src/ui/sidebar.ts @@ -0,0 +1,231 @@ +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, so this requires a CA-trusted certificate.' + }) + 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) + fpInput.setCustomValidity('') + if (!name || !host) return + // 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 + } + const config: ConnectionConfig = { + id: crypto.randomUUID(), + name, + host, + port, + fingerprints: trustedCert ? [] : [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..8ace984 --- /dev/null +++ b/src/ui/stream.ts @@ -0,0 +1,332 @@ +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' + +// 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 +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, REQUESTED_FPS) + 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, { fps: REQUESTED_FPS }) + } + + 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'), - }, - }, })