diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml new file mode 100644 index 0000000..0d7327f --- /dev/null +++ b/.github/workflows/windows.yml @@ -0,0 +1,40 @@ +name: Windows client +on: + push: + paths: ['Windows/**', '.github/workflows/windows.yml', 'Shared/Protocol.swift'] + pull_request: + paths: ['Windows/**', '.github/workflows/windows.yml', 'Shared/Protocol.swift'] + workflow_dispatch: +permissions: + contents: read +jobs: + build: + runs-on: windows-latest + defaults: + run: + working-directory: Windows + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: '22' + cache: npm + cache-dependency-path: Windows/package-lock.json + - run: npm ci + - run: npm test + - run: npm run test:smoke + - run: npm run dist + - name: Verify packaged application + run: | + $clientTest = Start-Process -FilePath .\dist\win-unpacked\iFrame.exe -ArgumentList '--smoke-test' -WorkingDirectory (Get-Location).Path -WindowStyle Hidden -Wait -PassThru + if ($clientTest.ExitCode -ne 0) { throw "Packaged app test failed: $($clientTest.ExitCode)" } + - uses: actions/upload-artifact@v4 + with: + name: iFrame-Windows-x64 + path: Windows/dist/iFrame-*-Windows-x64.exe + - uses: actions/upload-artifact@v4 + if: always() + with: + name: Windows-test-screenshots + path: Windows/test-output/*.png + if-no-files-found: ignore diff --git a/.gitignore b/.gitignore index ba2f657..64f5cb6 100644 --- a/.gitignore +++ b/.gitignore @@ -12,5 +12,9 @@ iFrame.xcodeproj/ *.xcuserstate xcuserdata/ +# Windows client +Windows/node_modules/ +Windows/dist/ +Windows/test-output/ # Linux client Linux/build/ diff --git a/README.md b/README.md index d56b890..5284650 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,10 @@ # iFrame -Low-latency Mac → iPad remote desktop for Apple silicon. It sends a video stream instead of pixel tiles. +Low-latency Mac → iPad or Windows remote desktop for Apple silicon. It sends a video stream instead of pixel tiles. -**Requirements:** a Mac with Apple silicon running macOS 14 or later (developed on an M4 Mac mini, macOS 15), an iPad on iPadOS 17 or later, Xcode, and [XcodeGen](https://github.com/yonaskolb/XcodeGen) plus `rsvg-convert` and ImageMagick if you regenerate icons (`brew install xcodegen librsvg imagemagick`). +**Host requirements:** a Mac with Apple silicon running macOS 14 or later (developed on an M4 Mac mini, macOS 15) and Xcode command-line tools. + +**Clients:** an iPad on iPadOS 17 or later, or a Windows 10/11 x64 PC. Building the iPad app requires Xcode and [XcodeGen](https://github.com/yonaskolb/XcodeGen), plus `rsvg-convert` and ImageMagick if you regenerate icons (`brew install xcodegen librsvg imagemagick`). The [Windows client](Windows/README.md) uses Node.js to build and includes its runtime when packaged. ``` Mac (iframe-host) iPad (iFrame app) @@ -78,6 +80,24 @@ See [Linux/README.md](Linux/README.md) for scaling, key mapping and hotkeys. | Two-finger drag | Scroll | | Three-finger tap | Toolbar (on-screen keyboard, stats, disconnect) | +## Client (Windows) + +The [Windows client](Windows/README.md) connects to the same Mac host using H.264. It includes +Mac discovery, PIN login, virtual-display settings, keyboard/mouse input, fullscreen, and live stats. + +On Windows 10/11 x64 with Node.js 22.12 or newer: + +```powershell +cd Windows +npm ci +npm start +# Build a standalone portable Windows executable: +npm run dist +``` + +Open `Windows/dist/iFrame-0.1.0-Windows-x64.exe`, choose or enter your Mac's address, and enter +the host PIN. The packaged app includes its runtime. See [Windows controls, testing, and limitations](Windows/README.md). + ## Notes - The virtual display uses `CGVirtualDisplay`, a private CoreGraphics API (the same one DeskPad, BetterDisplay and Chromium's tests use). A future macOS could change it; run the host with `--no-virtual` to stream the Mac's existing display instead. - `scripts/make-icon.py` generates the icon; `scripts/build-icons.sh` renders every icon size and the app's accent color from it. diff --git a/Windows/README.md b/Windows/README.md new file mode 100644 index 0000000..a7b8ee0 --- /dev/null +++ b/Windows/README.md @@ -0,0 +1,68 @@ +# iFrame for Windows + +A Windows 10/11 x64 client for the existing `iframe-host` on your Mac. The portable app includes its runtime; users do not need Node, Python, FFmpeg, or a separate codec installation. + +## Connect + +1. Start the Mac host using the instructions in the repository README. Grant its Screen Recording and Accessibility permissions. +2. Open `iFrame-0.1.0-Windows-x64.exe` from `Windows/dist`. +3. Choose a nearby Mac, or enter its IP address, Tailscale address, or hostname. The default TCP port is **7878**. Discovery uses mDNS on the local network; manual addresses also work when discovery is unavailable. Select **☆ Save** next to the port to keep an address under **Saved Macs**; select a saved Mac to fill in its address and port. +4. Enter the host PIN, choose the display size/density, and click **Connect to Mac**. + +The app remembers saved Macs, the last address and display settings, but never saves the PIN. Preferences are stored in Electron's per-user app data directory. Windows may ask to allow local network access for discovery. The distributed executable is unsigned. + +**Retina** requests a virtual Mac display at the selected pixel size with two pixels per Mac point. **Native** gives more desktop space. **Mirror** uses the Mac's existing display. Display size is chosen at connection time; resizing the app scales the picture without restarting the Mac display. Reconnect to change resolution or density. + +## Controls + +| Input | Action | +| --- | --- | +| Mouse / trackpad | Move, left/right/middle click, drag, vertical/horizontal scroll | +| Keyboard | Physical key passthrough using the Mac's keyboard layout | +| Ctrl (default) | Mac Command; Ctrl+C / Ctrl+V become Command+C / Command+V | +| Alt | Mac Option | +| Windows key, fullscreen | Mac Command; the Start menu and Windows+ shortcuts are captured while the stream is fullscreen and focused | +| Windows key, windowed (default mapping) | Mac Control, when Windows does not intercept it | +| Ctrl+Alt+F | Toggle fullscreen | +| Ctrl+Alt+R | Release keyboard focus to the toolbar | +| Ctrl+Alt+Q | Disconnect | +| Send text | Send pasted or typed Unicode text to the focused Mac field | +| Refresh | Request a new keyframe | + +Disable **Use Ctrl for Mac Command shortcuts** for literal Ctrl → Control and Windows → Command mapping. Windows reserves shortcuts such as Alt+Tab, Windows+L, and Ctrl+Alt+Delete; these stay local. Held keys and mouse buttons are released when focus leaves the desktop. The host also releases input when the connection ends. + +## Build and test + +Requires Windows x64 and Node.js **22.12 or newer** with npm. Run from `Windows`: + +```powershell +npm ci +npm test +npm run test:smoke +npm start +npm run dist +``` + +`dist/iFrame-0.1.0-Windows-x64.exe` is the portable executable. `dist/win-unpacked/iFrame.exe` can also be launched directly, provided its neighboring files remain in place. `npm run pack` builds only that unpacked directory. Packaging downloads Electron and the NSIS packaging tools on first use. The packaged app also accepts `--smoke-test` to run the integration test offscreen and write a result to `test-output` in the current directory. + +The Node tests exercise Swift-compatible wire bytes, fragmented TCP reads, input mapping, malformed data, handshake, acknowledgements, and authentication failures. The Electron smoke test starts a loopback mock host, decodes real H.264 key and delta frames one at a time, checks pixels and acknowledgements, forwards input, and exercises disconnect/reconnect and PIN errors. It renders offscreen and writes screenshots to `test-output/`; it needs no Mac or network credentials. + +`test/desktop.h264` is a synthetic four-frame test pattern, generated with: + +```powershell +ffmpeg -f lavfi -i testsrc2=size=160x96:rate=60 -frames:v 4 -c:v libx264 -preset ultrafast -tune zerolatency -pix_fmt yuv420p -x264-params aud=1:keyint=60:bframes=0 -f h264 test/desktop.h264 +``` + +FFmpeg is only needed to regenerate that fixture. Real Mac capture, permission behavior, mDNS across your network, and sustained high-resolution performance must be checked with a running Mac host. + +## Implementation and limits + +- Uses protocol version 1 without host changes: length-prefixed TCP, PIN hello, virtual-display request, video format/frame messages, decode ACKs, ping, input, and keyframe requests. +- Requests **H.264**. WebCodecs selects a compatible hardware or software decoder. A host forced to HEVC produces an actionable error; use automatic codec selection or `--codec h264` on the host. +- Converts four-byte AVCC NAL lengths to Annex B and injects SPS/PPS on keyframes. Frames are drawn directly to a canvas and acknowledged after decode and drawing, preserving the host's flow control. Malformed frames trigger throttled keyframe recovery; repeated decoder errors close the session. +- The renderer is sandboxed with context isolation, no Node integration, a restrictive content security policy, and a narrow validated IPC bridge. Socket framing is bounded at the host protocol's 32 MiB maximum. +- The host protocol is **not encrypted**. Use a trusted LAN or Tailscale; do not expose the host directly to the Internet. +- No audio, automatic clipboard synchronization, HEVC, or local remote-cursor overlay. The Mac cursor is part of the video. The Send text button is an explicit text transfer, not clipboard sync. +- This is an Electron client, not a native WinUI application. It includes Chromium and therefore has a larger distribution and memory footprint. + +MIT, matching the repository license. Bundled Electron and dependency licenses are included in the distribution. diff --git a/Windows/assets/icon.png b/Windows/assets/icon.png new file mode 100644 index 0000000..8c2916e Binary files /dev/null and b/Windows/assets/icon.png differ diff --git a/Windows/package-lock.json b/Windows/package-lock.json new file mode 100644 index 0000000..f48a7a5 --- /dev/null +++ b/Windows/package-lock.json @@ -0,0 +1,3661 @@ +{ + "name": "iframe-windows", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "iframe-windows", + "version": "0.1.0", + "license": "MIT", + "dependencies": { + "bonjour-service": "^1.4.4", + "koffi": "^2.16.3" + }, + "devDependencies": { + "electron": "^44.5.1", + "electron-builder": "^26.15.3" + } + }, + "node_modules/@electron-internal/extract-zip": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@electron-internal/extract-zip/-/extract-zip-1.0.5.tgz", + "integrity": "sha512-+bqFCP98pLI0Tt0XQo1TmlXtwjWchISndDOxCkEcIuUgXWpBnLyRI+2DU+mesvnMMX6L1XDqYNA0lXNDHd/yiA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=22.12.0" + } + }, + "node_modules/@electron/asar": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/@electron/asar/-/asar-3.4.1.tgz", + "integrity": "sha512-i4/rNPRS84t0vSRa2HorerGRXWyF4vThfHesw0dmcWHp+cspK743UanA0suA5Q5y8kzY2y6YKrvbIUn69BCAiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "commander": "^5.0.0", + "glob": "^7.1.6", + "minimatch": "^3.0.4" + }, + "bin": { + "asar": "bin/asar.js" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/@electron/asar/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@electron/asar/node_modules/brace-expansion": { + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.21.tgz", + "integrity": "sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@electron/asar/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@electron/fuses": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@electron/fuses/-/fuses-1.8.0.tgz", + "integrity": "sha512-zx0EIq78WlY/lBb1uXlziZmDZI4ubcCXIMJ4uGjXzZW0nS19TjSPeXPAjzzTmKQlJUZm0SbmZhPKP7tuQ1SsEw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.1", + "fs-extra": "^9.0.1", + "minimist": "^1.2.5" + }, + "bin": { + "electron-fuses": "dist/bin.js" + } + }, + "node_modules/@electron/fuses/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@electron/get": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@electron/get/-/get-5.1.0.tgz", + "integrity": "sha512-3kSBtG8ObcTVfXanm5vVJ6UnBLEVmVsRk1M+vGqCuMBV+XLCbJYuWQful+yIy0GQDsSlK0kHEriEHn7SPk4EnA==", + "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": "2.5.0", + "resolved": "https://registry.npmjs.org/@electron/notarize/-/notarize-2.5.0.tgz", + "integrity": "sha512-jNT8nwH1f9X5GEITXaQ8IF/KdskvIkOFfB2CvwumsveVidzpSc+mvhhTMdAGSYF3O+Nq49lJ7y+ssODRXu06+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "fs-extra": "^9.0.1", + "promise-retry": "^2.0.1" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@electron/notarize/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@electron/osx-sign": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@electron/osx-sign/-/osx-sign-1.3.3.tgz", + "integrity": "sha512-KZ8mhXvWv2rIEgMbWZ4y33bDHyUKMXnx4M0sTyPNK/vcB81ImdeY9Ggdqy0SWbMDgmbqyQ+phgejh6V3R2QuSg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "compare-version": "^0.1.2", + "debug": "^4.3.4", + "fs-extra": "^10.0.0", + "isbinaryfile": "^4.0.8", + "minimist": "^1.2.6", + "plist": "^3.0.5" + }, + "bin": { + "electron-osx-flat": "bin/electron-osx-flat.js", + "electron-osx-sign": "bin/electron-osx-sign.js" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@electron/osx-sign/node_modules/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/@electron/rebuild": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@electron/rebuild/-/rebuild-4.2.0.tgz", + "integrity": "sha512-RKL/O+jGoXJMxrx/5771y1n0xTKmFuOYGO3gMmwypBM6rsH0kou0mswwdXA2JrhIkE4xyC7v9vGk0n6NPzgOxQ==", + "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": "2.0.3", + "resolved": "https://registry.npmjs.org/@electron/universal/-/universal-2.0.3.tgz", + "integrity": "sha512-Wn9sPYIVFRFl5HmwMJkARCCf7rqK/EurkfQ/rJZ14mHP3iYTjZSIOSVonEAnhWeAXwtw7zOekGRlc6yTtZ0t+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@electron/asar": "^3.3.1", + "@malept/cross-spawn-promise": "^2.0.0", + "debug": "^4.3.1", + "dir-compare": "^4.2.0", + "fs-extra": "^11.1.1", + "minimatch": "^9.0.3", + "plist": "^3.1.0" + }, + "engines": { + "node": ">=16.4" + } + }, + "node_modules/@electron/universal/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@electron/universal/node_modules/brace-expansion": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.7.tgz", + "integrity": "sha512-uZbew1NqdmPDTMJ8ah1y+b+9QEJrfkXFk3RcTQw3X0jW/xRUvFKsg1CfQdSYGdTbXZWExtU3J3ccxtnfw1Fi0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@electron/universal/node_modules/fs-extra": { + "version": "11.4.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.4.1.tgz", + "integrity": "sha512-KYAb4c9BJQI6QqGKthV68OHe0badztdXJWKo0WtBA9IuCFPTKvE5ZdUBglP833aMjhaSPNO4A5j/EkzZtGlKjA==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/@electron/universal/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@electron/windows-sign": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@electron/windows-sign/-/windows-sign-1.2.2.tgz", + "integrity": "sha512-dfZeox66AvdPtb2lD8OsIIQh12Tp0GNCRUDfBHIKGpbmopZto2/A8nSpYYLoedPIHpqkeblZ/k8OV0Gy7PYuyQ==", + "dev": true, + "license": "BSD-2-Clause", + "optional": true, + "peer": true, + "dependencies": { + "cross-dirname": "^0.1.0", + "debug": "^4.3.4", + "fs-extra": "^11.1.1", + "minimist": "^1.2.8", + "postject": "^1.0.0-alpha.6" + }, + "bin": { + "electron-windows-sign": "bin/electron-windows-sign.js" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/@electron/windows-sign/node_modules/fs-extra": { + "version": "11.4.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.4.1.tgz", + "integrity": "sha512-KYAb4c9BJQI6QqGKthV68OHe0badztdXJWKo0WtBA9IuCFPTKvE5ZdUBglP833aMjhaSPNO4A5j/EkzZtGlKjA==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "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/@leichtgewicht/ip-codec": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz", + "integrity": "sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw==", + "license": "MIT" + }, + "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==", + "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", + "dependencies": { + "cross-spawn": "^7.0.1" + }, + "engines": { + "node": ">= 12.13.0" + } + }, + "node_modules/@malept/flatpak-bundler": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@malept/flatpak-bundler/-/flatpak-bundler-0.4.0.tgz", + "integrity": "sha512-9QOtNffcOF/c1seMCDnjckb3R9WHcG34tky+FHpNKKCW0wc/scYLwMtO+ptyGUfMW0/b/n4qRiALlaFHc9Oj7Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "fs-extra": "^9.0.0", + "lodash": "^4.17.15", + "tmp-promise": "^3.0.2" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@malept/flatpak-bundler/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@noble/hashes": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.4.0.tgz", + "integrity": "sha512-X5XaVWZIBCT7HHZGm5I7ZQXDwLG+bGXuSrMQAW+7Zvl87h1kmc1ZB1VSRJcpUfoUrGQp4Fkoxm5kZ+Ms+aW+eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@peculiar/asn1-schema": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-schema/-/asn1-schema-2.10.0.tgz", + "integrity": "sha512-GhokD41lV4gQrrLYm3wCkHfBOnJrnhDMgt4XeMW8gzfE1UdJqIuSwsE+ggf82XBjUXRJylcm+KIGQFa4utIVLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@peculiar/utils": "^2.0.2", + "asn1js": "^3.0.10", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@peculiar/json-schema": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/@peculiar/json-schema/-/json-schema-1.1.12.tgz", + "integrity": "sha512-coUfuoMeIB7B8/NMekxaDzLhaYmp0HZNPEjYRm9goRou8UZIC3z21s0sL9AWoCw4EG876QyO3kYrc61WNF9B/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@peculiar/utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@peculiar/utils/-/utils-2.0.3.tgz", + "integrity": "sha512-+oL3HPFRIZ1St2K50lWCXiioIgSoxzz7R1J3uF6neO2yl1sgmpgY6XXJH4BdpoDkMWznQTeYF6oWNDZLCdQ4eQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/webcrypto": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/@peculiar/webcrypto/-/webcrypto-1.7.1.tgz", + "integrity": "sha512-ODOov0sGMJMf3jPonOkgGqPknTsu+DdQ7kD++gz8aI+aFMOMHFbWAA2taqXXVTdP+OTOQR/znGvSpmkeI0WTYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.7.0", + "@peculiar/json-schema": "^1.1.12", + "@peculiar/utils": "^2.0.2", + "tslib": "^2.8.1", + "webcrypto-core": "^1.9.2" + }, + "engines": { + "node": ">=14.18.0" + } + }, + "node_modules/@sindresorhus/is": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", + "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/is?sponsor=1" + } + }, + "node_modules/@szmarczak/http-timer": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz", + "integrity": "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==", + "dev": true, + "license": "MIT", + "dependencies": { + "defer-to-connect": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@types/cacheable-request": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz", + "integrity": "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-cache-semantics": "*", + "@types/keyv": "^3.1.4", + "@types/node": "*", + "@types/responselike": "^1.0.0" + } + }, + "node_modules/@types/debug": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.13.tgz", + "integrity": "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/ms": "*" + } + }, + "node_modules/@types/fs-extra": { + "version": "9.0.13", + "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-9.0.13.tgz", + "integrity": "sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/http-cache-semantics": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-L3LgimLHXtGkWikKnsPg0/VFx9OGZaC+eN1u4r+OB1XRqH3meBIAVC2zr1WdMH+RHmnRkqliQAOHNJ/E0j/e0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/keyv": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz", + "integrity": "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "24.19.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.19.1.tgz", + "integrity": "sha512-aS3/DG0oM05K0RIXXP+hKjinGG5IgSSVGzswZxW3O0sS3pH4/fycXundUC9XsszgKCk4gHXylTEK6hyFxVxnoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": ">=7.24.0 <7.24.7" + } + }, + "node_modules/@types/responselike": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.3.tgz", + "integrity": "sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@xmldom/xmldom": { + "version": "0.8.15", + "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.15.tgz", + "integrity": "sha512-/5NV/vDALVFDXgLmfsy9TRCBlKwO2LNBFzpzvb9iIj+jR+eSc6DLYYvVOdivT/jm7MtU6TebYuRmzEOI7w40UA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "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/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/app-builder-lib": { + "version": "26.15.3", + "resolved": "https://registry.npmjs.org/app-builder-lib/-/app-builder-lib-26.15.3.tgz", + "integrity": "sha512-2VnyWkqsP5v5XbBhL3tD5Syx8iNPBYsoU7kY4S2fz7wg8Rj/nztWKCUzGKaFRTv0Xwf3/H058CR1Kvtd/3lRow==", + "dev": true, + "license": "MIT", + "dependencies": { + "@electron/asar": "3.4.1", + "@electron/fuses": "^1.8.0", + "@electron/get": "^3.0.0", + "@electron/notarize": "2.5.0", + "@electron/osx-sign": "1.3.3", + "@electron/rebuild": "^4.0.4", + "@electron/universal": "2.0.3", + "@malept/flatpak-bundler": "^0.4.0", + "@noble/hashes": "^2.2.0", + "@peculiar/webcrypto": "^1.7.1", + "@types/fs-extra": "9.0.13", + "ajv": "^8.18.0", + "asn1js": "^3.0.10", + "async-exit-hook": "^2.0.1", + "builder-util": "26.15.3", + "builder-util-runtime": "9.7.0", + "chromium-pickle-js": "^0.2.0", + "ci-info": "4.3.1", + "debug": "^4.3.4", + "dotenv": "^16.4.5", + "dotenv-expand": "^11.0.6", + "ejs": "^3.1.8", + "electron-publish": "26.15.3", + "fs-extra": "^10.1.0", + "hosted-git-info": "^4.1.0", + "isbinaryfile": "^5.0.0", + "jiti": "^2.4.2", + "js-yaml": "^4.1.0", + "json5": "^2.2.3", + "lazy-val": "^1.0.5", + "minimatch": "^10.2.5", + "pkijs": "^3.4.0", + "plist": "3.1.0", + "proper-lockfile": "^4.1.2", + "resedit": "^1.7.0", + "semver": "~7.7.3", + "tar": "^7.5.7", + "temp-file": "^3.4.0", + "tiny-async-pool": "1.3.0", + "unzipper": "^0.12.3", + "which": "^5.0.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "dmg-builder": "26.15.3", + "electron-builder-squirrel-windows": "26.15.3" + } + }, + "node_modules/app-builder-lib/node_modules/@electron/get": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@electron/get/-/get-3.1.0.tgz", + "integrity": "sha512-F+nKc0xW+kVbBRhFzaMgPy3KwmuNTYX1fx6+FxxoSnNgwYX6LD7AKBTWkU0MQ6IBoe7dz069CNkR673sPAgkCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "env-paths": "^2.2.0", + "fs-extra": "^8.1.0", + "got": "^11.8.5", + "progress": "^2.0.3", + "semver": "^6.2.0", + "sumchecker": "^3.0.1" + }, + "engines": { + "node": ">=14" + }, + "optionalDependencies": { + "global-agent": "^3.0.0" + } + }, + "node_modules/app-builder-lib/node_modules/@electron/get/node_modules/fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/app-builder-lib/node_modules/@electron/get/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/app-builder-lib/node_modules/ci-info": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.3.1.tgz", + "integrity": "sha512-Wdy2Igu8OcBpI2pZePZ5oWjPC38tmDVx5WKUXKwlLYkA0ozo85sLsLvkBbBn/sZaSCMFOGZJ14fvW9t5/d7kdA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/app-builder-lib/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/app-builder-lib/node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "dev": true, + "license": "MIT", + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/app-builder-lib/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/app-builder-lib/node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/asn1js": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/asn1js/-/asn1js-3.0.10.tgz", + "integrity": "sha512-S2s3aOytiKdFRdulw2qPE51MzjzVOisppcVv7jVFR+Kw0kxwvFrDcYA0h7Ndqbmj0HkMIXYWaoj7fli8kgx1eg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "pvtsutils": "^1.3.6", + "pvutils": "^1.1.5", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "dev": true, + "license": "MIT" + }, + "node_modules/async-exit-hook": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/async-exit-hook/-/async-exit-hook-2.0.1.tgz", + "integrity": "sha512-NW2cX8m1Q7KPA7a5M2ULQeZ2wR5qI5PAbw5L0UOMxdioVk9PMZ0h1TmyZEkPYrCvYjDlFICusOu1dlEKAAeXBw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/at-least-node": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", + "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/aws4": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.13.2.tgz", + "integrity": "sha512-lHe62zvbTB5eEABUVi/AwVh0ZKY9rMMDhmm+eeyuuUQbQ3+J+fONVQOZyj+DdrvD4BY33uYniyRJ4UJIaSKAfw==", + "dev": true, + "license": "MIT" + }, + "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/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/bluebird": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/bonjour-service": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/bonjour-service/-/bonjour-service-1.4.4.tgz", + "integrity": "sha512-jCZcVv7eoc4QesRscwEZtSROBen+6LpKAmBIsQYQrsAeVHLyMXWX/t6eIV5KiRZYNUBl8eVqImEEMQ8L5+c/Kw==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "multicast-dns": "^7.2.5" + } + }, + "node_modules/boolean": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/boolean/-/boolean-3.2.0.tgz", + "integrity": "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==", + "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/brace-expansion": { + "version": "5.0.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.12.tgz", + "integrity": "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/builder-util": { + "version": "26.15.3", + "resolved": "https://registry.npmjs.org/builder-util/-/builder-util-26.15.3.tgz", + "integrity": "sha512-q2hn7Mbo2nFNkVekPiHFx6Nfo3hURmES3tfBn+k5Pqxl2RkmP3QGqZUhH/q9Pch/4G05NRhPjDlVj1O8q4Txvw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/debug": "^4.1.6", + "builder-util-runtime": "9.7.0", + "chalk": "^4.1.2", + "cross-spawn": "^7.0.6", + "debug": "^4.3.4", + "fs-extra": "^10.1.0", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.0", + "js-yaml": "^4.1.0", + "sanitize-filename": "^1.6.3", + "source-map-support": "^0.5.19", + "stat-mode": "^1.0.0", + "temp-file": "^3.4.0", + "tiny-async-pool": "1.3.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/builder-util-runtime": { + "version": "9.7.0", + "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.7.0.tgz", + "integrity": "sha512-g/kR520giAFYkSXTzcmF3kqQq7wi8F6N6SzeDgZrqTBN+VHdmgWOyTdD1yD7AATDId/yXLvuP34CxW46/BwCdw==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.4", + "sax": "^1.2.4" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/bytestreamjs": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/bytestreamjs/-/bytestreamjs-2.0.1.tgz", + "integrity": "sha512-U1Z/ob71V/bXfVABvNr/Kumf5VyeQRBEm6Txb0PQ6S7V5GpBM3w4Cbqz/xPDicR5tN0uvDifng8C+5qECeGwyQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/cacheable-lookup": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz", + "integrity": "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.6.0" + } + }, + "node_modules/cacheable-request": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.4.tgz", + "integrity": "sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==", + "dev": true, + "license": "MIT", + "dependencies": { + "clone-response": "^1.0.2", + "get-stream": "^5.1.0", + "http-cache-semantics": "^4.0.0", + "keyv": "^4.0.0", + "lowercase-keys": "^2.0.0", + "normalize-url": "^6.0.1", + "responselike": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/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/chromium-pickle-js": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/chromium-pickle-js/-/chromium-pickle-js-0.2.0.tgz", + "integrity": "sha512-1R5Fho+jBq0DDydt+/vHWj5KJNJCKdARKOCwZUen84I5BreWoLqRLANH1U87eJy1tiASPtMnGqJJq0ZsLoRPOw==", + "dev": true, + "license": "MIT" + }, + "node_modules/ci-info": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.4.0.tgz", + "integrity": "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/clone-response": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.3.tgz", + "integrity": "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-response": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz", + "integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/compare-version": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/compare-version/-/compare-version-0.1.2.tgz", + "integrity": "sha512-pJDh5/4wrEnXX/VWRZvruAGHkzKdr46z11OlTPN+VrATlWWhSKewNCJ1futCO5C7eJB3nPMFZA1LeYtcFboZ2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-dirname": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/cross-dirname/-/cross-dirname-0.1.0.tgz", + "integrity": "sha512-+R08/oI0nl3vfPcqftZRpytksBXDzOUveBq/NBVx0sUp1axwzPQrKinNx5yd5sxPu8j1wIy8AfnVQ+5eFdha6Q==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true + }, + "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/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/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/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/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/decompress-response/node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/defer-to-connect": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", + "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/detect-node": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", + "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/dir-compare": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/dir-compare/-/dir-compare-4.2.0.tgz", + "integrity": "sha512-2xMCmOoMrdQIPHdsTawECdNPwlVFB9zGcz3kuhmBO6U3oU+UQjsue0i8ayLKpgBcm+hcXPMVSGUN9d+pvJ6+VQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimatch": "^3.0.5", + "p-limit": "^3.1.0 " + } + }, + "node_modules/dir-compare/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/dir-compare/node_modules/brace-expansion": { + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.21.tgz", + "integrity": "sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/dir-compare/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/dmg-builder": { + "version": "26.15.3", + "resolved": "https://registry.npmjs.org/dmg-builder/-/dmg-builder-26.15.3.tgz", + "integrity": "sha512-O3zJUFUYHJKgzPqioHxfxzBzlSC1eXCSr79gMSBKBP5AgjjpmrydMsMLotEg9fAJF36vdUncb+4ndRNxoPdlSQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "app-builder-lib": "26.15.3", + "builder-util": "26.15.3", + "fs-extra": "^10.1.0", + "js-yaml": "^4.1.0" + } + }, + "node_modules/dns-packet": { + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-5.6.1.tgz", + "integrity": "sha512-l4gcSouhcgIKRvyy99RNVOgxXiicE+2jZoNmaNmZ6JXiGajBOJAesk1OBlJuM5k2c+eudGdLxDqXuPCKIj6kpw==", + "license": "MIT", + "dependencies": { + "@leichtgewicht/ip-codec": "^2.0.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dotenv-expand": { + "version": "11.0.7", + "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-11.0.7.tgz", + "integrity": "sha512-zIHwmZPRshsCdpMDyVsqGmgyP0yT8GAgXUnkdAoJisxvf33k7yO6OuoKmcTGuXPWSsm8Oh88nZicRLA9Y0rUeA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "dotenv": "^16.4.5" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/duplexer2": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz", + "integrity": "sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "readable-stream": "^2.0.2" + } + }, + "node_modules/ejs": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", + "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "jake": "^10.8.5" + }, + "bin": { + "ejs": "bin/cli.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/electron": { + "version": "44.5.1", + "resolved": "https://registry.npmjs.org/electron/-/electron-44.5.1.tgz", + "integrity": "sha512-lx7AYoFIiyvEvgvpyWO9kOC60xtNPpiPZAth6Fmg9NutzpCAMLwpUlaBY2EXfZyIgUdzEmLMj9KvK0Vrokl1yQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@electron-internal/extract-zip": "^1.0.1", + "@electron/get": "^5.0.0", + "@types/node": "^24.9.0" + }, + "bin": { + "electron": "cli.js", + "install-electron": "install.js" + }, + "engines": { + "node": ">= 22.12.0" + } + }, + "node_modules/electron-builder": { + "version": "26.15.3", + "resolved": "https://registry.npmjs.org/electron-builder/-/electron-builder-26.15.3.tgz", + "integrity": "sha512-a1KM5heqS3gQCZzizXEI8RjJy3QVogULPdeSknt76uLDpBIW/HDGsMg/XgP0riP6PI9COsRvFITKKGDqA8fJxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "app-builder-lib": "26.15.3", + "builder-util": "26.15.3", + "builder-util-runtime": "9.7.0", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "dmg-builder": "26.15.3", + "fs-extra": "^10.1.0", + "lazy-val": "^1.0.5", + "simple-update-notifier": "2.0.0", + "yargs": "^17.6.2" + }, + "bin": { + "electron-builder": "cli.js", + "install-app-deps": "install-app-deps.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/electron-builder-squirrel-windows": { + "version": "26.15.3", + "resolved": "https://registry.npmjs.org/electron-builder-squirrel-windows/-/electron-builder-squirrel-windows-26.15.3.tgz", + "integrity": "sha512-Jc19XPV9y9+2bAdZPkXuVNGNIEFBq9poHC61l8Kv6FdK7DRG3+Ic0rerC0DXOaeHNz8yW0fg/JnF8GQROOF5MA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "app-builder-lib": "26.15.3", + "builder-util": "26.15.3", + "electron-winstaller": "5.4.0" + } + }, + "node_modules/electron-publish": { + "version": "26.15.3", + "resolved": "https://registry.npmjs.org/electron-publish/-/electron-publish-26.15.3.tgz", + "integrity": "sha512-g/2bn8YTavY4cuS5F+jOS7zmZbXXBV8KZ8yHKfJjFPoKtzBqrpCdNPxBd3tqdBwP7BVd0lGzf7Bk2s0KesWZ4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/fs-extra": "^9.0.11", + "aws4": "^1.13.2", + "builder-util": "26.15.3", + "builder-util-runtime": "9.7.0", + "chalk": "^4.1.2", + "form-data": "^4.0.5", + "fs-extra": "^10.1.0", + "lazy-val": "^1.0.5", + "mime": "^2.5.2" + } + }, + "node_modules/electron-winstaller": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/electron-winstaller/-/electron-winstaller-5.4.0.tgz", + "integrity": "sha512-bO3y10YikuUwUuDUQRM4KfwNkKhnpVO7IPdbsrejwN9/AABJzzTQ4GeHwyzNSrVO+tEH3/Np255a3sVZpZDjvg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@electron/asar": "^3.2.1", + "debug": "^4.1.1", + "fs-extra": "^7.0.1", + "lodash": "^4.17.21", + "temp": "^0.9.0" + }, + "engines": { + "node": ">=8.0.0" + }, + "optionalDependencies": { + "@electron/windows-sign": "^1.1.2" + } + }, + "node_modules/electron-winstaller/node_modules/fs-extra": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", + "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "graceful-fs": "^4.1.2", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/electron-winstaller/node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "dev": true, + "license": "MIT", + "peer": true, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/electron-winstaller/node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "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/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es6-error": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", + "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.8.tgz", + "integrity": "sha512-GZMtZUTNRpOVIECoXwLNZS5xUGE+mVNbTB8h/7Rwh2TFWcBQiPzTgyZi05BF9UMZKkLJv8XBRJTlU7zg8+ZfMg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "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/filelist": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.6.tgz", + "integrity": "sha512-5giy2PkLYY1cP39p17Ech+2xlpTRL9HLspOfEgm0L6CwBXBTgsK5ou0JtzYuepxkaQ/tvhCFIJ5uXo0OrM2DxA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "minimatch": "^5.0.1" + } + }, + "node_modules/filelist/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/filelist/node_modules/brace-expansion": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.7.tgz", + "integrity": "sha512-uZbew1NqdmPDTMJ8ah1y+b+9QEJrfkXFk3RcTQw3X0jW/xRUvFKsg1CfQdSYGdTbXZWExtU3J3ccxtnfw1Fi0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/filelist/node_modules/minimatch": { + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", + "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/form-data": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", + "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.4", + "mime-types": "^2.1.35" + }, + "engines": { + "node": ">= 6" + } + }, + "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.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "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": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.21.tgz", + "integrity": "sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/global-agent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/global-agent/-/global-agent-3.0.0.tgz", + "integrity": "sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==", + "dev": true, + "license": "BSD-3-Clause", + "optional": true, + "dependencies": { + "boolean": "^3.0.1", + "es6-error": "^4.1.1", + "matcher": "^3.0.0", + "roarr": "^2.15.3", + "semver": "^7.3.2", + "serialize-error": "^7.0.1" + }, + "engines": { + "node": ">=10.0" + } + }, + "node_modules/globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/got": { + "version": "11.8.6", + "resolved": "https://registry.npmjs.org/got/-/got-11.8.6.tgz", + "integrity": "sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sindresorhus/is": "^4.0.0", + "@szmarczak/http-timer": "^4.0.5", + "@types/cacheable-request": "^6.0.1", + "@types/responselike": "^1.0.0", + "cacheable-lookup": "^5.0.3", + "cacheable-request": "^7.0.2", + "decompress-response": "^6.0.0", + "http2-wrapper": "^1.0.0-beta.5.2", + "lowercase-keys": "^2.0.0", + "p-cancelable": "^2.0.0", + "responselike": "^2.0.0" + }, + "engines": { + "node": ">=10.19.0" + }, + "funding": { + "url": "https://github.com/sindresorhus/got?sponsor=1" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hosted-git-info": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz", + "integrity": "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==", + "dev": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/http-cache-semantics": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.3.0.tgz", + "integrity": "sha512-M5t5LlJpS1UHMjvwRQVdFHvPISGeLAxNcrWuJkeGh0KxsqCHZ1O3NXZU/8x7cD0BDcGW8kapxMKTvwlqrNkHkA==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/http2-wrapper": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz", + "integrity": "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "quick-lru": "^5.1.1", + "resolve-alpn": "^1.0.0" + }, + "engines": { + "node": ">=10.19.0" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/isbinaryfile": { + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-5.0.7.tgz", + "integrity": "sha512-gnWD14Jh3FzS3CPhF0AxNOJ8CxqeblPTADzI38r0wt8ZyQl5edpy75myt08EG2oKvpyiqSqsx+Wkz9vtkbTqYQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/gjtorikian/" + } + }, + "node_modules/isexe": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.5.tgz", + "integrity": "sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/jake": { + "version": "10.9.4", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.4.tgz", + "integrity": "sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "async": "^3.2.6", + "filelist": "^1.0.4", + "picocolors": "^1.1.1" + }, + "bin": { + "jake": "bin/cli.js" + }, + "engines": { + "node": ">=10" + } + }, + "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/js-yaml": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.2.tgz", + "integrity": "sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", + "dev": true, + "license": "ISC", + "optional": true + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "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/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/koffi": { + "version": "2.16.3", + "resolved": "https://registry.npmjs.org/koffi/-/koffi-2.16.3.tgz", + "integrity": "sha512-E9y1AsgYGlaxMhcZzHr8y96QF2U5XzA12GGVAfbWqIubTwPNMXQarfBzePNXHe0xtIEtNd6ifAv3GAKYGUeBAQ==", + "hasInstallScript": true, + "license": "MIT", + "funding": { + "url": "https://liberapay.com/Koromix" + } + }, + "node_modules/lazy-val": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/lazy-val/-/lazy-val-1.0.5.tgz", + "integrity": "sha512-0/BnGCCfyUMkBpeDgWihanIAF9JmZhHBgUhEqzvf+adhNGLoP6TaiI5oF8oyb3I45P+PcnrqihSf01M0l0G5+Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/lowercase-keys": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", + "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/matcher": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz", + "integrity": "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "escape-string-regexp": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "dev": true, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-response": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", + "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/minimatch": { + "version": "10.2.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", + "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.8" + }, + "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", + "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/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "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/multicast-dns": { + "version": "7.2.5", + "resolved": "https://registry.npmjs.org/multicast-dns/-/multicast-dns-7.2.5.tgz", + "integrity": "sha512-2eznPJP8z2BFLX50tf0LuODrpINqP1RVIm/CObbTcBRITQgmC/TjcREF1NeTBzIcR5XO/ukWo+YHOjBbFwIupg==", + "license": "MIT", + "dependencies": { + "dns-packet": "^5.2.2", + "thunky": "^1.0.2" + }, + "bin": { + "multicast-dns": "cli.js" + } + }, + "node_modules/node-abi": { + "version": "4.36.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-4.36.0.tgz", + "integrity": "sha512-zrvE1Qy5ElbZVqmJFSiNfwzNZzUCepasgp/4dw0Z0fsn40LJkAk9FA9oMh2SKMeNFRRe/3IfeOKvWwWr8u4SmQ==", + "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.4.0", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-12.4.0.tgz", + "integrity": "sha512-OMcPNvqTCFUnNaBlmdgq+lfNqY7gTiSmNRDjY3uAXRyudeKZEZxu3CLtjMQrx4zZxCX2b/mpNqTtwuCJgXhHkw==", + "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/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/node-gyp/node_modules/undici": { + "version": "6.29.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.29.0.tgz", + "integrity": "sha512-R+RODBqp6i2pPflGdq+xIOUkl+RNfGgHwoinecKu/JCuf2uO06cOKoDbI2P7Dn6KcswdKwrczbU6IYJ6K8X+wg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.17" + } + }, + "node_modules/node-gyp/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/node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "dev": true, + "license": "MIT" + }, + "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/normalize-url": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", + "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/p-cancelable": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz", + "integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/pe-library": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/pe-library/-/pe-library-0.4.1.tgz", + "integrity": "sha512-eRWB5LBz7PpDu4PUlwT0PhnQfTQJlDDdPa35urV4Osrm0t0AqQFGn+UIkU3klZvwJ8KPO3VbBFsXquA6p6kqZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12", + "npm": ">=6" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/jet2jet" + } + }, + "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/sponsors/jonschlinkert" + } + }, + "node_modules/pkijs": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/pkijs/-/pkijs-3.4.1.tgz", + "integrity": "sha512-Oo/NZcSWccq8KyoG7gLE9fnltgHns+pNCjCAp/WmjsUySi+sX7y4z4Xqu4fVb42CDHzRPl33fjzT15V1wvcyhA==", + "dev": true, + "license": "BSD-3-Clause", + "workspaces": [ + "website" + ], + "dependencies": { + "@noble/hashes": "1.8.0", + "asn1js": "^3.0.6", + "bytestreamjs": "^2.0.1", + "pvtsutils": "^1.3.6", + "pvutils": "^1.1.3", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/pkijs/node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/plist": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/plist/-/plist-3.1.0.tgz", + "integrity": "sha512-uysumyrvkUX0rX/dEVqt8gC3sTBzd4zoWfLeS29nb53imdaXVvLINYXTI2GNqzaMuvacNx4uJQ8+b3zXR0pkgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@xmldom/xmldom": "^0.8.8", + "base64-js": "^1.5.1", + "xmlbuilder": "^15.1.1" + }, + "engines": { + "node": ">=10.4.0" + } + }, + "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", + "optional": true, + "peer": true, + "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", + "optional": true, + "peer": true, + "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/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true, + "license": "MIT" + }, + "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/proper-lockfile": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.2.tgz", + "integrity": "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "retry": "^0.12.0", + "signal-exit": "^3.0.2" + } + }, + "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/pvtsutils": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/pvtsutils/-/pvtsutils-1.3.6.tgz", + "integrity": "sha512-PLgQXQ6H2FWCaeRak8vvk1GW462lMxB5s3Jm673N82zI4vqtVUPuZdffdZbPDFRoU8kAhItWFtPCWiPpp4/EDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.8.1" + } + }, + "node_modules/pvutils": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pvutils/-/pvutils-1.2.0.tgz", + "integrity": "sha512-BbubeCEyTuQjVMakvJQ/Sxbc93F2pwmbsxONT/ZRrwU7Ua38d8unYTwXpTVLAKJ4BDuH9IGztCjQcd/N/39Dvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/quick-lru": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", + "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resedit": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/resedit/-/resedit-1.7.2.tgz", + "integrity": "sha512-vHjcY2MlAITJhC0eRD/Vv8Vlgmu9Sd3LX9zZvtGzU5ZImdTN3+d6e/4mnTyV8vEbyf1sgNIrWxhWlrys52OkEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pe-library": "^0.4.1" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/jet2jet" + } + }, + "node_modules/resolve-alpn": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz", + "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==", + "dev": true, + "license": "MIT" + }, + "node_modules/responselike": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.1.tgz", + "integrity": "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "lowercase-keys": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/rimraf": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", + "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "license": "ISC", + "peer": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + } + }, + "node_modules/roarr": { + "version": "2.15.4", + "resolved": "https://registry.npmjs.org/roarr/-/roarr-2.15.4.tgz", + "integrity": "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==", + "dev": true, + "license": "BSD-3-Clause", + "optional": true, + "dependencies": { + "boolean": "^3.0.1", + "detect-node": "^2.0.4", + "globalthis": "^1.0.1", + "json-stringify-safe": "^5.0.1", + "semver-compare": "^1.0.0", + "sprintf-js": "^1.1.2" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, + "license": "MIT" + }, + "node_modules/sanitize-filename": { + "version": "1.6.4", + "resolved": "https://registry.npmjs.org/sanitize-filename/-/sanitize-filename-1.6.4.tgz", + "integrity": "sha512-9ZyI08PsvdQl2r/bBIGubpVdR3RR9sY6RDiWFPreA21C/EFlQhmgo20UZlNjZMMZNubusLhAQozkA0Od5J21Eg==", + "dev": true, + "license": "WTFPL OR ISC", + "dependencies": { + "truncate-utf8-bytes": "^1.0.0" + } + }, + "node_modules/sax": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.6.1.tgz", + "integrity": "sha512-42tBVwLWnaQvW5zc4HbZrTuWccECCZfBi92FDuwtqxasH+JbPB3/FOKb1m222K42R4WxuxzzMsTswfzgtSu64Q==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=11.0.0" + } + }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/semver-compare": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz", + "integrity": "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/serialize-error": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-7.0.1.tgz", + "integrity": "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "type-fest": "^0.13.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "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/simple-update-notifier": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", + "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/sprintf-js": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", + "dev": true, + "license": "BSD-3-Clause", + "optional": true + }, + "node_modules/stat-mode": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/stat-mode/-/stat-mode-1.0.0.tgz", + "integrity": "sha512-jH9EhtKIjuXZ2cWxmXS8ZP80XyC3iasQxMDV8jzhNJpfDb7VbQLVW4Wvsxz9QZvzV+G4YoSfBUVKDOyxLzi/sg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "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" + }, + "engines": { + "node": ">= 8.0" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tar": { + "version": "7.5.22", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.22.tgz", + "integrity": "sha512-MFO/QzvtAOmJbkhOaCTvbGcFN9L9b+JunIsDwaKljSOdcLMea3NJ1k9Usz/rjdfSXTq4dfzfeS7W4p4YOAAHeA==", + "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" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/tar/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/temp": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/temp/-/temp-0.9.4.tgz", + "integrity": "sha512-yYrrsWnrXMcdsnu/7YMYAofM1ktpL5By7vZhf15CrXijWWrEYZks5AXBudalfSWJLlnen/QUJUB5aoB0kqZUGA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "mkdirp": "^0.5.1", + "rimraf": "~2.6.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/temp-file": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/temp-file/-/temp-file-3.4.0.tgz", + "integrity": "sha512-C5tjlC/HCtVUOi3KWVokd4vHVViOmGjtLwIh4MuzPo/nMYTV/p1urt3RnMz2IWXDdKEGJH3k5+KPxtqRsUYGtg==", + "dev": true, + "license": "MIT", + "dependencies": { + "async-exit-hook": "^2.0.1", + "fs-extra": "^10.0.0" + } + }, + "node_modules/thunky": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz", + "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==", + "license": "MIT" + }, + "node_modules/tiny-async-pool": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/tiny-async-pool/-/tiny-async-pool-1.3.0.tgz", + "integrity": "sha512-01EAw5EDrcVrdgyCLgoSPvqznC0sVxDSVeiOz09FUpjh71G79VCqneOr+xvt7T1r76CF6ZZfPjHorN2+d+3mqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^5.5.0" + } + }, + "node_modules/tiny-async-pool/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", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tmp": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.7.tgz", + "integrity": "sha512-e0votIpp4Uo2AJYSzVHV6xCcawuiez3DzqDAbrTc3YxBkplN6e+dM13ZeIcZnDg/QpSuU2zfZ3rzwY8ukEnaXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.14" + } + }, + "node_modules/tmp-promise": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/tmp-promise/-/tmp-promise-3.0.3.tgz", + "integrity": "sha512-RwM7MoPojPxsOBYnyd2hy0bxtIlVrihNs9pj5SUvY8Zz1sQcQG2tG1hSr8PDxfgEB8RNKDhqbIlroIarSNDNsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tmp": "^0.2.0" + } + }, + "node_modules/truncate-utf8-bytes": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz", + "integrity": "sha512-95Pu1QXQvruGEhv62XCMO3Mm90GscOCClvrIUwCM0PYOXK3kaF3l3sIHxx71ThJfcbM2O5Au6SO3AWCSEfW4mQ==", + "dev": true, + "license": "WTFPL", + "dependencies": { + "utf8-byte-length": "^1.0.1" + } + }, + "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==", + "dev": true, + "license": "0BSD" + }, + "node_modules/type-fest": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz", + "integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "optional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/undici": { + "version": "7.30.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.30.0.tgz", + "integrity": "sha512-dkrQXeHSaoamnItlYbmzG0wFYrM0ZwDxCIg0A7aKjTyyhh9svRzCNFEzV+Vm05/yehjCzjDZ31KXfGEjYSztDQ==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=20.18.1" + } + }, + "node_modules/undici-types": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz", + "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==", + "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/unzipper": { + "version": "0.12.5", + "resolved": "https://registry.npmjs.org/unzipper/-/unzipper-0.12.5.tgz", + "integrity": "sha512-tXYOi9R57Uj/2Z25SOs5RRSzq886MBQj2gY8dPL+xl/kv6s6SvByoKfAtvfVeEuhntWDgjd2o9p2lb4TVPAz0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "bluebird": "~3.7.2", + "duplexer2": "~0.1.4", + "fs-extra": "11.3.1", + "graceful-fs": "^4.2.2", + "node-int64": "^0.4.0" + } + }, + "node_modules/unzipper/node_modules/fs-extra": { + "version": "11.3.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.1.tgz", + "integrity": "sha512-eXvGGwZ5CL17ZSwHWd3bbgk7UUpF6IFHtP57NYYakPvHOs8GDgDe5KJI36jIJzDkJ6eJjuzRA8eBQb6SkKue0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/utf8-byte-length": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/utf8-byte-length/-/utf8-byte-length-1.0.5.tgz", + "integrity": "sha512-Xn0w3MtiQ6zoz2vFyUVruaCL53O/DwUvkEeOvj+uulMm0BkUGYWmBYVyElqZaSLhY6ZD0ulfU3aBra2aVT4xfA==", + "dev": true, + "license": "(WTFPL OR MIT)" + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/webcrypto-core": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/webcrypto-core/-/webcrypto-core-1.9.2.tgz", + "integrity": "sha512-gsXecm82UQNlTBURJGuqOWy1Ww08S3kZUcr3aOJS02Pk0xLtkfeUAVC0u0xhgdonFme80edSJUIJyuvL/7250Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.7.0", + "@peculiar/json-schema": "^1.1.12", + "@peculiar/utils": "^2.0.2", + "asn1js": "^3.0.10", + "tslib": "^2.8.1" + } + }, + "node_modules/which": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-5.0.0.tgz", + "integrity": "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^3.1.1" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/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/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/yargs": { + "version": "17.7.3", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.3.tgz", + "integrity": "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/Windows/package.json b/Windows/package.json new file mode 100644 index 0000000..b6ca52b --- /dev/null +++ b/Windows/package.json @@ -0,0 +1,52 @@ +{ + "name": "iframe-windows", + "version": "0.1.0", + "description": "Windows client for the iFrame Mac remote desktop host", + "author": "iFrame contributors", + "license": "MIT", + "main": "src/main.cjs", + "scripts": { + "start": "electron .", + "test": "node --test test/*.test.cjs", + "test:smoke": "electron . --smoke-test", + "dist": "electron-builder --win portable --x64", + "pack": "electron-builder --win --dir --x64" + }, + "build": { + "appId": "com.sciamop.iframe.windows", + "productName": "iFrame", + "directories": { + "output": "dist" + }, + "files": [ + "src/**/*", + "ui/**/*", + "assets/**/*", + "test/smoke.cjs", + "test/desktop.h264", + "package.json" + ], + "extraFiles": [ + { + "from": "../LICENSE", + "to": "LICENSE-iFrame.txt" + } + ], + "win": { + "target": "portable", + "icon": "assets/icon.png", + "signExecutable": false + }, + "portable": { + "artifactName": "iFrame-${version}-Windows-x64.exe" + } + }, + "dependencies": { + "bonjour-service": "^1.4.4", + "koffi": "^2.16.3" + }, + "devDependencies": { + "electron": "^44.5.1", + "electron-builder": "^26.15.3" + } +} diff --git a/Windows/src/main.cjs b/Windows/src/main.cjs new file mode 100644 index 0000000..af0fd51 --- /dev/null +++ b/Windows/src/main.cjs @@ -0,0 +1,78 @@ +'use strict'; +const { app, BrowserWindow, ipcMain, powerSaveBlocker } = require('electron'); +const path = require('node:path'); +const { Bonjour } = require('bonjour-service'); +const { Session } = require('./session.cjs'); +const { validateOptions } = require('./protocol.cjs'); +const { WinKeyHook } = require('./winkey.cjs'); +const smoke = process.argv.includes('--smoke-test'); +if (smoke) app.setPath('userData', path.join(process.cwd(), 'test-output/profile')); +let window, discovery, browser, blocker, streaming = false, fullscreen = false, focused = false; +const session = new Session(); +const hosts = new Map(); +function emit(channel, value) { if (window && !window.isDestroyed()) window.webContents.send(channel, value); } +function stopBlocker() { if (blocker !== undefined) powerSaveBlocker.stop(blocker); blocker = undefined; } +const winKey = new WinKeyHook(value => emit('meta-key', value)); +// Track window events: isFullScreen() can still report the old state while enter-full-screen fires. +function updateWinKey() { winKey.set(!smoke && streaming && fullscreen && focused); } +function setStreaming(value) { streaming = value; updateWinKey(); } +session.on('message', value => emit('stream-message', value)); +session.on('rtt', value => emit('rtt', value)); +session.on('state', value => { + if (value.phase === 'streaming' && blocker === undefined) blocker = powerSaveBlocker.start('prevent-display-sleep'); + if (value.phase === 'failed') stopBlocker(); + setStreaming(value.phase === 'streaming'); + emit('state', value); +}); +function trusted(event) { return window && event.sender === window.webContents && event.senderFrame === window.webContents.mainFrame; } +function handle(name, callback) { + ipcMain.handle(name, (event, ...args) => { if (!trusted(event)) throw new Error('Untrusted caller'); return callback(...args); }); +} +function listen(name, callback) { + ipcMain.on(name, (event, ...args) => { if (!trusted(event)) return; try { callback(...args); } + catch (e) { session.close(); stopBlocker(); setStreaming(false); emit('state', {phase: 'failed', message: e.message}); } }); +} +handle('connect', options => { session.connect(validateOptions(options)); }); +handle('disconnect', () => { session.close(); stopBlocker(); setStreaming(false); emit('state', {phase: 'idle'}); }); +handle('fullscreen', () => { window.setFullScreen(!window.isFullScreen()); return window.isFullScreen(); }); +handle('hosts', () => [...hosts.values()]); +listen('input', input => session.input(input)); +listen('ack', (id, micros) => session.ack(id, micros)); +listen('keyframe', () => session.keyframe()); +function startDiscovery() { + try { + discovery = new Bonjour({}, () => emit('discovery-warning', 'Discovery is unavailable. Enter the Mac address below.')); + browser = discovery.find({ type: 'iframe', protocol: 'tcp' }); + browser.on('up', service => { + const host = service.addresses?.find(a => /^\d+\.\d+\.\d+\.\d+$/.test(a)) ?? service.host; + if (!host) return; + hosts.set(service.fqdn, { id: service.fqdn, name: service.name, host, port: service.port }); + emit('hosts', [...hosts.values()]); + }); + browser.on('down', service => { hosts.delete(service.fqdn); emit('hosts', [...hosts.values()]); }); + } catch { emit('discovery-warning', 'Discovery is unavailable. Enter the Mac address below.'); } +} +app.whenReady().then(async () => { + window = new BrowserWindow({ width: 1180, height: 820, minWidth: 760, minHeight: 620, show: !smoke, + title: 'iFrame', backgroundColor: '#101416', icon: path.join(__dirname, '../assets/icon.png'), + webPreferences: { preload: path.join(__dirname, 'preload.cjs'), contextIsolation: true, nodeIntegration: false, + sandbox: true, offscreen: smoke, backgroundThrottling: false, devTools: !app.isPackaged } }); + window.setMenu(null); + window.webContents.setWindowOpenHandler(() => ({action: 'deny'})); + window.webContents.on('will-navigate', event => event.preventDefault()); + window.webContents.session.setPermissionRequestHandler((_wc, _permission, callback) => callback(false)); + window.webContents.on('render-process-gone', () => { session.close(); stopBlocker(); setStreaming(false); }); + window.on('blur', () => { focused = false; updateWinKey(); emit('release-input'); }); + window.on('focus', () => { focused = true; updateWinKey(); }); + window.on('enter-full-screen', () => { fullscreen = true; updateWinKey(); }); + window.on('leave-full-screen', () => { fullscreen = false; updateWinKey(); }); + focused = window.isFocused(); + window.on('closed', () => { session.close(); stopBlocker(); window = null; focused = fullscreen = false; setStreaming(false); }); + await window.loadFile(path.join(__dirname, '../ui/index.html')); + if (smoke) { + try { await require('../test/smoke.cjs').run(window); app.exit(0); } + catch (e) { console.error(e); app.exit(1); } + } else startDiscovery(); +}); +app.on('window-all-closed', () => app.quit()); +app.on('before-quit', () => { session.close(); stopBlocker(); winKey.set(false); browser?.stop(); discovery?.destroy(); }); diff --git a/Windows/src/preload.cjs b/Windows/src/preload.cjs new file mode 100644 index 0000000..2c6cdfe --- /dev/null +++ b/Windows/src/preload.cjs @@ -0,0 +1,19 @@ +'use strict'; +const { contextBridge, ipcRenderer } = require('electron'); +const subscribe = channel => callback => { + const listener = (_event, value) => callback(value); + ipcRenderer.on(channel, listener); + return () => ipcRenderer.removeListener(channel, listener); +}; +contextBridge.exposeInMainWorld('iframe', { + connect: options => ipcRenderer.invoke('connect', options), + disconnect: () => ipcRenderer.invoke('disconnect'), + fullscreen: () => ipcRenderer.invoke('fullscreen'), + hosts: () => ipcRenderer.invoke('hosts'), + input: value => ipcRenderer.send('input', value), + ack: (id, micros) => ipcRenderer.send('ack', id, micros), + keyframe: () => ipcRenderer.send('keyframe'), + onMessage: subscribe('stream-message'), onState: subscribe('state'), onHosts: subscribe('hosts'), + onRtt: subscribe('rtt'), onReleaseInput: subscribe('release-input'), onDiscoveryWarning: subscribe('discovery-warning'), + onMetaKey: subscribe('meta-key') +}); diff --git a/Windows/src/protocol.cjs b/Windows/src/protocol.cjs new file mode 100644 index 0000000..2425e16 --- /dev/null +++ b/Windows/src/protocol.cjs @@ -0,0 +1,77 @@ +'use strict'; +const MAX_MESSAGE = 32 << 20; +const T = Object.freeze({ welcome: 1, format: 2, frame: 3, stats: 4, pong: 5, + authFailed: 6, textFocus: 7, hello: 16, mouseMove: 17, mouseButton: 18, + scroll: 19, key: 20, text: 21, requestKeyframe: 22, ping: 23, ack: 24, display: 25 }); + +function packet(type, payload = Buffer.alloc(0)) { + if (payload.length > MAX_MESSAGE) throw new Error('Message too large'); + const header = Buffer.alloc(5); + header[0] = type; + header.writeUInt32BE(payload.length, 1); + return Buffer.concat([header, payload]); +} + +// Fixed header and one allocation per body, even when TCP fragments a large frame. +class Parser { + constructor(deliver) { this.deliver = deliver; this.header = Buffer.alloc(5); this.used = 0; this.body = null; } + push(chunk) { + let offset = 0; + while (offset < chunk.length) { + const target = this.body ?? this.header; + const count = Math.min(target.length - this.used, chunk.length - offset); + chunk.copy(target, this.used, offset); + this.used += count; offset += count; + if (this.used !== target.length) continue; + this.used = 0; + if (this.body) { + const body = this.body; this.body = null; + this.deliver(this.header[0], body); + } else { + const length = this.header.readUInt32BE(1); + if (length > MAX_MESSAGE) throw new Error('Host sent an oversized message'); + if (length) this.body = Buffer.allocUnsafe(length); + else this.deliver(this.header[0], Buffer.alloc(0)); + } + } + } +} + +function floats(...values) { + const b = Buffer.alloc(values.length * 4); + values.forEach((v, i) => b.writeFloatBE(v, i * 4)); return b; +} +function inputPacket(input) { + const finite = (...values) => values.every(v => typeof v === 'number' && Number.isFinite(v)); + const point = () => finite(input.x, input.y) && input.x >= 0 && input.x <= 1 && input.y >= 0 && input.y <= 1; + switch (input.kind) { + case 'move': if (point()) return packet(T.mouseMove, floats(input.x, input.y)); break; + case 'button': + if (point() && [0, 1, 2].includes(input.button) && typeof input.down === 'boolean') + return packet(T.mouseButton, Buffer.concat([Buffer.from([input.button, +input.down]), floats(input.x, input.y)])); + break; + case 'scroll': if (finite(input.dx, input.dy) && Math.abs(input.dx) <= 10000 && Math.abs(input.dy) <= 10000) + return packet(T.scroll, floats(input.dx, input.dy)); break; + case 'key': { + if (!Number.isInteger(input.code) || input.code < 0 || input.code > 127 || ![0, 1, 2].includes(input.action) || + !Number.isInteger(input.mods) || input.mods < 0 || input.mods > 31) break; + const b = Buffer.alloc(7); b.writeUInt16BE(input.code); b[2] = input.action; b.writeUInt32BE(input.mods, 3); + return packet(T.key, b); + } + case 'text': if (typeof input.text === 'string' && input.text.length <= 16384) + return packet(T.text, Buffer.from(input.text)); break; + } + throw new Error('Invalid input'); +} + +function validateOptions(o) { + if (!o || typeof o.host !== 'string' || !o.host.trim() || o.host.length > 253 || /[\s/\\]/.test(o.host.trim())) + throw new Error('Enter a Mac IP address or hostname, without a URL prefix.'); + if (!Number.isInteger(o.port) || o.port < 1 || o.port > 65535) throw new Error('Port must be between 1 and 65535.'); + if (typeof o.pin !== 'string' || o.pin.length > 256) throw new Error('Invalid PIN.'); + if (![30, 60, 120].includes(o.fps) || ![0, 1, 1.5, 2].includes(o.scale)) throw new Error('Invalid stream settings.'); + if (!Number.isInteger(o.width) || !Number.isInteger(o.height) || o.width < 640 || o.height < 480 || o.width > 7680 || o.height > 4320) + throw new Error('Display must be between 640 × 480 and 7680 × 4320.'); + return { ...o, host: o.host.trim().replace(/^\[|\]$/g, ''), pin: o.pin.trim() }; +} +module.exports = { T, MAX_MESSAGE, packet, Parser, floats, inputPacket, validateOptions }; diff --git a/Windows/src/session.cjs b/Windows/src/session.cjs new file mode 100644 index 0000000..6986b0f --- /dev/null +++ b/Windows/src/session.cjs @@ -0,0 +1,85 @@ +'use strict'; +const net = require('node:net'); +const os = require('node:os'); +const { EventEmitter } = require('node:events'); +const { T, packet, Parser, inputPacket, validateOptions } = require('./protocol.cjs'); + +class Session extends EventEmitter { + constructor() { super(); this.socket = null; this.ready = false; this.pending = new Map(); } + connect(options) { + const o = validateOptions(options); + this.close(); + const socket = new net.Socket(); this.socket = socket; + let welcomed = false; + const fail = message => { if (this.socket !== socket) return; this.close(); this.emit('state', { phase: 'failed', message }); }; + const parser = new Parser((type, data) => { + if (this.socket !== socket) return; + if (type === T.authFailed) return fail('Wrong PIN or incompatible host protocol. Check the PIN on your Mac.'); + if (type === T.welcome) { + const w = JSON.parse(data.toString()); + if (w.codec !== 0) return fail('The host is forcing HEVC. Restart it with H.264 or automatic codec selection.'); + if (!Number.isInteger(w.width) || !Number.isInteger(w.height) || w.width < 1 || w.height < 1 || w.width > 16384 || w.height > 16384) + return fail('Invalid display size from host.'); + welcomed = true; this.ready = true; clearTimeout(this.handshake); + this.emit('message', { type, data }); + this.emit('state', { phase: 'streaming', welcome: w }); + } else if (!welcomed) { + throw new Error('Unexpected message before welcome'); + } else if (type === T.pong) { + if (data.length !== 8) throw new Error('Invalid ping response'); + const elapsed = process.hrtime.bigint() - data.readBigUInt64BE(); + this.emit('rtt', Math.max(0, Number(elapsed) / 1e6)); + } else { + if (type === T.frame) { + if (data.length < 14) throw new Error('Truncated video frame'); + if (this.pending.size >= 32) throw new Error('Too many unacknowledged frames'); + this.pending.set(data.readUInt32BE(), Date.now()); + } + this.emit('message', { type, data }); + } + }); + socket.setNoDelay(true); socket.setKeepAlive(true, 5000); socket.setTimeout(15000); + socket.on('timeout', () => fail('The Mac stopped responding. Check the network and reconnect.')); + socket.on('data', chunk => { try { parser.push(chunk); } catch (e) { fail(`Stream error: ${e.message}`); } }); + socket.on('error', e => fail(`Cannot connect to ${o.host}:${o.port} (${e.code ?? e.message}).`)); + socket.on('close', () => fail('The Mac closed the connection. Check its screen recording permission and host log.')); + socket.on('connect', () => { + this.send(T.hello, Buffer.from(JSON.stringify({ version: 1, pin: o.pin, name: os.hostname(), supportsHEVC: false, + maxFPS: o.fps, display: { width: o.width, height: o.height, uiScale: o.scale } }))); + this.timer = setInterval(() => { + if (!this.ready) return; + if ([...this.pending.values()].some(time => Date.now() - time > 10000)) + return fail('Video decoding stalled. Reconnect or try a lower resolution.'); + const ping = Buffer.alloc(8); ping.writeBigUInt64BE(process.hrtime.bigint()); this.send(T.ping, ping); + }, 1000); + }); + this.handshake = setTimeout(() => fail('Connection timed out. Confirm iframe-host is running and the port is reachable.'), 15000); + this.emit('state', { phase: 'connecting' }); + socket.connect(o.port, o.host); + } + send(type, payload) { if (this.socket && !this.socket.destroyed) this.socket.write(packet(type, payload)); } + input(input) { + if (!this.ready) return; + const p = inputPacket(input); + // Never allow a blocked network to accumulate seconds of stale input. + if (this.socket.writableLength > 256 * 1024) { + this.close(); this.emit('state', { phase: 'failed', message: 'Network is too slow for interactive input. Reconnect.' }); return; + } + this.socket.write(p); + } + ack(id, micros) { + if (!this.ready || !Number.isInteger(id) || !this.pending.delete(id)) return; + const b = Buffer.alloc(8); b.writeUInt32BE(id); b.writeUInt32BE(Math.max(0, Math.min(0xffffffff, Math.round(micros) || 0)), 4); + this.send(T.ack, b); + } + keyframe() { + if (!this.ready || Date.now() - (this.lastKeyframe ?? 0) < 250) return; + this.lastKeyframe = Date.now(); this.send(T.requestKeyframe); + } + close() { + clearTimeout(this.handshake); clearInterval(this.timer); + const socket = this.socket; this.socket = null; this.ready = false; + socket?.destroy(); this.pending.clear(); this.lastKeyframe = 0; + } +} +module.exports = { Session }; diff --git a/Windows/src/winkey.cjs b/Windows/src/winkey.cjs new file mode 100644 index 0000000..effd332 --- /dev/null +++ b/Windows/src/winkey.cjs @@ -0,0 +1,50 @@ +'use strict'; +// Windows hands the Windows key to the shell (Start menu, Win+D, Win+E…) before any window +// sees it. While the stream is fullscreen and focused, a low-level keyboard hook takes the key +// so the Mac can receive it as Command. Windows+L and Ctrl+Alt+Delete stay with the system. +const VK_LWIN = 0x5b, VK_RWIN = 0x5c, WM_KEYDOWN = 0x100, WM_SYSKEYDOWN = 0x104, WH_KEYBOARD_LL = 13; + +class WinKeyHook { + constructor(onKey) { this.onKey = onKey; this.hook = null; this.held = new Set(); } + load() { + if (this.api) return this.api; + const koffi = require('koffi'); + const user32 = koffi.load('user32.dll'), kernel32 = koffi.load('kernel32.dll'); + const KBD = koffi.struct('KBDLLHOOKSTRUCT', { vkCode: 'uint32', scanCode: 'uint32', flags: 'uint32', time: 'uint32', extra: 'uintptr' }); + const Proc = koffi.proto('intptr __stdcall LowLevelKeyboardProc(int code, uintptr wParam, void *lParam)'); + const api = { + koffi, KBD, Proc, + set: user32.func('void * __stdcall SetWindowsHookExW(int id, LowLevelKeyboardProc *proc, void *module, uint32 thread)'), + unhook: user32.func('bool __stdcall UnhookWindowsHookEx(void *hook)'), + next: user32.func('intptr __stdcall CallNextHookEx(void *hook, int code, uintptr wParam, void *lParam)'), + module: kernel32.func('void * __stdcall GetModuleHandleW(const char16_t *name)') + }; + api.callback = koffi.register((code, wParam, lParam) => { + if (code >= 0) { + const { vkCode } = koffi.decode(lParam, KBD); + if (vkCode === VK_LWIN || vkCode === VK_RWIN) { + const key = vkCode === VK_LWIN ? 'MetaLeft' : 'MetaRight', down = wParam === WM_KEYDOWN || wParam === WM_SYSKEYDOWN; + // Pass through a release whose press Windows already saw, or its key state sticks. + if (down || this.held.has(key)) { + if (down) this.held.add(key); else this.held.delete(key); + try { this.onKey({ code: key, down }); } catch {} + return 1; + } + } + } + return api.next(null, code, wParam, lParam); + }, koffi.pointer(Proc)); + return this.api = api; + } + set(enabled) { + if (process.platform !== 'win32' || enabled === !!this.hook) return; + try { + const api = this.load(); + if (enabled) { this.hook = api.set(WH_KEYBOARD_LL, api.callback, api.module(null), 0) || null; return; } + api.unhook(this.hook); this.hook = null; + } catch { this.hook = null; return; } + for (const code of this.held) this.onKey({ code, down: false }); + this.held.clear(); + } +} +module.exports = { WinKeyHook }; diff --git a/Windows/test/desktop.h264 b/Windows/test/desktop.h264 new file mode 100644 index 0000000..8fe4ea0 Binary files /dev/null and b/Windows/test/desktop.h264 differ diff --git a/Windows/test/protocol.test.cjs b/Windows/test/protocol.test.cjs new file mode 100644 index 0000000..2d09bd7 --- /dev/null +++ b/Windows/test/protocol.test.cjs @@ -0,0 +1,89 @@ +const { test } = require('node:test'); +const assert = require('node:assert/strict'); +const net = require('node:net'); +const { once } = require('node:events'); +const { T, MAX_MESSAGE, packet, Parser, inputPacket, validateOptions } = require('../src/protocol.cjs'); +const { Session } = require('../src/session.cjs'); +const options = {host:'127.0.0.1', port:7878, pin:'1234', width:1920, height:1080, fps:60, scale:2}; + +test('framing handles every split, concatenation and empty messages', () => { + const expected = [[T.welcome,Buffer.from('hello')],[T.requestKeyframe,Buffer.alloc(0)],[T.frame,Buffer.alloc(8192,71)]]; + const stream = Buffer.concat(expected.map(([t,b]) => packet(t,b))); + for (const size of [1,2,3,4,5,7,512,stream.length]) { + const got = [], parser = new Parser((t,b) => got.push([t,b])); + for (let i = 0; i < stream.length; i += size) parser.push(stream.subarray(i,i+size)); + assert.deepEqual(got,expected); + } +}); +test('oversized payload rejected before allocation', () => { + const header = Buffer.alloc(5); header.writeUInt32BE(MAX_MESSAGE+1,1); + assert.throws(() => new Parser(() => {}).push(header), /oversized/); +}); +test('Swift wire compatibility for mouse, key modifiers and scroll', () => { + assert.equal(inputPacket({kind:'move',x:0.5,y:1}).toString('hex'),'11000000083f0000003f800000'); + assert.equal(inputPacket({kind:'button',button:1,down:true,x:0,y:1}).toString('hex'),'120000000a0101000000003f800000'); + assert.equal(inputPacket({kind:'key',code:55,action:1,mods:8}).toString('hex'),'140000000700370100000008'); + assert.equal(inputPacket({kind:'scroll',dx:-1,dy:2}).toString('hex'),'1300000008bf80000040000000'); + assert.throws(() => inputPacket({kind:'move',x:NaN,y:1})); + assert.throws(() => inputPacket({kind:'key',code:0,action:1,mods:32})); +}); +test('connection options reject URLs, invalid sizes and ports', () => { + assert.equal(validateOptions({...options,host:' [::1] '}).host,'::1'); + for (const invalid of [{host:'http://mac'},{port:0},{width:20000},{scale:3},{fps:90}]) + assert.throws(() => validateOptions({...options,...invalid})); +}); +test('AVCC conversion and malformed bitstreams', async () => { + const {annexB,parseFormat} = await import('../ui/video.mjs'); + assert.deepEqual([...annexB([0,0,0,2,0x65,0x11])],[0,0,0,1,0x65,0x11]); + assert.throws(() => annexB([0,0,0,9,0x65]),/Truncated/); + assert.throws(() => parseFormat([1,0]),/H.264/); + assert.throws(() => parseFormat([0,1,0,0,0,9,0x67]),/Invalid/); +}); +test('letterboxing and key mapping preserve remote coordinates', async () => { + const {normalizedPoint,mapKey,modifiers} = await import('../ui/input.mjs'); + const rect = {left:0,top:0,width:1000,height:1000}; + assert.equal(normalizedPoint(500,100,rect,1920,1080),null); + assert.deepEqual(normalizedPoint(500,500,rect,1920,1080),{x:0.5,y:0.5}); + assert.deepEqual(normalizedPoint(1200,1200,rect,1920,1080,true),{x:1,y:1}); + assert.equal(mapKey('ControlLeft',true),55); assert.equal(mapKey('ControlLeft',false),59); + assert.equal(mapKey('KeyA',false),0); + assert.equal(modifiers({ctrlKey:true,shiftKey:false,altKey:false,metaKey:false,getModifierState:()=>false},true),8); +}); + +test('loopback handshake, decode acknowledgements, ping and input', async t => { + const server = net.createServer(); server.listen(0,'127.0.0.1'); await once(server,'listening'); + const session = new Session(); t.after(() => { session.close(); server.close(); }); + const connected = once(server,'connection'); session.connect({...options,port:server.address().port}); + const [peer] = await connected; t.after(() => peer.destroy()); + const messages = []; let notify; + peer.on('data', chunk => parser.push(chunk)); + const parser = new Parser((type,data) => { messages.push({type,data}); notify?.(); }); + const wait = async predicate => { + const timeout = setTimeout(() => notify?.(true),2000); + while (!predicate()) { const expired = await new Promise(resolve => {notify = resolve;}); if (expired) throw new Error('Timed out'); } + clearTimeout(timeout); + }; + await wait(() => messages.length > 0); + const hello = JSON.parse(messages[0].data); assert.equal(hello.pin,'1234'); assert.equal(hello.supportsHEVC,false); + assert.equal(hello.display.uiScale,2); + const streaming = once(session,'state'); + peer.write(packet(T.welcome,Buffer.from(JSON.stringify({width:1920,height:1080,codec:0})))); + assert.equal((await streaming)[0].phase,'streaming'); + const received = once(session,'message'); const frame = Buffer.alloc(14); frame.writeUInt32BE(42); + peer.write(packet(T.frame,frame)); await received; + assert.equal(messages.filter(m => m.type === T.ack).length,0,'ACK must wait for decoder'); + session.ack(42,1500); session.ack(42,1500); + session.input({kind:'key',code:0,action:1,mods:8}); + await wait(() => messages.some(m => m.type === T.key)); + const acks = messages.filter(m => m.type === T.ack); assert.equal(acks.length,1); assert.equal(acks[0].data.readUInt32BE(),42); + const rtt = once(session,'rtt'); const ping = Buffer.alloc(8); ping.writeBigUInt64BE(process.hrtime.bigint()); + peer.write(packet(T.pong,ping)); assert.ok((await rtt)[0] >= 0); +}); +test('wrong PIN returns actionable error and closes socket', async t => { + const server = net.createServer(peer => { peer.on('data', () => peer.write(packet(T.authFailed))); }); + server.listen(0,'127.0.0.1'); await once(server,'listening'); + const session = new Session(); t.after(() => {session.close();server.close();}); + session.connect({...options,port:server.address().port}); + const [state] = await once(session,'state'); assert.equal(state.phase,'failed'); assert.match(state.message,/Wrong PIN/); + assert.equal(session.socket,null); +}); diff --git a/Windows/test/smoke.cjs b/Windows/test/smoke.cjs new file mode 100644 index 0000000..666bc43 --- /dev/null +++ b/Windows/test/smoke.cjs @@ -0,0 +1,98 @@ +const net = require('node:net'); +const fs = require('node:fs/promises'); +const path = require('node:path'); +const assert = require('node:assert/strict'); +const { once } = require('node:events'); +const { T, packet, Parser } = require('../src/protocol.cjs'); + +async function fixture() { + const raw = await fs.readFile(path.join(__dirname,'desktop.h264')); + const starts = []; + for (let i=0;i raw.subarray(s[1],starts[i+1]?.[0] ?? raw.length)); + const sets = nals.filter(n => [7,8].includes(n[0]&31)); + const groups=[]; let group=[]; + for (const n of nals) { + if ((n[0]&31)===9) {if(group.length)groups.push(group);group=[];} + if (![7,8,9].includes(n[0]&31)) group.push(n); + } + if(group.length) groups.push(group); + const sized = n => {const length=Buffer.alloc(4);length.writeUInt32BE(n.length);return Buffer.concat([length,n]);}; + return {format:Buffer.concat([Buffer.from([0,sets.length]),...sets.map(sized)]), + frames:groups.map((g,i) => {const head=Buffer.alloc(13);head.writeUInt32BE(i+1);head.writeBigUInt64BE(BigInt(i*16666667),4);head[12]=+(g.some(n => (n[0]&31)===5));return Buffer.concat([head,...g.map(sized)]);})}; +} +exports.run = async window => { + const out = path.join(process.cwd(),'test-output'); await fs.mkdir(out,{recursive:true}); + const errors=[]; + window.webContents.on('console-message', (_event,details) => { if (details.level === 'error') errors.push(details.message); }); + await window.webContents.executeJavaScript(`document.getElementById('host').value=''; document.getElementById('port').value='7878';`); + await new Promise(resolve => setTimeout(resolve,500)); + await fs.writeFile(path.join(out,'connect.png'),(await window.webContents.capturePage(undefined,{stayHidden:true,stayAwake:true})).toPNG()); + const sample=await fixture(), received=[], peers=new Set(); let sent=0, rejection=false; + const server=net.createServer(peer => { + peers.add(peer);peer.on('close',()=>peers.delete(peer)); + const send = (type,data) => peer.write(packet(type,data)); + const parser=new Parser((type,data) => { + received.push({type,data}); + if(type===T.hello) { + if(rejection) return send(T.authFailed); + assert.equal(JSON.parse(data).supportsHEVC,false); + send(T.welcome,Buffer.from(JSON.stringify({width:160,height:96,codec:0,fps:60,hostName:'Test Mac',isVirtual:true}))); + send(T.format,sample.format);send(T.frame,sample.frames[sent++]); + } else if(type===T.ack && sentparser.push(chunk)); + }); + server.listen(0,'127.0.0.1');await once(server,'listening'); + const evaluate=code=>window.webContents.executeJavaScript(code); + const until=async (predicate,label) => { + const deadline=Date.now()+12000; + while(!(await predicate())) {if(Date.now()>deadline) throw new Error(`Timeout: ${label}; renderer errors: ${errors.join('; ')}`);await new Promise(r=>setTimeout(r,50));} + }; + try { + await evaluate(`document.getElementById('host').value='127.0.0.1'; document.getElementById('port').value='${server.address().port}'; document.getElementById('pin').value='1234'; document.getElementById('connect-form').requestSubmit();`); + await until(()=>received.filter(m=>m.type===T.ack).length===sample.frames.length,'decoded frame ACKs'); + assert.ok(sample.frames.length>=4); + const pixel=await evaluate(`Array.from(document.getElementById('video').getContext('2d').getImageData(80,48,1,1).data)`); + assert.ok(pixel.slice(0,3).some(v=>v>20),'Decoded pixels must be visible'); + assert.equal(await evaluate(`document.getElementById('waiting').hidden`),true); + await fs.writeFile(path.join(out,'stream.png'),(await window.webContents.capturePage()).toPNG()); + window.webContents.sendInputEvent({type:'keyDown',keyCode:'A'}); + window.webContents.sendInputEvent({type:'keyUp',keyCode:'A'}); + await until(()=>received.filter(m=>m.type===T.key).length>=2,'keyboard passthrough'); + assert.equal(received.find(m=>m.type===T.key).data.readUInt16BE(),0); + const keyCount = received.filter(m=>m.type===T.key).length; + window.webContents.sendInputEvent({type:'keyDown',keyCode:'B'}); + await until(()=>received.filter(m=>m.type===T.key).length>keyCount,'held key'); + await evaluate(`document.getElementById('fullscreen').focus()`); + // Offscreen windows never have OS focus; send the same notification as main's blur handler. + window.webContents.send('release-input'); + await until(()=>received.some(m=>m.type===T.key && m.data.readUInt16BE()===11 && m.data[2]===0),'release held key on blur'); + window.webContents.sendInputEvent({type:'mouseDown',x:500,y:400,button:'right',clickCount:1}); + window.webContents.sendInputEvent({type:'mouseUp',x:500,y:400,button:'right',clickCount:1}); + await until(()=>received.filter(m=>m.type===T.mouseButton).length>=2,'right mouse click'); + assert.equal(received.find(m=>m.type===T.mouseButton).data[0],1); + // Completing a click loses pointer capture, but must preserve held keyboard modifiers. + window.webContents.sendInputEvent({type:'keyDown',keyCode:'Shift'}); + await until(()=>received.some(m=>m.type===T.key && m.data.readUInt16BE()===56 && m.data[2]===1),'held Shift'); + await evaluate(`document.getElementById('video').dispatchEvent(new PointerEvent('lostpointercapture'))`); + await new Promise(resolve=>setTimeout(resolve,50)); + assert.equal(received.filter(m=>m.type===T.key && m.data.readUInt16BE()===56 && m.data[2]===0).length,0,'Click must not release Shift'); + window.webContents.sendInputEvent({type:'keyUp',keyCode:'Shift'}); + await evaluate(`document.getElementById('send-text').click(); document.getElementById('text-content').value='Hello, Mac — 你好'; document.getElementById('text-submit').click();`); + await until(()=>received.some(m=>m.type===T.text),'Unicode text'); + assert.equal(received.find(m=>m.type===T.text).data.toString(),'Hello, Mac — 你好'); + await evaluate(`window.iframe.disconnect()`); + await until(()=>evaluate(`!document.getElementById('connect-page').hidden`),'disconnect UI'); + rejection=true; + await evaluate(`document.getElementById('connect-form').requestSubmit()`); + await until(()=>evaluate(`document.getElementById('status').textContent.includes('Wrong PIN')`),'wrong PIN UI'); + assert.deepEqual(errors,[]); + await fs.writeFile(path.join(out,'smoke-result.json'),JSON.stringify({passed:true,executable:process.execPath,frames:sample.frames.length,checks:['decode','ack','pixels','keyboard','mouse','unicode','focus release','disconnect','PIN rejection']},null,2)); + console.log(`PASS Electron smoke: ${sample.frames.length} real H.264 frames decoded and ACKed, pixels rendered, keyboard/mouse/Unicode input, focus release, disconnect and PIN rejection. Screenshots: ${out}`); + } finally {for(const peer of peers)peer.destroy();server.close();} +}; diff --git a/Windows/test/sps.test.cjs b/Windows/test/sps.test.cjs new file mode 100644 index 0000000..b185ab1 --- /dev/null +++ b/Windows/test/sps.test.cjs @@ -0,0 +1,19 @@ +const { test } = require('node:test'); +const assert = require('node:assert/strict'); +const fs = require('node:fs'); +const path = require('node:path'); + +test('SPS rewrite declares zero reorder frames for VideoToolbox-style VUI', async () => { + const { lowDelaySPS } = await import('../ui/sps.mjs'); + const raw = fs.readFileSync(path.join(__dirname, 'desktop.h264')); + const start = raw.findIndex((b, i) => b === 1 && raw[i-1] === 0 && raw[i-2] === 0 && (raw[i+1] & 31) === 7) + 1; + let end = raw.indexOf(Buffer.from([0, 0, 1]), start); while (raw[end-1] === 0) end--; + const original = new Uint8Array(raw.subarray(start, end)); + // Remove the restriction the way VideoToolbox omits it, then restore it. + const stripped = lowDelaySPS(original, null), fixed = lowDelaySPS(stripped); + assert.notDeepEqual(stripped, original); + assert.deepEqual(lowDelaySPS(stripped, null), stripped); + assert.deepEqual(lowDelaySPS(fixed), fixed); + assert.deepEqual(lowDelaySPS(lowDelaySPS(original, 4)), lowDelaySPS(original)); + assert.equal(fixed[0], original[0]); +}); diff --git a/Windows/ui/app.mjs b/Windows/ui/app.mjs new file mode 100644 index 0000000..ecf0a6c --- /dev/null +++ b/Windows/ui/app.mjs @@ -0,0 +1,134 @@ +import { Video } from './video.mjs'; +import { attachInput } from './input.mjs'; +const $ = id => document.getElementById(id), api = window.iframe; +let active = false, welcome, rtt = 0, hostStats = {}, frames = 0, decodeTotal = 0; +const settings = ['host','port','resolution','fps','scale']; +try { + const saved = JSON.parse(localStorage.getItem('iframe-settings') ?? '{}'); + for (const id of settings) if (saved[id] !== undefined) $(id).value = saved[id]; + $('swap').checked = saved.swap !== false; +} catch { /* Ignore an unreadable preference file. The PIN is never persisted. */ } +const status = message => { $('status').textContent = message; }; +function saveSettings() { + try { localStorage.setItem('iframe-settings', JSON.stringify(Object.fromEntries([...settings.map(id => [id, $(id).value]), ['swap', $('swap').checked]]))); } catch {} +} +async function fail(message) { await api.disconnect(); status(message); } +const video = new Video($('video'), api, fail, ms => { frames++; decodeTotal += ms; $('waiting').hidden = true; }, + message => { $('waiting').textContent = message; }); +const releaseInput = attachInput($('video'), api, { + active: () => active, swap: () => $('swap').checked, + shortcut: code => { if (code === 'KeyF') toggleFullscreen(); else if (code === 'KeyQ') disconnect(); else $('fullscreen').focus(); } +}); +function disconnect() { releaseInput(); return api.disconnect(); } +async function toggleFullscreen() { + const fullscreen = await api.fullscreen(); $('fullscreen').textContent = fullscreen ? 'Exit fullscreen' : 'Fullscreen'; + if (active) $('video').focus(); +} +$('connect-form').addEventListener('submit', async e => { + e.preventDefault(); status(''); + let width, height; + if ($('resolution').value === 'screen') { + width = Math.max(640, Math.min(7680, Math.round(screen.width * devicePixelRatio / 2) * 2)); + height = Math.max(480, Math.min(4320, Math.round(screen.height * devicePixelRatio / 2) * 2)); + } else [width, height] = $('resolution').value.split('x').map(Number); + saveSettings(); + try { + if (typeof VideoDecoder === 'undefined') throw new Error('Video decoding is unavailable on this Windows installation.'); + await api.connect({host:$('host').value, port:Number($('port').value), pin:$('pin').value, + width, height, scale:Number($('scale').value), fps:Number($('fps').value)}); + } catch (e) { status(e.message.replace(/^Error invoking remote method '[^']+': Error: /, '')); } +}); +$('cancel').onclick = disconnect; $('disconnect').onclick = disconnect; $('fullscreen').onclick = toggleFullscreen; +$('refresh').onclick = () => { api.keyframe(); $('video').focus(); }; +$('stats-toggle').onclick = () => { + $('stats').hidden = !$('stats').hidden; $('stats-toggle').setAttribute('aria-pressed', String(!$('stats').hidden)); $('video').focus(); +}; +$('send-text').onclick = () => { releaseInput(); $('text-dialog').showModal(); $('text-content').focus(); }; +$('text-dialog').addEventListener('close', () => { + if ($('text-dialog').returnValue === 'send' && active) { api.input({kind:'text', text:$('text-content').value}); $('text-content').value = ''; } + if (active) $('video').focus(); +}); +api.onState(state => { + const connecting = state.phase === 'connecting'; + $('connect').disabled = connecting; $('cancel').hidden = !connecting; + for (const input of document.querySelectorAll('#connect-form input, #connect-form select, #discovered, #bookmark, #bookmarks button')) input.disabled = connecting; + if (!connecting) updateBookmarkButton(); + if (state.phase === 'streaming') { + active = true; welcome = state.welcome; + $('connect-page').hidden = true; $('stream-page').hidden = false; $('waiting').hidden = false; + $('waiting').textContent = 'Waiting for the first frame…'; + $('host-name').textContent = welcome.hostName; + $('stream-info').textContent = `${welcome.width} × ${welcome.height} · H.264 · ${welcome.fps} fps`; + $('keyboard-mode').textContent = $('swap').checked ? 'Ctrl → ⌘ Command' : 'Windows → ⌘ Command'; + $('pin').value = ''; $('video').focus(); + } else if (!connecting) { + releaseInput(); active = false; video.close(); welcome = null; frames = 0; decodeTotal = 0; hostStats = {}; rtt = 0; + $('text-dialog').close('cancel'); $('connect-page').hidden = false; $('stream-page').hidden = true; + status(state.phase === 'failed' ? state.message : ''); $('connect').focus(); + } else status('Connecting to your Mac…'); +}); +api.onMessage(({type, data}) => { + try { + if (type === 1) { video.close(); $('waiting').hidden = false; } + else if (type === 2) video.configure(data); + else if (type === 3) video.frame(data); + else if (type === 4) hostStats = JSON.parse(new TextDecoder().decode(data)); + } catch (e) { fail(e.message); } +}); +api.onRtt(value => { rtt = value; }); +let knownHosts = []; +function showHosts(hosts) { + knownHosts = hosts; const selected = $('discovered').value; + $('discovered').replaceChildren(new Option(hosts.length ? 'Choose a Mac…' : 'No Macs found yet · enter an address below', '')); + for (const host of hosts) $('discovered').add(new Option(`${host.name} · ${host.host}`, host.id)); + $('discovered').value = selected; +} +api.onHosts(showHosts); api.hosts().then(showHosts); +api.onDiscoveryWarning(message => { $('discovery-hint').textContent = message; }); +$('discovered').onchange = () => { + const host = knownHosts.find(h => h.id === $('discovered').value); if (!host) return; + $('host').value = host.host; $('port').value = host.port; updateBookmarkButton(); $('pin').focus(); +}; +// Saved Macs: name, address and port only. The PIN is never stored. +let bookmarks = []; +try { bookmarks = JSON.parse(localStorage.getItem('iframe-bookmarks') ?? '[]').filter(b => typeof b?.host === 'string' && Number.isInteger(b.port)); } catch {} +const address = () => ({host: $('host').value.trim(), port: Number($('port').value)}); +const bookmarkIndex = ({host, port}) => bookmarks.findIndex(b => b.host.toLowerCase() === host.toLowerCase() && b.port === port); +function showBookmarks() { + try { localStorage.setItem('iframe-bookmarks', JSON.stringify(bookmarks)); } catch {} + $('bookmarks-section').hidden = !bookmarks.length; + $('bookmarks').replaceChildren(...bookmarks.map(b => { + const item = document.createElement('div'), open = document.createElement('button'), remove = document.createElement('button'); + const name = document.createElement('span'), where = document.createElement('small'); + item.className = 'bookmark'; item.setAttribute('role', 'listitem'); + open.type = remove.type = 'button'; open.className = 'open'; remove.className = 'remove'; + name.textContent = b.name; where.textContent = b.name === b.host ? `port ${b.port}` : `${b.host}:${b.port}`; open.append(name, where); + open.title = `Fill in ${b.host}:${b.port}`; remove.textContent = '×'; remove.setAttribute('aria-label', `Remove ${b.name}`); + open.onclick = () => { $('host').value = b.host; $('port').value = b.port; updateBookmarkButton(); $('pin').focus(); }; + remove.onclick = () => { bookmarks = bookmarks.filter(x => x !== b); showBookmarks(); }; + item.append(open, remove); return item; + })); + updateBookmarkButton(); +} +function updateBookmarkButton() { + const a = address(), saved = bookmarkIndex(a) >= 0; + $('bookmark').disabled = !a.host || !Number.isInteger(a.port) || a.port < 1 || a.port > 65535; + $('bookmark').textContent = saved ? '★ Saved' : '☆ Save'; + $('bookmark').setAttribute('aria-pressed', String(saved)); + $('bookmark').title = saved ? 'Remove this Mac from saved Macs' : 'Save this address'; +} +$('bookmark').onclick = () => { + const a = address(), i = bookmarkIndex(a); + if (i >= 0) bookmarks.splice(i, 1); + else bookmarks.push({...a, name: knownHosts.find(h => h.host === a.host && h.port === a.port)?.name ?? a.host}); + showBookmarks(); +}; +$('host').addEventListener('input', updateBookmarkButton); $('port').addEventListener('input', updateBookmarkButton); +showBookmarks(); +setInterval(() => { + if (active) { + const n = v => Number.isFinite(v) ? v.toFixed(1) : '—'; + $('stats').textContent = `${welcome?.isVirtual ? 'Virtual display' : 'Mac display'} · H.264\nReceived ${frames} fps\nDecode ${n(frames ? decodeTotal/frames : 0)} ms\nRound trip ${n(rtt)} ms\nBitrate ${n(hostStats.mbps)} Mbps\nHost encode ${n(hostStats.encodeMs)} ms\nHost drops ${hostStats.dropped ?? '—'}`; + } + frames = 0; decodeTotal = 0; +}, 1000); diff --git a/Windows/ui/index.html b/Windows/ui/index.html new file mode 100644 index 0000000..5c172eb --- /dev/null +++ b/Windows/ui/index.html @@ -0,0 +1,51 @@ + + + + + + + iFrame + + + +
+
iFrameFOR WINDOWS
+
+
+
YOUR MAC, WITHIN REACH
+

A little window.
A whole Mac.

+

Bring your Mac desktop to Windows.
Connect, settle in, and make yourself at home.

+ +

Use a trusted local network or Tailscale.
iFrame uses a PIN; the stream is not encrypted.

+
+
+

Connect to your Mac

01
+ + + +

Or enter an IP address or hostname below.

+
+
+ +
+
+ + +
+ + +
+ +
+
+
MAC → WINDOWSYour workspace, a little closer.
+
+
+
Mac
+
Waiting for the first frame…
+ +
+

Send text to your Mac

Type or paste text here, then send it to the focused field.

+ + + diff --git a/Windows/ui/input.mjs b/Windows/ui/input.mjs new file mode 100644 index 0000000..69e5c1a --- /dev/null +++ b/Windows/ui/input.mjs @@ -0,0 +1,87 @@ +export const keyMap = { + KeyA:0,KeyS:1,KeyD:2,KeyF:3,KeyH:4,KeyG:5,KeyZ:6,KeyX:7,KeyC:8,KeyV:9,IntlBackslash:10,KeyB:11, + KeyQ:12,KeyW:13,KeyE:14,KeyR:15,KeyY:16,KeyT:17,Digit1:18,Digit2:19,Digit3:20,Digit4:21,Digit6:22,Digit5:23, + Equal:24,Digit9:25,Digit7:26,Minus:27,Digit8:28,Digit0:29,BracketRight:30,KeyO:31,KeyU:32,BracketLeft:33, + KeyI:34,KeyP:35,Enter:36,KeyL:37,KeyJ:38,Quote:39,KeyK:40,Semicolon:41,Backslash:42,Comma:43,Slash:44, + KeyN:45,KeyM:46,Period:47,Tab:48,Space:49,Backquote:50,Backspace:51,Escape:53,MetaRight:54,MetaLeft:55, + ShiftLeft:56,CapsLock:57,AltLeft:58,ControlLeft:59,ShiftRight:60,AltRight:61,ControlRight:62, + NumpadDecimal:65,NumpadMultiply:67,NumpadAdd:69,NumLock:71,NumpadDivide:75,NumpadEnter:76,NumpadSubtract:78, + NumpadEqual:81,Numpad0:82,Numpad1:83,Numpad2:84,Numpad3:85,Numpad4:86,Numpad5:87,Numpad6:88,Numpad7:89,Numpad8:91,Numpad9:92, + F1:122,F2:120,F3:99,F4:118,F5:96,F6:97,F7:98,F8:100,F9:101,F10:109,F11:103,F12:111, + F13:105,F14:107,F15:113,F16:106,F17:64,F18:79,F19:80,F20:90, + Insert:114,Home:115,PageUp:116,Delete:117,End:119,PageDown:121,ArrowLeft:123,ArrowRight:124,ArrowDown:125,ArrowUp:126 +}; +export function mapKey(code, swap) { + if (swap) code = ({ControlLeft:'MetaLeft',ControlRight:'MetaRight',MetaLeft:'ControlLeft',MetaRight:'ControlRight'})[code] ?? code; + return keyMap[code]; +} +export function modifiers(e, swap) { + return (+e.shiftKey) | ((swap ? +e.metaKey : +e.ctrlKey) << 1) | (+e.altKey << 2) | + ((swap ? +e.ctrlKey : +e.metaKey) << 3) | (+e.getModifierState('CapsLock') << 4); +} +export function normalizedPoint(x, y, rect, width, height, clamp = false) { + const scale = Math.min(rect.width / width, rect.height / height); + const w = width * scale, h = height * scale; + const nx = (x - rect.left - (rect.width - w) / 2) / w, ny = (y - rect.top - (rect.height - h) / 2) / h; + if (!Number.isFinite(nx) || !Number.isFinite(ny) || (!clamp && (nx < 0 || nx > 1 || ny < 0 || ny > 1))) return null; + return {x: Math.max(0, Math.min(1, nx)), y: Math.max(0, Math.min(1, ny))}; +} +export function attachInput(canvas, api, options) { + const keys = new Map(), buttons = new Set(); let position = {x: 0.5, y: 0.5}, move = null, raf; + const flushMove = () => { if (move) { api.input({kind:'move', ...move}); move = null; } }; + const releaseButtons = () => { + cancelAnimationFrame(raf); move = null; + for (const button of buttons) api.input({kind:'button', button, down:false, ...position}); buttons.clear(); + }; + const release = () => { + for (const code of keys.values()) api.input({kind:'key', code, action:0, mods:0}); keys.clear(); + releaseButtons(); + }; + // Windows keys captured by the fullscreen hook arrive here instead of as DOM events; they are always Command. + const hooked = () => keys.has('HookMetaLeft') || keys.has('HookMetaRight') ? 8 : 0; + const heldMods = () => [...keys.values()].reduce((m, c) => m | (({56:1, 60:1, 59:2, 62:2, 58:4, 61:4, 55:8, 54:8})[c] ?? 0), 0); + api.onMetaKey(({code, down}) => { + const id = 'Hook' + code; if (!options.active() || (!down && !keys.has(id))) return; + const action = down ? (keys.has(id) ? 2 : 1) : 0; + if (down) keys.set(id, keyMap[code]); else keys.delete(id); + api.input({kind:'key', code:keyMap[code], action, mods:heldMods()}); + }); + const point = (e, clamp) => normalizedPoint(e.clientX, e.clientY, canvas.getBoundingClientRect(), canvas.width, canvas.height, clamp); + canvas.addEventListener('pointermove', e => { + if (!options.active()) return; + const p = point(e, buttons.size > 0); if (!p) return; + position = p; move = p; cancelAnimationFrame(raf); raf = requestAnimationFrame(flushMove); + }); + canvas.addEventListener('pointerdown', e => { + if (!options.active()) return; + const p = point(e, false), button = ({0:0, 1:2, 2:1})[e.button]; + if (!p || button === undefined) return; + e.preventDefault(); canvas.focus(); canvas.setPointerCapture(e.pointerId); position = p; flushMove(); buttons.add(button); + api.input({kind:'button', button, down:true, ...p}); + }); + canvas.addEventListener('pointerup', e => { + const button = ({0:0, 1:2, 2:1})[e.button]; if (!buttons.delete(button)) return; + position = point(e, true) ?? position; flushMove(); api.input({kind:'button', button, down:false, ...position}); + }); + canvas.addEventListener('lostpointercapture', releaseButtons); + canvas.addEventListener('pointercancel', releaseButtons); + canvas.addEventListener('contextmenu', e => e.preventDefault()); + canvas.addEventListener('wheel', e => { + if (!options.active() || !point(e, false)) return; + e.preventDefault(); const unit = e.deltaMode === 1 ? 16 : e.deltaMode === 2 ? 800 : 1; + api.input({kind:'scroll', dx:Math.max(-10000,Math.min(10000,-e.deltaX*unit)), dy:Math.max(-10000,Math.min(10000,-e.deltaY*unit))}); + }, {passive:false}); + for (const type of ['keydown','keyup']) canvas.addEventListener(type, e => { + if (!options.active()) return; + e.preventDefault(); e.stopPropagation(); + if (e.ctrlKey && e.altKey && ['KeyF','KeyQ','KeyR'].includes(e.code)) { + if (type === 'keydown' && !e.repeat) { release(); options.shortcut(e.code); } return; + } + const code = keys.get(e.code) ?? mapKey(e.code, options.swap()); if (code === undefined) return; + if (type === 'keyup' && !keys.has(e.code)) return; + api.input({kind:'key', code, action:type === 'keyup' ? 0 : e.repeat ? 2 : 1, mods:modifiers(e, options.swap()) | hooked()}); + if (type === 'keyup') keys.delete(e.code); else keys.set(e.code, code); + }); + canvas.addEventListener('blur', release); window.addEventListener('blur', release); api.onReleaseInput(release); + return release; +} diff --git a/Windows/ui/sps.mjs b/Windows/ui/sps.mjs new file mode 100644 index 0000000..0cbd884 --- /dev/null +++ b/Windows/ui/sps.mjs @@ -0,0 +1,101 @@ +// VideoToolbox writes H.264 SPS without VUI bitstream_restriction. Chromium's decoders then +// assume the stream may reorder up to a full DPB (4+ frames) and hold output until it fills, +// which never happens with the host's three-frame flow-control window. Declaring +// max_num_reorder_frames = 0 is accurate (the host disables frame reordering) and makes the +// decoder output every frame as soon as it is decoded. Same technique as WebRTC's SpsVuiRewriter. + +function unescape(bytes) { + const out = []; let zeros = 0; + for (const b of bytes) { + if (zeros >= 2 && b === 3) { zeros = 0; continue; } + zeros = b === 0 ? zeros + 1 : 0; out.push(b); + } + return out; +} +function escape(bytes) { + const out = []; let zeros = 0; + for (const b of bytes) { + if (zeros >= 2 && b <= 3) { out.push(3); zeros = 0; } + zeros = b === 0 ? zeros + 1 : 0; out.push(b); + } + return Uint8Array.from(out); +} + +class Reader { + constructor(bytes) { this.bytes = bytes; this.pos = 0; } + bit() { + if (this.pos >= this.bytes.length * 8) throw new Error('Truncated SPS'); + const b = (this.bytes[this.pos >> 3] >> (7 - (this.pos & 7))) & 1; this.pos++; return b; + } + bits(n) { let v = 0; for (let i = 0; i < n; i++) v = v * 2 + this.bit(); return v; } + ue() { let zeros = 0; while (!this.bit()) if (++zeros > 31) throw new Error('Invalid SPS'); return 2 ** zeros - 1 + this.bits(zeros); } + se() { const v = this.ue(); return v & 1 ? (v + 1) / 2 : -v / 2; } +} +class Writer { + constructor() { this.out = []; this.pos = 0; } + bit(b) { if (!(this.pos & 7)) this.out.push(0); if (b) this.out[this.out.length - 1] |= 0x80 >> (this.pos & 7); this.pos++; } + bits(n, v) { for (let i = n - 1; i >= 0; i--) this.bit(Math.floor(v / 2 ** i) & 1); } + ue(v) { const n = Math.floor(Math.log2(v + 1)); this.bits(n, 0); this.bits(n + 1, v + 1); } + // Copy the bits a reader consumed since `from`. + copy(r, from) { for (let p = from; p < r.pos; p++) this.bit((r.bytes[p >> 3] >> (7 - (p & 7))) & 1); } +} + +function hrd(r) { + const count = r.ue() + 1; r.bits(8); + for (let i = 0; i < count; i++) { r.ue(); r.ue(); r.bit(); } + r.bits(20); +} + +/// Returns the SPS NAL unit (header byte included) with max_num_reorder_frames = 0, or the +/// original unit when it already says so. Tests pass `reorder = null` to strip the restriction. +export function lowDelaySPS(nal, reorder = 0) { + const r = new Reader(unescape(nal.subarray(1))), w = new Writer(); + const profile = r.bits(8); r.bits(16); r.ue(); + if ([100, 110, 122, 244, 44, 83, 86, 118, 128, 138, 139, 134, 135].includes(profile)) { + const chroma = r.ue(); if (chroma === 3) r.bit(); + r.ue(); r.ue(); r.bit(); + if (r.bit()) for (let i = 0; i < (chroma === 3 ? 12 : 8); i++) if (r.bit()) { + let last = 8, next = 8; + for (let j = 0; j < (i < 6 ? 16 : 64) && next; j++) { next = (last + r.se() + 256) % 256; last = next || last; } + } + } + r.ue(); + const poc = r.ue(); + if (poc === 0) r.ue(); + else if (poc === 1) { r.bit(); r.se(); r.se(); const n = r.ue(); for (let i = 0; i < n; i++) r.se(); } + const refs = r.ue(); r.bit(); r.ue(); r.ue(); + if (!r.bit()) r.bit(); + r.bit(); + if (r.bit()) { r.ue(); r.ue(); r.ue(); r.ue(); } + w.copy(r, 0); + let mv = 1, bytesDenom = 2, bitsDenom = 1, mvH = 15, mvV = 15; + const buffering = Math.max(1, refs); + if (!r.bit()) { + w.bit(1); w.bits(8, 0); // VUI with nothing but the restriction + } else { + w.bit(1); const start = r.pos; + if (r.bit() && r.bits(8) === 255) r.bits(32); + if (r.bit()) r.bit(); + if (r.bit()) { r.bits(4); if (r.bit()) r.bits(24); } + if (r.bit()) { r.ue(); r.ue(); } + if (r.bit()) { r.bits(32); r.bits(32); r.bit(); } + const nalHRD = r.bit(); if (nalHRD) hrd(r); + const vclHRD = r.bit(); if (vclHRD) hrd(r); + if (nalHRD || vclHRD) r.bit(); + r.bit(); + w.copy(r, start); + if (r.bit()) { + mv = r.bit(); bytesDenom = r.ue(); bitsDenom = r.ue(); mvH = r.ue(); mvV = r.ue(); + if (r.ue() === reorder) return nal; + r.ue(); // max_dec_frame_buffering: recomputed below + } + } + if (reorder === null) { w.bit(0); return finish(nal, w); } + w.bit(1); w.bit(mv); w.ue(bytesDenom); w.ue(bitsDenom); w.ue(mvH); w.ue(mvV); w.ue(reorder); w.ue(Math.max(buffering, reorder)); + return finish(nal, w); +} +function finish(nal, w) { + w.bit(1); while (w.pos & 7) w.bit(0); + const body = escape(w.out), result = new Uint8Array(1 + body.length); + result[0] = nal[0]; result.set(body, 1); return result; +} diff --git a/Windows/ui/style.css b/Windows/ui/style.css new file mode 100644 index 0000000..1143553 --- /dev/null +++ b/Windows/ui/style.css @@ -0,0 +1,13 @@ +:root{font-family:"Segoe UI",sans-serif;color:#edf0eb;background:#101416;color-scheme:dark;font-synthesis:none}*{box-sizing:border-box}body{margin:0}button,input,select,textarea{font:inherit}button,select{cursor:pointer}button{border:1px solid #384143;border-radius:7px;padding:9px 13px;background:#232b2c;color:#e9eeea}button:hover{background:#303b3c}button:disabled{opacity:.55;cursor:wait}button:focus-visible,input:focus-visible,select:focus-visible,textarea:focus-visible{outline:2px solid #ccedb4;outline-offset:3px}[hidden]{display:none!important}#connect-page{max-width:1250px;margin:auto;padding:30px 48px 22px}.brand{display:flex;align-items:center;gap:12px;font-size:25px;font-weight:650;letter-spacing:-1px}.brand img{border-radius:12px}.platform{font-size:10px;color:#8a9695;letter-spacing:2px;margin-left:10px;font-weight:500}.connection-layout{display:grid;grid-template-columns:1.05fr 1fr;gap:60px;align-items:center;margin:36px 0}.eyebrow{font-size:10px;letter-spacing:2px;color:#c5dab8;display:flex;align-items:center;gap:10px}.dot{display:inline-block;width:7px;height:7px;border-radius:50%;background:#ceeab9;box-shadow:0 0 14px #d0edb13b}h1{font-size:clamp(38px,4.7vw,60px);line-height:1.09;letter-spacing:-2.6px;font-weight:500;margin:22px 0}p{color:#99a6a5}.lead{font-size:15px;line-height:1.85}.connect-card{background:#1a2022;border:1px solid #30393a;border-radius:16px;padding:26px 28px;box-shadow:0 22px 70px #0003}.card-heading{display:flex;align-items:center;justify-content:space-between;margin-bottom:25px}h2{font-size:20px;font-weight:550;letter-spacing:-.5px;margin:0}.version{font-family:monospace;color:#73817e;font-size:12px}label{display:block;font-size:10px;letter-spacing:1.4px;color:#a9b6b3;margin:15px 0 8px}input,select,textarea{background:#121819;color:#e2e9e4;border:1px solid #3a4544;border-radius:7px;padding:11px 12px;width:100%;font-size:13px;min-width:0}input::placeholder{color:#778381}select{padding-right:24px}.hint{font-size:11px;margin:9px 0 18px}.address-row{display:grid;grid-template-columns:1fr 80px auto;gap:12px}.settings-row{display:grid;grid-template-columns:1fr 110px;gap:12px}.separator{height:1px;background:#303939;margin:23px 0 4px}.check{display:flex;align-items:center;gap:9px;font-size:12px;letter-spacing:0;margin:18px 0 20px;color:#b9c5be}.check input{width:14px;height:14px;accent-color:#cbe7b5}.primary{background:#d2edb9;color:#203024;border:0;font-weight:650;padding:13px 16px}.primary:hover{background:#e1f6d0}#connect{display:flex;align-items:center;justify-content:space-between;width:100%}#connect span{font-size:20px;font-weight:400}.quiet{background:transparent;width:100%;margin-top:10px}.card-footer{font-size:11px;text-align:center;margin:17px 0 0;color:#7e8d89}code{font-size:11px;color:#a9b8ae}#status{font-size:12px;line-height:1.6;color:#ecc4a7;margin-bottom:12px;white-space:pre-wrap}#status:empty{display:none}.network-note{font-size:11px;line-height:1.7;color:#75847f;margin-top:28px}.page-footer{display:flex;justify-content:space-between;border-top:1px solid #293233;padding-top:18px;font-size:10px;color:#7f9089;letter-spacing:1px}.page-footer span:first-child{letter-spacing:2px}.desktop-art{margin-top:30px;max-width:380px}.art-window{border:1px solid #526356;border-radius:10px;overflow:hidden;transform:perspective(1000px) rotateY(8deg) rotateX(2deg);box-shadow:10px 25px 60px #0007}.art-bar{display:flex;gap:5px;align-items:center;height:25px;background:#303c37;padding:0 10px}.art-bar i{width:5px;height:5px;border-radius:50%;background:#8c9e90}.art-bar span{font-size:9px;flex:1;text-align:center;color:#b2c1b4;margin-right:25px}.art-desktop{height:180px;background:linear-gradient(135deg,#293b35,#5a785e);position:relative;overflow:hidden}.orb{position:absolute;width:240px;height:240px;border-radius:50%;background:linear-gradient(120deg,#c5d7a980,#41664d);box-shadow:inset 5px 8px 22px #dbe8a43b}.orb.one{top:45px;left:-55px}.orb.two{right:-70px;top:-90px;width:270px;height:270px;background:linear-gradient(100deg,#718674,#253e30)}.art-card{position:absolute;top:35px;left:100px;width:175px;height:100px;border-radius:7px;border:1px solid #9bae9580;background:#dae5c9e6;box-shadow:5px 10px 30px #162c3440;padding:19px}.art-card span{display:block;width:40px;height:7px;background:#6b8464;border-radius:2px;margin-bottom:14px}.art-card b{display:block;height:4px;background:#9dab93;margin:7px 0;border-radius:2px}.art-card b:last-child{width:65%}.art-dock{display:flex;gap:6px;position:absolute;bottom:8px;left:50%;transform:translateX(-50%);padding:6px 9px;background:#b8c8b14d;border:1px solid #c9d7b740;border-radius:8px}.art-dock i{width:15px;height:15px;border-radius:4px;background:#c6d9b5}.art-dock i:nth-child(even){background:#8dada2}.art-caption{display:block;text-align:center;font-size:10px;color:#77897e;margin-top:15px}#stream-page{height:100vh;display:flex;flex-direction:column}.toolbar{display:flex;align-items:center;justify-content:space-between;gap:15px;padding:12px 16px;border-bottom:1px solid #303939;background:#1a2022}.stream-name{display:flex;align-items:center;gap:10px;min-width:0}.stream-name strong{font-size:13px;max-width:180px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}#stream-info{font-size:11px;color:#8a9b93}.toolbar-actions{display:flex;gap:7px;flex-shrink:0}.toolbar button{font-size:11px;padding:7px 10px}.disconnect{color:#f0c2a8}#viewport{flex:1;min-height:0;background:#090c0d;position:relative;display:flex;align-items:center;justify-content:center}canvas{width:100%;height:100%;object-fit:contain;outline:none;cursor:crosshair;touch-action:none}canvas:focus{cursor:none}#waiting{position:absolute;color:#a1b49f;font-size:14px;pointer-events:none}#stats{position:absolute;left:16px;top:16px;background:#121917e8;border:1px solid #647a5655;border-radius:8px;padding:13px 16px;font:12px/1.9 Consolas,monospace;color:#d4e6c8;white-space:pre;pointer-events:none}.stream-footer{padding:8px 16px;font-size:10px;color:#8ea084;display:flex;justify-content:space-between}.stream-footer b{color:#aeb9ae;font-weight:400;margin-left:4px}dialog{width:480px;border:1px solid #526052;border-radius:12px;background:#1a2220;padding:26px}dialog::backdrop{background:#0009}dialog p{font-size:13px;line-height:1.6}textarea{resize:vertical}.dialog-actions{display:flex;justify-content:flex-end;gap:10px;margin-top:16px}@media(max-width:950px){#connect-page{padding:24px 28px}.connection-layout{gap:28px}.connect-card{padding:23px}.desktop-art{max-width:310px}.art-card{left:70px}.stream-name #stream-info{display:none}h1{font-size:42px}}@media(max-height:760px){.connection-layout{margin:22px 0}.card-heading{margin-bottom:16px}.connect-card{padding:20px 25px}label{margin-top:11px}.separator{margin-top:17px}.desktop-art{margin-top:20px}.art-desktop{height:150px}input,select{padding:9px 10px}} +@media(max-height:900px){#connect-page{padding-top:22px;padding-bottom:16px}.connection-layout{margin:22px 0}.card-heading{margin-bottom:16px}.connect-card{padding:20px 25px}label{margin-top:11px}.separator{margin-top:17px}.desktop-art{margin-top:20px}.art-desktop{height:150px}input,select{padding:9px 10px}.check{margin:15px 0}.page-footer{padding-top:12px}} +.bookmark-toggle{height:41px;white-space:nowrap;font-size:12px} +.bookmark-toggle[aria-pressed=true]{color:#d2edb9;border-color:#5d7353} +.bookmarks{display:flex;flex-wrap:wrap;gap:8px;margin-bottom:6px} +.bookmark{display:flex;border:1px solid #3a4544;border-radius:7px;background:#121819;overflow:hidden} +.bookmark button{border:0;border-radius:0;background:transparent;padding:7px 10px;font-size:12px} +.bookmark button:hover{background:#232b2c} +.bookmark .open{display:flex;flex-direction:column;align-items:flex-start;line-height:1.35;max-width:220px} +.bookmark .open span{max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} +.bookmark small{font-size:10px;color:#8a9695;font-family:Consolas,monospace} +.bookmark .remove{color:#8a9695;padding:7px 9px;border-left:1px solid #2b3434} +.bookmark .remove:hover{color:#f0c2a8} diff --git a/Windows/ui/video.mjs b/Windows/ui/video.mjs new file mode 100644 index 0000000..9f196e5 --- /dev/null +++ b/Windows/ui/video.mjs @@ -0,0 +1,134 @@ +import { lowDelaySPS } from './sps.mjs'; +export function parseFormat(bytes) { + const b = new Uint8Array(bytes), view = new DataView(b.buffer, b.byteOffset, b.byteLength); + if (b.length < 2 || b[0] !== 0) throw new Error('Only H.264 is supported. Disable forced HEVC on the Mac.'); + let offset = 2; const sets = []; + for (let i = 0; i < b[1]; i++) { + if (offset + 4 > b.length) throw new Error('Truncated video format'); + const length = view.getUint32(offset); offset += 4; + if (!length || offset + length > b.length) throw new Error('Invalid parameter set'); + const set = b.slice(offset, offset + length); offset += length; + sets.push((set[0] & 31) === 7 ? lowDelaySPS(set) : set); + } + if (offset !== b.length) throw new Error('Unexpected video format data'); + const sps = sets.find(s => (s[0] & 31) === 7), pps = sets.find(s => (s[0] & 31) === 8); + if (!sps || sps.length < 4 || !pps) throw new Error('Missing H.264 parameter sets'); + return { sets, codec: 'avc1.' + [...sps.slice(1, 4)].map(v => v.toString(16).padStart(2, '0')).join('') }; +} +// WebCodecs Annex B mode: convert the host's four-byte AVCC lengths to start codes. +export function annexB(bytes, sets = []) { + const b = new Uint8Array(bytes), view = new DataView(b.buffer, b.byteOffset, b.byteLength); + const prefix = sets.reduce((n, s) => n + 4 + s.length, 0); + const result = new Uint8Array(prefix + b.length); let out = 0; + for (const set of sets) { result.set([0, 0, 0, 1], out); out += 4; result.set(set, out); out += set.length; } + let offset = 0; + while (offset < b.length) { + if (offset + 4 > b.length) throw new Error('Truncated NAL length'); + const length = view.getUint32(offset); offset += 4; + if (!length || offset + length > b.length) throw new Error('Truncated NAL unit'); + result.set([0, 0, 0, 1], out); out += 4; + result.set(b.subarray(offset, offset + length), out); out += length; offset += length; + } + if (!b.length) throw new Error('Empty video frame'); + return result; +} + +export class Video { + constructor(canvas, api, onError, onFrame = () => {}, onStatus = () => {}) { + this.canvas = canvas; this.context = canvas.getContext('2d', {alpha: false, desynchronized: true}); + this.api = api; this.onError = onError; this.onFrame = onFrame; this.onStatus = onStatus; + this.pending = new Map(); this.serial = 0; this.recoveries = 0; + this.acceleration = 'no-preference'; + } + stopDecoder() { + clearTimeout(this.watchdog); + this.generation = (this.generation ?? 0) + 1; + if (this.decoder && this.decoder.state !== 'closed') this.decoder.close(); + this.decoder = null; + } + close() { + this.stopDecoder(); + for (const {id} of this.pending.values()) this.api.ack(id, 0); + this.pending.clear(); this.waitKey = true; + } + configure(bytes) { + this.close(); this.format = parseFormat(bytes); + this.createDecoder(); + } + createDecoder() { + const generation = this.generation; + this.decoder = new VideoDecoder({ + output: frame => { + try { + if (generation !== this.generation) return; + const pending = this.pending.get(frame.timestamp); + if (!pending) throw new Error('Decoder returned an unexpected frame timestamp'); + if (this.canvas.width !== frame.displayWidth || this.canvas.height !== frame.displayHeight) { + this.canvas.width = frame.displayWidth; this.canvas.height = frame.displayHeight; + } + this.context.drawImage(frame, 0, 0, this.canvas.width, this.canvas.height); + if (pending) { + const micros = (performance.now() - pending.start) * 1000; + this.api.ack(pending.id, micros); this.pending.delete(frame.timestamp); this.onFrame(micros / 1000); + this.recoveries = 0; this.armWatchdog(); + } + } catch (error) { this.recover(error); + } finally { frame.close(); } + }, + error: error => { if (generation === this.generation) this.recover(error); } + }); + this.decoder.configure({ codec: this.format.codec, optimizeForLatency: true, hardwareAcceleration: this.acceleration }); + } + armWatchdog() { + clearTimeout(this.watchdog); + if (!this.pending.size) return; + const generation = this.generation; + this.watchdog = setTimeout(() => { + if (generation !== this.generation || !this.pending.size) return; + if (this.acceleration === 'no-preference') this.useSoftwareDecoder(); + else this.recover(new Error('Software decoder did not output a frame')); + }, this.acceleration === 'no-preference' ? 1000 : 2000); + } + useSoftwareDecoder() { + // The host permits only three frames in flight. A hardware decoder that buffers + // four or more before output deadlocks that window, even with optimizeForLatency. + // Replay the initial keyframe and its deltas without acknowledging undecoded data. + const waiting = [...this.pending.entries()]; + this.acceleration = 'prefer-software'; + this.onStatus('Trying the compatibility video decoder…'); + if (!waiting.length || !waiting[0][1].key) { + this.recover(new Error('Hardware decoder stopped producing frames')); return; + } + this.stopDecoder(); + try { + this.createDecoder(); + for (const [timestamp, frame] of waiting) + this.decoder.decode(new EncodedVideoChunk({type:frame.key ? 'key' : 'delta', timestamp, data:frame.data})); + this.waitKey = false; this.armWatchdog(); + } catch (error) { this.recover(error); } + } + recover(error) { + // Set the host's keyframe flag before freeing its flow-control window. + this.api.keyframe(); + this.close(); + if (++this.recoveries > 3) return this.onError(`H.264 decoding failed: ${error.message}. Try 1920 × 1080 or reconnect.`); + this.acceleration = 'prefer-software'; + try { this.createDecoder(); this.api.keyframe(); } catch (e) { this.onError(e.message); } + } + frame(bytes) { + const b = new Uint8Array(bytes); + if (b.length < 14) throw new Error('Truncated frame'); + const view = new DataView(b.buffer, b.byteOffset, b.byteLength), id = view.getUint32(0), key = !!(b[12] & 1); + if (!this.decoder || (this.waitKey && !key)) { this.api.ack(id, 0); this.api.keyframe(); return; } + try { + const data = annexB(b.subarray(13), key ? this.format.sets : []); + // Unique local timestamps also work across host display restarts and repeated PTS. + const timestamp = ++this.serial * 16667; + this.pending.set(timestamp, {id, start: performance.now(), key, data}); + this.decoder.decode(new EncodedVideoChunk({type: key ? 'key' : 'delta', timestamp, data})); + this.waitKey = false; + // Do not postpone the deadline each time an additional buffered frame arrives. + if (this.pending.size === 1) this.armWatchdog(); + } catch (e) { this.api.ack(id, 0); this.recover(e); } + } +}