From 097f7d08f5d9e0dc006f3a106b92d7db16752f37 Mon Sep 17 00:00:00 2001 From: Todd Faulls Date: Mon, 5 Oct 2026 15:31:30 -0400 Subject: [PATCH 1/2] Add Windows client MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Electron client for the existing Mac host (protocol v1, H.264): Bonjour discovery, PIN login, virtual-display settings, WebCodecs decoding with ack-based flow control, keyboard/mouse/text input, fullscreen and stats. - Rewrite the H.264 SPS to declare max_num_reorder_frames = 0. VideoToolbox omits VUI bitstream_restriction, so Chromium's decoders buffered a full DPB (4+ frames) before output and deadlocked against the host's three-frame window ("Waiting for the first frame…" / "Video decoding stalled"). The host disables reordering, so this is accurate. - In fullscreen, capture the Windows key with a low-level keyboard hook (koffi) and send it to the Mac as Command instead of opening Start. - CI workflow builds, tests and smoke-tests the packaged app. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/windows.yml | 40 + .gitignore | 5 + README.md | 24 +- Windows/README.md | 68 + Windows/assets/icon.png | Bin 0 -> 65927 bytes Windows/package-lock.json | 3661 ++++++++++++++++++++++++++++++++ Windows/package.json | 52 + Windows/src/main.cjs | 78 + Windows/src/preload.cjs | 19 + Windows/src/protocol.cjs | 77 + Windows/src/session.cjs | 85 + Windows/src/winkey.cjs | 50 + Windows/test/desktop.h264 | Bin 0 -> 7042 bytes Windows/test/protocol.test.cjs | 89 + Windows/test/smoke.cjs | 98 + Windows/test/sps.test.cjs | 19 + Windows/ui/app.mjs | 97 + Windows/ui/index.html | 50 + Windows/ui/input.mjs | 87 + Windows/ui/sps.mjs | 101 + Windows/ui/style.css | 2 + Windows/ui/video.mjs | 134 ++ 22 files changed, 4834 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/windows.yml create mode 100644 Windows/README.md create mode 100644 Windows/assets/icon.png create mode 100644 Windows/package-lock.json create mode 100644 Windows/package.json create mode 100644 Windows/src/main.cjs create mode 100644 Windows/src/preload.cjs create mode 100644 Windows/src/protocol.cjs create mode 100644 Windows/src/session.cjs create mode 100644 Windows/src/winkey.cjs create mode 100644 Windows/test/desktop.h264 create mode 100644 Windows/test/protocol.test.cjs create mode 100644 Windows/test/smoke.cjs create mode 100644 Windows/test/sps.test.cjs create mode 100644 Windows/ui/app.mjs create mode 100644 Windows/ui/index.html create mode 100644 Windows/ui/input.mjs create mode 100644 Windows/ui/sps.mjs create mode 100644 Windows/ui/style.css create mode 100644 Windows/ui/video.mjs 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 aaa5fd7..6ac73e1 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,8 @@ iFrame.xcodeproj/ .swiftpm/ *.xcuserstate xcuserdata/ + +# Windows client +Windows/node_modules/ +Windows/dist/ +Windows/test-output/ diff --git a/README.md b/README.md index c3e6cdf..406a1be 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) @@ -65,6 +67,24 @@ Your Mac appears under **Mac** on the connect screen. Enter the PIN, pick a disp | 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..1b328c2 --- /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. +4. Enter the host PIN, choose the display size/density, and click **Connect to Mac**. + +The app remembers the 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 0000000000000000000000000000000000000000..8c2916e88cbd13bb1efabb1e9b41e777093f7de7 GIT binary patch literal 65927 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

G%Vl=7M{xpT@AH1tzPi;8o*6w}9PxdkSjAhk zO^@E&1)brLon&`d=Y1dio)2b{?LT|g%3>eH`rtK&4zrTc_DM~`@KE+!&b`Op=ve$`(lKw#%R2f#X|tJ_zFBs5 zg4AH7u0f*Dj;G+l7pT_9b9=@Ki1W;G!arRct0y! zo4`ytMtv$8Xm}Az&CI4*4nZkYj(a_Qsde2}jhms)F zMaggqq#7$QK!Fcn`I*@;G~yb_;m1r-EgK^eNc&stc!dy;udkwCL3|${tW=K>KM2+1 z-2Rcq=hhD(4D|w3YE&oIi~k8f70k#ylgWi(wjde_i^MT4+-V{m7(^m$i}CmVUW_Im zlsGAY4ngdNu;g_W2Gc24uBKC{;b<*EA7hd?5y&csG!JZaM7QNOM!%s2D@hrbY)kzi ziC&cbvniS)EV?HE96g5u&#dNkCV29yVP>Yn?=m{;zSIWq^})0)w(!=AjyEGPu%C54_;i|p1u>ys!HZfxj2N&f8r{;50;XjaFim|cUc_rO zD=K}ehQQY;OyD4Hcr5?<-2fLt&X1>jCah+iLsjAt$J@xMqibibc?rLzG<*J;58#3N zzUZa=RD|_Gt@l~2-aiOo1{8MB3i(R)d4eT;TjLwEdd-^jwxyWck&ESK1vO+pFvFu)1 zd+oy#=;VRXifvN}bT(q&5@&`q=M+~#g!k9*6LOvZF+pp7gj_UFz^u+K*(~)e*R&ti z?AlRHi!)j@!OQ@vi4nf)z%u!N!&94ZO0Ep$4wI={b#OUvT`--;W+9QV*T$!st2>qTIZ~C^=v)bOz#g){)X@)bab+lQj!9X#w;Q=*u>#RwE ziJ<cN>7+DRcL+&y?z9R{D}qz*j*j z7&CWR_oWbDjAsSDo~**6+DjQuK(ZLwvg+`3)`>Bz%6{g9TeoCxmJOH1qoYbWAAF!X zzNUA-nrBfRDhp1of-YsiJ_o%^g+(nZvQ!!unBAYOM_e_E8B_GJzOA2u9!=RfGu~YX zRxSdn(C7$}Y3B*QR%mZ&Q1iSBTH;8BDM-n{RyF7d+i&?o=@M^uwtt%D2#3J_8L9uV z*o$N-w}ae#di)^f)Y*;0_Tp~~iH0zSe=X^SMm~Y_7naLQA(d3vie#NyNBt=NDN_Fmkw;CKq!x?<{_Qja@rb#$ET{>gD7PN;Lx~^tBLXdEUn0wV6Igm%Q*L#g2A_ z4~|_j2g>9}x|9rYZd)A9h3?xA4bKvPTFl`Yn0-I?~SxYqc`*iTYp_7}n$^KMoZx?-E0w zp^0T7ZiJrVQy+7-}@UN^;@{!azy8Ccxmy%{yEDk@zVzyhMaX)7ht%!EW}P8 zOm5i)7eePnZf}P|w#5()-Ep-&A5`LaB=%169rXXa70!N&cX}sJij=y1Z+@Dd(R)>= z?0BsxL-zN;1rgjz2>S)}@OKxSRgR=DF3QY6UmAA*1gc2z*R$&*vnO?lt)Rwwz}hTZZeSeOrxP&zn;*7x-}BIE1rZ44&<8WXbpo7jDzQ!2?9$+9TI zZ*KCk(cH>zZ7LS#ON^Ipy(;YsoDRY7iqEVEQbo6i>I6h7>~z`HUJ$ED42S{yDyv`F zX0;1Sip}<9)}CVTJ!PidoiqfNfj@XME*)ybh}s8%+`r^7Uxgm z&xfg_XJI&CT=>^aR7YjyYlT~A`$rF+`srjHH|CiPKyP6V7(9eIP65>WG%KV-Jm{Cq z!Q()CGD-a`UD#+yzk+O`yLQEA&Y@yj_X^Jlys8!3VI5Cxzf`GO^X00^lD?b90@)YQ z>c_uD?E(IcxE?mhFB=w`Ay|iR=fS)MmL-lWl+wdKn6}sl;r0T@eb8qsE`f|Grp^9s zBQml_D_N5|*8Go3lGK6M^sz7hBx_%4ZD<}_e{Xa%e>jggki*U8oR(sHBHnhG?y!l_ zxOX0Bp2%ED8%zzPR5Wg$v8sR0R7W$dGG&JpbvW*xR{Je73J#mxBN?#2i{Ewv7l%KG zUstV>?dk5xzoFvr_rl$)P7*aMWgGD%W8xLET@%36zVzd!<*vBDA~90O2jeP0UbN-W zU0iMtt5dBh`_9&U|C--!e%aBswwQGM+RmO+S}6zMcwW zRLvl(P^ADlef6xo@zsy)KE&y7akH^%)wRpL+oL+|UL|TpJK7t0!}n#O;kShbw-KUF z;QC;?7y8-agNMwzjj6l{WBvq%!1WKoQa>>!3Wj5%SBE!NKy9q%foJu(Bx3t~y~FAG zDN409KjgBNiwoMzZh~6^wx%k49CgK6?athPYM8P2u`!I2862YSfnQ;&{Q4&nz6iecj3XKM{F**ePa_j9A|R7~sKjF6A?#20 zPBZ*=?B!m_;YKsT;pPTaQwoMo`yhJ23Ov|ksr8YngV3=77;dX^A@1g~8u1nH$%!vA zN;flMJ#MC=$cT@r3`BvHntjC)Ytp&GfObPk!)(wZU;*u#V$BCMBh69j7*^n_-;uWd$brC4+w!c^-llZfNc0RpAk{MErP{#%EOtNzE9Y#6@e0Tk- z;5T={nhesJ*Mc85!WcZ(0AwB9mHqm?hW6<(7o?>-1l=>4WL2$L)<+h#GFZdoMcMB( z3(aaC=7$8AVyT(=8Pw7T$(A)V)`%532An?4iv=rZHUMdUb%7qk)i9qv)R$k+JJsy^ zTKzO^QUedNh#*Q;A*07nyGYzlA|q@8XMybFU(y-2ppzZP6SI`*vQ*edF2_2*c#v-F?X1}#l)b3O%iJt^s4lsNiP3fZ0Dc6>jZoF zfc{N@ilRO}j7!Wd{cZ{_J7`4079P?_JmIdI-bk)-J2Av+)I%MX^6Abcw~yESafQvM zlo$>y#rcl&3)0jb!<|s-87|0@IZDJ_GwXdii!bi^h{zFSQLM>rSb8g^9&VuU?G>k!l1hC zVQ_fEgrr0VE8k8x^Yzn&8GecpscyMqN++5kN~1zV#s>!o;i03l!&P zZ&mh+W7)mw%{uG!4$_MVW|Tk<)JPHowETE#3XE`8P{)H8AoVWqP6(-eF?1I|GJVG+ zNCE;DnnDR=YWLKgbsx-s-xlxSsw2DQ8CqDOhVl?M=3cuWhxQx0TU~M!6_VT!_!%I< zQ+~Mjy1|Swe0wd=^8np^(S&!}7LIYpcKb_tD>LCVO(%kbN2~cmm-orhhRwoblxhhm z6$tdUyj@VHj(g)pRQa6Or{Fc9^wqyKZk1CNO`4+e4^3@<+2@^GJs`w3)7I1(Ael+l z1{FpOOT zs)G&_E+bn&avyB})xWUxGgnQ(ad)&tmc@Ne_SK;}U+);2HB(4! zI0)Mb%+7S-K6aXwVA()T4>yYHWQRpr?XoaMzk8c#1J{&T5O8mcc<8a)PU;pZZKees zgX8#J1SQU7RpE+M$^MA*^(79zm}l~LFptF)j?AKAJlEYl!@w77hP`d)69aY2b`rTU zP|ef+2#JSaLen?YxSHjk@gio0YLATc{H%E8az9!&1)bINq=9nJ%lG5^$0J{0A&Fz( z$HpR9j=t>mG?0)r*zF>5ZnP!JQeo;fRXq{PvnMqZ0F_j67hLx6Vu#l?@ zlBkB^cxd4brWdbFZta8kfa9w=bPY{dof+ufsZ$yz76>}{i=G!#VqU02Sf8|fn@9s3 z)MUoR@X}fEEi^bz9L&M)Nt9}^B=_xF$?ynwe%`}uQ0MA6K7 zv?fcw!p_GQeO@mLVhIr6FGYllNBusL7VjD9WE<_ryQJRDHzlXq0YaZ+G8FVkCZSzfkV4Wyd48I}{$He>p@vkx3 zDs`r!v75)-vWnZM+n#YBTQv+8lJVq=C`aemiHZbaXUk7che6JT=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 0000000000000000000000000000000000000000..8fe4ea07ef6782462a135ecff4785edcba4b52a7 GIT binary patch literal 7042 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 { + 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..e742ecb --- /dev/null +++ b/Windows/ui/app.mjs @@ -0,0 +1,97 @@ +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')) input.disabled = connecting; + 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; $('pin').focus(); +}; +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..01ecd81 --- /dev/null +++ b/Windows/ui/index.html @@ -0,0 +1,50 @@ + + + + + + + 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..b21ca09 --- /dev/null +++ b/Windows/ui/style.css @@ -0,0 +1,2 @@ +: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;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}} 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); } + } +} From 1fabbd3ec5bd9f9f6c5f03cf1069c0cd4f409a60 Mon Sep 17 00:00:00 2001 From: Todd Faulls Date: Mon, 5 Oct 2026 15:38:05 -0400 Subject: [PATCH 2/2] Windows client: bookmark Mac addresses MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Save the current address and port with ☆ Save; saved Macs appear as chips above nearby Macs and fill in the address with one click. Stored in local app data; the PIN is never saved. Co-Authored-By: Claude Opus 5.5 --- Windows/README.md | 4 ++-- Windows/ui/app.mjs | 41 +++++++++++++++++++++++++++++++++++++++-- Windows/ui/index.html | 3 ++- Windows/ui/style.css | 13 ++++++++++++- 4 files changed, 55 insertions(+), 6 deletions(-) diff --git a/Windows/README.md b/Windows/README.md index 1b328c2..a7b8ee0 100644 --- a/Windows/README.md +++ b/Windows/README.md @@ -6,10 +6,10 @@ A Windows 10/11 x64 client for the existing `iframe-host` on your Mac. The porta 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. +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 the 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. +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. diff --git a/Windows/ui/app.mjs b/Windows/ui/app.mjs index e742ecb..ecf0a6c 100644 --- a/Windows/ui/app.mjs +++ b/Windows/ui/app.mjs @@ -51,7 +51,8 @@ $('text-dialog').addEventListener('close', () => { 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')) input.disabled = 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; @@ -86,8 +87,44 @@ 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; $('pin').focus(); + $('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) : '—'; diff --git a/Windows/ui/index.html b/Windows/ui/index.html index 01ecd81..5c172eb 100644 --- a/Windows/ui/index.html +++ b/Windows/ui/index.html @@ -20,11 +20,12 @@

A little window.
A whole Mac.

Connect to your Mac

01
+

Or enter an IP address or hostname below.

-
+
diff --git a/Windows/ui/style.css b/Windows/ui/style.css index b21ca09..1143553 100644 --- a/Windows/ui/style.css +++ b/Windows/ui/style.css @@ -1,2 +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;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}} +: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}