diff --git a/.gitattributes b/.gitattributes index 85b0b3075..66b2b7202 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,3 +1,4 @@ opennow-stable/src/main/gfn/games.ts whitespace=blank-at-eol,blank-at-eof,space-before-tab,cr-at-eol opennow-stable/src/main/ipc/accountCatalogHandlers.ts whitespace=blank-at-eol,blank-at-eof,space-before-tab,cr-at-eol opennow-stable/src/renderer/src/components/HomePage.tsx whitespace=blank-at-eol,blank-at-eof,space-before-tab,cr-at-eol +*.patch text eol=lf diff --git a/.github/workflows/auto-build.yml b/.github/workflows/auto-build.yml index d72f2a996..2d0f54c2a 100644 --- a/.github/workflows/auto-build.yml +++ b/.github/workflows/auto-build.yml @@ -86,7 +86,8 @@ jobs: const isPullRequest = eventName === "pull_request"; const isDevValidationPullRequest = isPullRequest && baseRef === "dev"; const include = isDevValidationPullRequest - ? builds.filter(({ label }) => label === "windows-x64" || label === "linux-x64") + ? builds.filter(({ label }) => + label === "windows-x64" || label === "linux-x64" || label === "linux-arm64") : builds; fs.appendFileSync(process.env.GITHUB_OUTPUT, `matrix=${JSON.stringify({ include })}\n`); @@ -122,6 +123,32 @@ jobs: with: bun-version: 1.3.14 + - name: Setup Rust + if: runner.os == 'Linux' + uses: dtolnay/rust-toolchain@stable + + - name: Install Linux native streamer dependencies + if: runner.os == 'Linux' + env: + DEBIAN_FRONTEND: noninteractive + run: | + sudo apt-get update + sudo apt-get install -y --no-install-recommends \ + build-essential \ + pkg-config \ + libx11-dev \ + libglib2.0-dev \ + libgstreamer1.0-dev \ + libgstreamer-plugins-base1.0-dev \ + libgstreamer-plugins-bad1.0-dev \ + gstreamer1.0-libav \ + gstreamer1.0-plugins-base \ + gstreamer1.0-plugins-good \ + gstreamer1.0-plugins-bad \ + gstreamer1.0-nice \ + gstreamer1.0-gl \ + gstreamer1.0-x + - name: Install dependencies run: bun install --frozen-lockfile @@ -137,6 +164,12 @@ jobs: id: test run: npm test + - name: Test and build Linux native streamer + if: runner.os == 'Linux' + run: | + cargo test --manifest-path ../native/opennow-streamer/Cargo.toml --features gstreamer + npm run native:build + - name: CI summary if: always() working-directory: . diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e10e95b5b..b74c4b1de 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -3,8 +3,16 @@ name: release on: workflow_dispatch: inputs: + release_type: + description: Publish a stable release or a test-only nightly from dev + required: true + default: stable + type: choice + options: + - stable + - nightly version: - description: Version to release, e.g. 1.2.3 or v1.2.3 + description: Target version (for a nightly, use the next stable version, e.g. 1.2.3) required: true type: string push: @@ -22,13 +30,15 @@ permissions: jobs: preflight: name: preflight - runs-on: blacksmith-2vcpu-ubuntu-2404 + runs-on: blacksmith-4vcpu-ubuntu-2404 outputs: version: ${{ steps.release.outputs.version }} prerelease: ${{ steps.release.outputs.prerelease }} make_latest: ${{ steps.release.outputs.make_latest }} release_tag: ${{ steps.release.outputs.release_tag }} source_ref: ${{ steps.release.outputs.source_ref }} + release_type: ${{ steps.release.outputs.release_type }} + release_name: ${{ steps.release.outputs.release_name }} steps: - name: Checkout @@ -55,8 +65,16 @@ jobs: env: INPUT_VERSION: ${{ inputs.version }} REF_NAME: ${{ github.ref_name }} + REF_TYPE: ${{ github.ref_type }} EVENT_NAME: ${{ github.event_name }} + RELEASE_TYPE: ${{ inputs.release_type }} + RUN_NUMBER: ${{ github.run_number }} + RUN_ATTEMPT: ${{ github.run_attempt }} + GH_TOKEN: ${{ github.token }} run: | + set -euo pipefail + + release_type="${RELEASE_TYPE:-stable}" raw="${INPUT_VERSION:-$REF_NAME}" case "$raw" in opennow-stable-v*) version="${raw#opennow-stable-v}" ;; @@ -69,6 +87,33 @@ jobs: exit 1 fi + release_name="OpenNOW v${version}" + if [[ "$release_type" == "nightly" ]]; then + if [[ "$EVENT_NAME" != "workflow_dispatch" || "$REF_TYPE" != "branch" || "$REF_NAME" != "dev" ]]; then + echo "Nightly releases must be manually dispatched from the dev branch." >&2 + exit 1 + fi + if [[ "$version" == *-* || "$version" == *+* ]]; then + echo "Nightly target version must be a stable semver such as 1.2.3; the nightly suffix is added automatically." >&2 + exit 1 + fi + latest_tag="$(gh api "repos/${GITHUB_REPOSITORY}/releases/latest" --jq .tag_name)" + latest_version="${latest_tag#opennow-stable-v}" + latest_version="${latest_version#v}" + if [[ "$latest_version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + highest_version="$(printf '%s\n%s\n' "$latest_version" "$version" | sort -V | tail -n 1)" + if [[ "$version" == "$latest_version" || "$highest_version" != "$version" ]]; then + echo "Nightly target $version must be newer than the latest stable release $latest_version." >&2 + exit 1 + fi + fi + version="${version}-nightly.${RUN_NUMBER}.${RUN_ATTEMPT}" + release_name="OpenNOW Nightly v${version}" + elif [[ "$release_type" != "stable" ]]; then + echo "Unsupported release type: $release_type" >&2 + exit 1 + fi + prerelease=false if [[ "$version" == *-* ]]; then prerelease=true @@ -93,11 +138,13 @@ jobs: echo "make_latest=$make_latest" echo "release_tag=$release_tag" echo "source_ref=$source_ref" + echo "release_type=$release_type" + echo "release_name=$release_name" } >> "$GITHUB_OUTPUT" prepare-source: name: prepare-source - runs-on: blacksmith-2vcpu-ubuntu-2404 + runs-on: blacksmith-4vcpu-ubuntu-2404 needs: preflight permissions: contents: write @@ -120,7 +167,14 @@ jobs: REF_TYPE: ${{ github.ref_type }} RELEASE_VERSION: ${{ needs.preflight.outputs.version }} SOURCE_REF: ${{ needs.preflight.outputs.source_ref }} + RELEASE_TYPE: ${{ needs.preflight.outputs.release_type }} run: | + if [[ "$RELEASE_TYPE" == "nightly" ]]; then + echo "Nightly version is applied only inside build jobs; dev remains unchanged." + echo "release_sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT" + exit 0 + fi + if [[ "$EVENT_NAME" != "workflow_dispatch" ]]; then node opennow-stable/scripts/sync-release-version.mjs --check "$RELEASE_VERSION" echo "release_sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT" @@ -155,7 +209,7 @@ jobs: matrix: include: - label: windows-x64 - os: blacksmith-2vcpu-windows-2025 + os: blacksmith-4vcpu-windows-2025 builder_args: "--win nsis portable --x64" native_build: "true" bundle_gstreamer: "1" @@ -166,7 +220,7 @@ jobs: opennow-stable/dist-release/latest*.yml opennow-stable/dist-release/*.blockmap - label: windows-arm64 - os: blacksmith-2vcpu-windows-2025 + os: blacksmith-4vcpu-windows-2025 builder_args: "--win nsis portable --arm64" native_build: "false" bundle_gstreamer: "0" @@ -199,7 +253,7 @@ jobs: opennow-stable/dist-release/*-arm64.zip opennow-stable/dist-release/latest-mac-arm64.yml - label: linux-x64 - os: blacksmith-2vcpu-ubuntu-2404 + os: blacksmith-4vcpu-ubuntu-2404 builder_args: "--linux AppImage deb --x64" native_build: "true" bundle_gstreamer: "0" @@ -210,7 +264,7 @@ jobs: opennow-stable/dist-release/*-amd64.deb opennow-stable/dist-release/latest-linux.yml - label: linux-arm64 - os: blacksmith-2vcpu-ubuntu-2404-arm + os: blacksmith-4vcpu-ubuntu-2404-arm builder_args: "--linux AppImage deb --arm64" native_build: "true" bundle_gstreamer: "0" @@ -232,7 +286,7 @@ jobs: npm_config_audit: "false" npm_config_fund: "false" GSTREAMER_VERSION: "1.28.2" - GSTREAMER_WINDOWS_VERSION: "1.26.8" + GSTREAMER_WINDOWS_VERSION: "1.28.3" CARGO_NET_RETRY: "10" CARGO_HTTP_MULTIPLEXING: "false" CARGO_REGISTRIES_CRATES_IO_PROTOCOL: sparse @@ -294,6 +348,7 @@ jobs: rpm \ build-essential \ pkg-config \ + libx11-dev \ libglib2.0-dev \ libgstreamer1.0-dev \ libgstreamer-plugins-base1.0-dev \ @@ -304,8 +359,10 @@ jobs: gstreamer1.0-plugins-good \ gstreamer1.0-plugins-bad \ gstreamer1.0-plugins-ugly \ + gstreamer1.0-nice \ gstreamer1.0-vaapi \ gstreamer1.0-gl \ + gstreamer1.0-x \ libva2 \ libva-drm2 \ libvulkan1 \ @@ -365,15 +422,14 @@ jobs: if: matrix.native_build == 'true' && runner.os == 'Windows' shell: pwsh run: | - $installBase = "C:\gstreamer" + $installRoot = "C:\gstreamer\1.0\msvc_x86_64" $roots = @( - "C:\gstreamer\1.0\msvc_x86_64", + $installRoot, "C:\Program Files\gstreamer\1.0\msvc_x86_64" ) $searchBases = @("C:\gstreamer", "C:\Program Files\gstreamer") $script:gstreamerPcPaths = @() - $script:lastRuntimeMsiExitCode = $null - $script:lastDevelMsiExitCode = $null + $script:lastInstallerExitCode = $null function Test-GStreamerRoot([string] $candidate) { return [bool]($candidate -and (Test-Path (Join-Path $candidate "lib\pkgconfig\gstreamer-1.0.pc")) -and @@ -395,7 +451,7 @@ jobs: } return $null } - function Get-GStreamerMsi([string] $fileName, [string] $downloadDir, [string[]] $urlBases) { + function Get-GStreamerInstaller([string] $fileName, [string] $downloadDir, [string[]] $urlBases) { New-Item -ItemType Directory -Force -Path $downloadDir | Out-Null $output = Join-Path $downloadDir $fileName $partial = "$output.part" @@ -431,33 +487,34 @@ jobs: } return $null } - function Install-GStreamerMsiSdk([string] $version) { + function Install-GStreamerSdk([string] $version) { $urlBases = @( "https://gstreamer.freedesktop.org/data/pkg/windows/$version/msvc", "https://gstreamer.freedesktop.org/pkg/windows/$version/msvc" ) $downloadDir = Join-Path $env:GITHUB_WORKSPACE ".cache\gstreamer-windows\$version" - $runtime = Get-GStreamerMsi "gstreamer-1.0-msvc-x86_64-$version.msi" $downloadDir $urlBases - if (-not $runtime) { return $false } - $devel = Get-GStreamerMsi "gstreamer-1.0-devel-msvc-x86_64-$version.msi" $downloadDir $urlBases - if (-not $devel) { return $false } - $runtimeInstall = Start-Process msiexec.exe -ArgumentList @("/i", $runtime, "/qn", "/norestart", "INSTALLDIR=$installBase", "ADDLOCAL=ALL") -Wait -PassThru - $script:lastRuntimeMsiExitCode = $runtimeInstall.ExitCode - if ($runtimeInstall.ExitCode -ne 0) { return $false } - $develInstall = Start-Process msiexec.exe -ArgumentList @("/i", $devel, "/qn", "/norestart", "INSTALLDIR=$installBase", "ADDLOCAL=ALL") -Wait -PassThru - $script:lastDevelMsiExitCode = $develInstall.ExitCode - if ($develInstall.ExitCode -ne 0) { return $false } + $installer = Get-GStreamerInstaller "gstreamer-1.0-msvc-x86_64-$version.exe" $downloadDir $urlBases + if (-not $installer) { return $false } + $install = Start-Process -FilePath $installer -ArgumentList @( + "/VERYSILENT", + "/SUPPRESSMSGBOXES", + "/NORESTART", + "/TYPE=devel", + "/DIR=$installRoot" + ) -Wait -PassThru -NoNewWindow + $script:lastInstallerExitCode = $install.ExitCode + if ($install.ExitCode -ne 0) { return $false } return [bool](Find-GStreamerRoot) } $root = Find-GStreamerRoot if ($root) { Write-Host "Using cached GStreamer Windows SDK: $root" - } elseif (-not (Install-GStreamerMsiSdk $env:GSTREAMER_WINDOWS_VERSION)) { + } elseif (-not (Install-GStreamerSdk $env:GSTREAMER_WINDOWS_VERSION)) { [void](Find-GStreamerRoot) Write-Host "GStreamer explicit roots searched: $($roots -join ', ')" Write-Host "GStreamer recursive search bases: $($searchBases -join ', ')" Write-Host "Discovered gstreamer-1.0.pc paths: $(if ($script:gstreamerPcPaths.Count) { $script:gstreamerPcPaths -join ', ' } else { 'none' })" - Write-Error "Failed to install/discover GStreamer Windows MSI SDK version $env:GSTREAMER_WINDOWS_VERSION. Runtime MSI exit code: $script:lastRuntimeMsiExitCode; devel MSI exit code: $script:lastDevelMsiExitCode." + Write-Error "Failed to install/discover GStreamer Windows SDK version $env:GSTREAMER_WINDOWS_VERSION. Installer exit code: $script:lastInstallerExitCode." exit 1 } $root = Find-GStreamerRoot @@ -488,9 +545,17 @@ jobs: Write-Error "GStreamer Windows SDK installed, but no H.264 decoder plugins were found among: $($h264Decoders -join ', ')" exit 1 } + # Official Cerbero Windows packages disable gstvulkan. OpenNOW injects a + # vendored Vulkan plugin into the private runtime after bundling. + Write-Host "Skipping base-SDK Vulkan element checks; Vulkan is injected into the private OpenNOW runtime bundle." "GSTREAMER_1_0_ROOT_MSVC_X86_64=$root" | Out-File -FilePath $env:GITHUB_ENV -Append "$root\bin" | Out-File -FilePath $env:GITHUB_PATH -Append + - name: Build patched Windows GStreamer D3D11 plugin + if: matrix.native_build == 'true' && runner.os == 'Windows' + shell: pwsh + run: ./scripts/build-patched-gstreamer-d3d11.ps1 -GStreamerRoot "$env:GSTREAMER_1_0_ROOT_MSVC_X86_64" -Version "$env:GSTREAMER_WINDOWS_VERSION" + - name: Install FPM for arm64 deb builds if: matrix.label == 'linux-arm64' run: sudo apt-get install -y ruby ruby-dev build-essential && sudo gem install fpm @@ -557,7 +622,7 @@ jobs: release: name: publish-release - runs-on: blacksmith-2vcpu-ubuntu-2404 + runs-on: blacksmith-4vcpu-ubuntu-2404 needs: - preflight - prepare-source @@ -621,20 +686,38 @@ jobs: mv /tmp/merged.yml "$x64_feed" rm "$arm64_feed" - - name: Publish GitHub release + - name: Stage GitHub release draft uses: softprops/action-gh-release@v2 with: tag_name: ${{ needs.preflight.outputs.release_tag }} target_commitish: ${{ needs.prepare-source.outputs.release_sha }} - name: OpenNOW v${{ needs.preflight.outputs.version }} + name: ${{ needs.preflight.outputs.release_name }} + draft: true prerelease: ${{ needs.preflight.outputs.prerelease }} make_latest: ${{ needs.preflight.outputs.make_latest }} generate_release_notes: true files: release-artifacts/**/* + - name: Publish completed GitHub release + shell: bash + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + RELEASE_REPOSITORY: ${{ github.repository }} + RELEASE_TAG: ${{ needs.preflight.outputs.release_tag }} + PRERELEASE: ${{ needs.preflight.outputs.prerelease }} + MAKE_LATEST: ${{ needs.preflight.outputs.make_latest }} + run: | + set -euo pipefail + args=(release edit "$RELEASE_TAG" --repo "$RELEASE_REPOSITORY" "--draft=false" "--prerelease=$PRERELEASE") + if [[ "$MAKE_LATEST" == "true" ]]; then + args+=(--latest) + fi + gh "${args[@]}" + finalize: name: finalize-version-bump - runs-on: blacksmith-2vcpu-ubuntu-2404 + if: needs.preflight.outputs.release_type != 'nightly' + runs-on: blacksmith-4vcpu-ubuntu-2404 needs: - preflight - release diff --git a/.gitignore b/.gitignore index 44a744cf0..6386cb89c 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,9 @@ dist/ target/ target2/ target3/ +/.cache/ +/video/ +/docs/research/ # Native streamer build outputs are generated by `npm run native:build`. native/opennow-streamer/bin/opennow-streamer.exe native/opennow-streamer/bin/*/* @@ -56,4 +59,6 @@ result .deriveddata/ .deriveddata-device/ .opencode -vendor/ \ No newline at end of file +vendor/ +!native/opennow-streamer/vendor/ +!native/opennow-streamer/vendor/** diff --git a/AGENTS.md b/AGENTS.md index faec0e959..4c9d49f26 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -15,10 +15,22 @@ If a tradeoff is required, choose correctness and robustness over short-term con - `locales/` contains localization sources and generated Crowdin output. See Localization before editing. - `OpenNOW-Site/` is a separate repository when present; do not modify it from OpenNOW tasks unless explicitly requested. +## Platform Layout (multi-provider ready) + +Cloud streaming providers live under a `platforms//` folder in each process boundary. GeForce NOW (`gfn`) is the first provider: + +- Main: `opennow-stable/src/main/platforms/gfn/` +- Renderer stream protocol: `opennow-stable/src/renderer/src/platforms/gfn/` +- Shared contracts: `opennow-stable/src/shared/gfn/` (barrel `@shared/gfn`) +- Platform registry/capabilities: `opennow-stable/src/shared/platforms/` and `opennow-stable/src/main/platforms/` + +When adding another provider, create `platforms//` mirrors and register it in `shared/platforms` — do not sprinkle provider protocol details into app shell, IPC wiring, or unrelated UI modules. + ## Module Boundaries -- Shared GFN main-process protocol details belong under `opennow-stable/src/main/gfn`. Prefer focused modules with one owner per concern (`clientHeaders.ts` for client identity/header constants, `proxyFetch.ts` for proxy-aware fetches, `proxyUrl.ts` for proxy URL normalization, `request.ts` for common response handling). -- Do not duplicate NVIDIA/GFN constants, request headers, platform/device ID mapping, auth header construction, proxy behavior, or error parsing across feature files. Add to or extract a focused shared module first, then consume it from features. +- Shared GFN main-process protocol details belong under `opennow-stable/src/main/platforms/gfn`. Prefer focused modules with one owner per concern (`clientHeaders.ts` for client identity/header constants, `proxyFetch.ts` for proxy-aware fetches, `proxyUrl.ts` for proxy URL normalization, `request.ts` for common response handling, `endpoints` helpers via `@shared/gfn/endpoints` for public URL construction). +- Shared GFN DTOs/helpers are split by concern under `opennow-stable/src/shared/gfn/` (`auth`, `catalog`, `session`, `stream`, `api`, …). Import from `@shared/gfn` for stability, or from a focused submodule when that keeps ownership clearer. +- Do not duplicate NVIDIA/GFN constants, request headers, platform/device ID mapping, auth header construction, proxy behavior, zone URL construction, or error parsing across feature files. Add to or extract a focused shared module first, then consume it from features. - Keep feature files (`auth.ts`, `games.ts`, `cloudmatch.ts`, `subscription.ts`, etc.) responsible for product flow and payload shape, not for re-declaring shared client identity or transport details. - Preserve provider/alliance behavior, stable device IDs, session refresh semantics, and `SessionError.fromResponse()` handling when refactoring GFN code. diff --git a/_tmp-test-out.txt b/_tmp-test-out.txt new file mode 100644 index 000000000..d69ed0970 Binary files /dev/null and b/_tmp-test-out.txt differ diff --git a/_tmp-typecheck-out.txt b/_tmp-typecheck-out.txt new file mode 100644 index 000000000..54d7e1fe6 Binary files /dev/null and b/_tmp-typecheck-out.txt differ diff --git a/locales/en.json b/locales/en.json index d435cee74..e59d1fd7d 100644 --- a/locales/en.json +++ b/locales/en.json @@ -1,4 +1,7 @@ { + "common": { + "loading": "Loading..." + }, "app": { "name": "OpenNOW", "tagline": "Open-source cloud gaming client", @@ -261,11 +264,13 @@ "labels": { "launchError": "Launch Error", "nowLoading": "Now Loading", - "adQueue": "Ad Queue" + "adQueue": "Ad Queue", + "launchProgress": "Game launch progress" }, "steps": { "queue": "Queue", "setup": "Setup", + "connect": "Connect", "ready": "Ready" }, "status": { @@ -279,6 +284,28 @@ "settingUpStream": "Setting up stream...", "loading": "Loading..." }, + "detail": { + "queueMoving": "The live queue is moving. Your place is secured at #{{position}}.", + "findingCapacity": "Finding the best available cloud rig for this session.", + "rigReserved": "A gaming rig is reserved. Applying your session configuration now.", + "servicesStarting": "Starting game services and preparing the remote display.", + "mediaHandshake": "Negotiating the low-latency video and input channels." + }, + "cozy": { + "queue": "Settle in. We'll keep your place and bring the game in when it's ready.", + "setup": "Your cloud rig is reserved and getting comfortable for you.", + "starting": "The game is starting on your cloud rig.", + "connecting": "The game is ready. Waiting for the first clear frame.", + "next": "Up next" + }, + "telemetry": { + "queuePosition": "Queue position", + "calculating": "Calculating", + "cleared": "Cleared", + "elapsed": "Elapsed", + "session": "Session", + "protected": "Secured" + }, "ads": { "resumeToStayInQueue": "Resume ads to stay in queue.", "availableForProgression": "{{count}} ad available for queue progression.", @@ -564,6 +591,8 @@ }, "experimentalL4SRequest": "Experimental L4S Request", "experimentalL4SRequestHint": "Request the GeForce NOW L4S streaming feature on newly created sessions. This does not change browser WebRTC behavior by itself and may be ignored by the service or network path.", + "identifyAsSteamDeck": "Identify as Steam Deck", + "identifyAsSteamDeckHint": "Identify as the Steam Deck GFN client to unlock Deck-specific resolutions and 90 FPS. Video options refresh automatically.", "launchInConsoleMode": "Console Mode (TV / Big Picture)", "launchInConsoleModeHint": "Start OpenNOW fullscreen with Controller Mode enabled, like GeForce NOW's TV mode. Sessions ask NVIDIA to launch games gamepad-friendly (big picture) while Console Mode or Controller Mode is active." }, @@ -692,7 +721,7 @@ "controllerMode": "Controller Mode", "controllerModeHint": "Use a large-screen console-style shell with controller button hints and a horizontal library layout.", "escapeExitsFullscreen": "Escape Exits Fullscreen", - "escapeExitsFullscreenHint": "When enabled, pressing Escape will exit fullscreen. When disabled (default), Escape is forwarded to the game while the mouse is pointer-locked.", + "escapeExitsFullscreenHint": "When enabled, pressing Escape exits fullscreen immediately. When disabled (default), Escape is forwarded to the game while pointer-locked; hold Escape ~1.5s to exit fullscreen.", "discordRichPresence": "Discord Rich Presence", "discordRichPresenceHint": "Show the game you are streaming as your Discord activity, including elapsed time.", "posterSize": "Poster Size", @@ -721,6 +750,11 @@ "checkForUpdates": "Check for Updates", "downloadUpdate": "Download Update", "restartToInstall": "Restart to Install", + "updateChannel": "Update Channel", + "updateChannelHint": "Choose Stable for regular releases or Nightly to preview the latest builds from the dev branch.", + "updateChannelStable": "Stable (Default)", + "updateChannelNightly": "Nightly", + "updateChannelNightlyHint": "Nightly builds may be less reliable. Switching channels immediately checks for an available update.", "automaticallyCheckForUpdates": "Automatically Check for Updates", "automaticallyCheckForUpdatesOnHint": "When on, packaged builds check GitHub Releases in the background after startup and periodically while OpenNOW is running.", "automaticallyCheckForUpdatesOffHint": "When off, OpenNOW stays on the current version unless you use the manual update buttons below.", @@ -734,6 +768,10 @@ "deleteCacheFailed": "Failed to delete cache. Please try again.", "whatsNew": "What's new", "whatsNewHint": "View the release notes for the current version of OpenNOW.", + "errorReporting": "Send anonymous diagnostics", + "errorReportingHint": "Sends anonymous crash reports and app opens (install ID, version, OS). Never includes your NVIDIA account, games library, or gameplay.", + "sendFeedback": "Send feedback", + "sendFeedbackHint": "Share a bug report or idea with the OpenNOW team.", "updateStatus": { "packagedBuildsOnly": "Packaged builds only", "checking": "Checking", @@ -748,10 +786,10 @@ "nativeStreamingHint": "Native streaming is experimental and may have platform-specific bugs, glitches, or fallbacks to Chromium/WebRTC. Enable it only if you are comfortable testing the GStreamer-based desktop streamer for new sessions.", "enablePromptKicker": "Experimental", "enablePromptTitle": "Enable Native Streamer?", - "enablePromptBody": "The native streamer is still experimental. OpenNOW shortcuts do not work while the native streamer window has focus.", + "enablePromptBody": "The native streamer is still experimental. Internal mode keeps video inside the OpenNOW window; external floating mode is available as a fallback in settings.", "enablePromptNewSessions": "Native streaming applies to new sessions only.", - "enablePromptShortcuts": "Overlay, exit, fullscreen, stats, and other shortcuts will not trigger inside the native streamer window.", - "enablePromptAltTab": "Alt+Tab back into the OpenNOW window to exit the stream or use OpenNOW shortcuts.", + "enablePromptShortcuts": "In internal mode, OpenNOW shortcuts stay available. External floating mode captures input in its own window.", + "enablePromptAltTab": "If you use external floating mode, Alt+Tab back into OpenNOW to exit the stream or use app shortcuts.", "enablePromptCancel": "Keep Disabled", "enablePromptEnable": "Enable Native Streamer", "enablePromptEsc": "cancel", @@ -769,15 +807,41 @@ "runtimeDefaultHint": "Packaged Windows/macOS builds auto-detect a bundled runtime next to the native streamer. Linux uses distro packages.", "linuxRuntimeHint": "Linux AppImage/private GStreamer bundling is intentionally not used by default because VAAPI/V4L2/Vulkan plugins must match the host distro and GPU driver stack. .deb packages declare Debian/Ubuntu dependencies automatically.", "videoPath": "Video Path", + "thisPc": "This PC", + "supportedVideoBackends": "Backend Compatibility", + "hardwarePathsReady": "{{count}} hardware path ready", + "hardwarePathsReady_plural": "{{count}} hardware paths ready", + "supported": "Supported", + "unavailable": "Unavailable", + "autoPick": "Auto pick", + "selected": "Selected", + "codecSupport": "Codec support", + "noRenderer": "No renderer", + "systemMemory": "System memory", + "noCodecsAvailable": "No compatible codecs were detected for this backend.", + "probingBackends": "Probing the installed GPU drivers and GStreamer plugins…", + "activePath": "Active path", + "capabilityProbeHint": "Refresh the streamer status to probe the video backends supported by this PC.", + "backendUnavailableOnPc": "This backend is not supported by the installed GPU drivers and GStreamer plugins.", "codecSupportUnknown": "Codec support unknown", "memoryPathUnknown": "Memory path unknown", "videoPathDefaultHint": "OpenNOW will show the active hardware decode path after GStreamer is detected.", - "directxBackend": "DirectX Backend", - "directxBackendHint": "Applies to the next native streamer process. Auto uses the fastest default path; choose DirectX 11 or DirectX 12 to force a specific Windows renderer.", + "directxBackend": "Video Backend", + "directxBackendHint": "Applies to the next native streamer process. Unsupported choices are disabled from the live capability probe. Auto selects the best compatible path.", "framePacing": "Frame Pacing", "lowestLatency": "Lowest Latency", "smoothGsync": "Smooth G-Sync", - "framePacingHint": "Lowest Latency avoids G-Sync pacing and is best for mouse feel. Smooth G-Sync can reduce tearing, but may cap rendering to the monitor refresh rate." + "framePacingHint": "Lowest Latency avoids G-Sync pacing and is best for mouse feel. Smooth G-Sync can reduce tearing, but may cap rendering to the monitor refresh rate.", + "renderMode": "Render Mode", + "renderModeInternal": "Internal (One Window)", + "renderModeExternal": "External (Floating)", + "renderModeHint": "Internal (default) embeds native video in the OpenNOW window. External keeps the floating GStreamer window for testing/fallback. Change applies to the next native streamer process.", + "renderModeInternalOnlyHint": "This platform uses Internal (one window) only. Keyboard, mouse, and controller input stay in Electron and are forwarded over IPC. External floating mode is Windows-only.", + "transportMode": "Transport Mode", + "transportModeWebrtc": "WebRTC (Default)", + "transportModeNvst": "NVST (Experimental)", + "transportModeHint": "WebRTC is the supported path. NVST (experimental) uses classic RTSPS + UDP video (Moonlight-hypothesis scaffold) while keyboard/mouse/controller stay on WebRTC SCTP datachannels. First live sessions may need SRTP/pcap validation.", + "transportModeWindowsOnlyHint": "NVST transport is Windows-only for now. This platform always uses WebRTC." }, "thanks": { "title": "Thanks for helping OpenNOW grow", @@ -799,6 +863,7 @@ } }, "navbar": { + "sendFeedback": "Feedback", "subscriptionDetails": "Subscription details", "playtimeDetails": "Playtime Details", "storageDetails": "Storage Details", @@ -879,5 +944,43 @@ "viewOnGitHub": "View full release on GitHub", "unavailable": "Release notes couldn't be loaded right now.", "settingsLink": "What's new" + }, + "telemetryConsent": { + "kicker": "Privacy", + "title": "Help improve OpenNOW?", + "body": "OpenNOW can send anonymous diagnostics so we can find crashes and understand how many installs are active.", + "sendsErrors": "Sends crash details, app version, and OS information", + "sendsAppOpens": "Sends an anonymous signal when the app opens (for install counts)", + "sendsAnonymousId": "Uses a random install ID — not linked to your name or email", + "neverSendsAccount": "Never sends your NVIDIA account, tokens, or library", + "neverSendsGameplay": "Never records gameplay, streams, or session content", + "changeLater": "You can change this anytime in Settings → About.", + "accept": "Send anonymous diagnostics", + "decline": "Not now" + }, + "feedback": { + "kicker": "Feedback", + "title": "Send feedback", + "category": "Category", + "categories": { + "bug": "Bug report", + "idea": "Idea / feature request", + "other": "Other" + }, + "message": "Message", + "messagePlaceholder": "Tell us what happened or what you'd like to see…", + "includeSystemInfo": "Include app version and system info", + "includeLogs": "Include recent app logs", + "includeLogsHint": "Attaches redacted main + renderer console logs (no passwords, tokens, or emails). Helpful for bug reports.", + "send": "Send feedback", + "sending": "Sending…", + "cancel": "Cancel", + "close": "Close", + "success": "Thanks — your feedback was sent.", + "errors": { + "empty": "Please enter a message before sending.", + "notConfigured": "Feedback is unavailable because PostHog is not configured in this build.", + "sendFailed": "Couldn't send feedback. Please try again." + } } } diff --git a/native/gstreamer-patches/0001-d3d11-enable-tearing-vrr.patch b/native/gstreamer-patches/0001-d3d11-enable-tearing-vrr.patch new file mode 100644 index 000000000..1bfce4f65 --- /dev/null +++ b/native/gstreamer-patches/0001-d3d11-enable-tearing-vrr.patch @@ -0,0 +1,55 @@ +--- a/subprojects/gst-plugins-bad/sys/d3d11/gstd3d11window_win32.cpp ++++ b/subprojects/gst-plugins-bad/sys/d3d11/gstd3d11window_win32.cpp +@@ -1155,9 +1155,36 @@ + DXGI_SWAP_CHAIN_DESC desc = { 0, }; + IDXGISwapChain *new_swapchain = NULL; + GstD3D11Device *device = window->device; ++ HRESULT hr; + + self->have_swapchain1 = FALSE; + ++ const gchar *allow_tearing_env = g_getenv ("OPENNOW_NATIVE_D3D_ALLOW_TEARING"); ++ if (allow_tearing_env && ++ (!g_ascii_strcasecmp (allow_tearing_env, "1") || ++ !g_ascii_strcasecmp (allow_tearing_env, "true") || ++ !g_ascii_strcasecmp (allow_tearing_env, "on") || ++ !g_ascii_strcasecmp (allow_tearing_env, "yes"))) { ++ IDXGIFactory1 *factory = ++ gst_d3d11_device_get_dxgi_factory_handle (device); ++ ComPtr < IDXGIFactory5 > factory5; ++ BOOL allow_tearing = FALSE; ++ ++ hr = factory->QueryInterface (IID_PPV_ARGS (&factory5)); ++ if (SUCCEEDED (hr)) { ++ hr = factory5->CheckFeatureSupport (DXGI_FEATURE_PRESENT_ALLOW_TEARING, ++ &allow_tearing, sizeof (allow_tearing)); ++ } ++ ++ if (SUCCEEDED (hr) && allow_tearing) { ++ swapchain_flags |= DXGI_SWAP_CHAIN_FLAG_ALLOW_TEARING; ++ GST_INFO_OBJECT (self, "DXGI tearing/VRR presentation enabled"); ++ } else { ++ GST_WARNING_OBJECT (self, ++ "DXGI tearing/VRR presentation requested but unsupported"); ++ } ++ } ++ + { + DXGI_SWAP_CHAIN_DESC1 desc1 = { 0, }; + desc1.Width = 0; +@@ -1309,6 +1336,15 @@ + return GST_D3D11_WINDOW_FLOW_CLOSED; + } + ++ DXGI_SWAP_CHAIN_DESC swapchain_desc = { 0, }; ++ BOOL exclusive_fullscreen = FALSE; ++ if (SUCCEEDED (window->swap_chain->GetDesc (&swapchain_desc)) && ++ (swapchain_desc.Flags & DXGI_SWAP_CHAIN_FLAG_ALLOW_TEARING) && ++ (FAILED (window->swap_chain->GetFullscreenState ( ++ &exclusive_fullscreen, nullptr)) || !exclusive_fullscreen)) { ++ present_flags |= DXGI_PRESENT_ALLOW_TEARING; ++ } ++ + if (self->have_swapchain1) { + IDXGISwapChain1 *swap_chain1 = (IDXGISwapChain1 *) window->swap_chain; + DXGI_PRESENT_PARAMETERS present_params = { 0, }; diff --git a/native/opennow-streamer/Cargo.lock b/native/opennow-streamer/Cargo.lock index b5aac4314..83236c1bd 100644 --- a/native/opennow-streamer/Cargo.lock +++ b/native/opennow-streamer/Cargo.lock @@ -11,6 +11,21 @@ dependencies = [ "memchr", ] +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anyhow" +version = "1.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" + [[package]] name = "atomic_refcell" version = "0.1.14" @@ -35,6 +50,43 @@ version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" +[[package]] +name = "bitstream-io" +version = "4.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7eff00be299a18769011411c9def0d827e8f2d7bf0c3dbf53633147a8867fd1f" +dependencies = [ + "no_std_io2", +] + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "byte-slice-cast" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7575182f7272186991736b70173b0ea045398f984bf5ebbb3804736ce1330c9d" + +[[package]] +name = "bytes" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" + +[[package]] +name = "cc" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5add81bb678e6cb321aff7fa0dc7689ad82b112dbc032cea19f91d6b8e3582b9" +dependencies = [ + "find-msvc-tools", + "shlex", +] + [[package]] name = "cfg-expr" version = "0.20.7" @@ -51,6 +103,49 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +[[package]] +name = "chacha20" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81" +dependencies = [ + "cfg-if", + "cpufeatures", + "rand_core", +] + +[[package]] +name = "chrono" +version = "0.4.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" +dependencies = [ + "iana-time-zone", + "num-traits", + "windows-link", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" + [[package]] name = "either" version = "1.15.0" @@ -63,6 +158,27 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "futures" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + [[package]] name = "futures-channel" version = "0.3.32" @@ -70,6 +186,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" dependencies = [ "futures-core", + "futures-sink", ] [[package]] @@ -89,6 +206,12 @@ dependencies = [ "futures-util", ] +[[package]] +name = "futures-io" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4577ecaa3c4f96589d473f679a71b596316f6641bc350038b962a5daf0085d7a" + [[package]] name = "futures-macro" version = "0.3.32" @@ -100,6 +223,12 @@ dependencies = [ "syn", ] +[[package]] +name = "futures-sink" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e34418ac499d6305c2fb5ad0ed2f6ac998c5f8ca209b4510f7f94242c647e307" + [[package]] name = "futures-task" version = "0.3.32" @@ -112,13 +241,46 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" dependencies = [ + "futures-channel", "futures-core", + "futures-io", "futures-macro", + "futures-sink", "futures-task", + "memchr", "pin-project-lite", "slab", ] +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "rand_core", +] + +[[package]] +name = "gio" +version = "0.22.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b3e1f669909c326b9413bde5a742097b8c90a7d78f45326db13668984769ded" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "gio-sys", + "glib", + "libc", + "pin-project-lite", + "smallvec", +] + [[package]] name = "gio-sys" version = "0.22.0" @@ -186,11 +348,55 @@ dependencies = [ "system-deps", ] +[[package]] +name = "gst-plugin-rtp" +version = "0.15.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94a8636a5d8ab4d590e66c4cd103beac097d4c1f053f5723e47f141f5af67d0e" +dependencies = [ + "anyhow", + "atomic_refcell", + "bitstream-io", + "byte-slice-cast", + "chrono", + "futures", + "gio", + "glib", + "gst-plugin-version-helper", + "gstreamer", + "gstreamer-audio", + "gstreamer-base", + "gstreamer-net", + "gstreamer-rtp", + "gstreamer-video", + "hex", + "log", + "rand", + "rtcp-types", + "rtp-types", + "slab", + "smallvec", + "thiserror 2.0.18", + "time", + "tokio", + "tokio-util", +] + +[[package]] +name = "gst-plugin-version-helper" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94668bc2592732b8c2b653668ae41211d45988fb61264888b9c2d545d4bd826d" +dependencies = [ + "chrono", + "toml_edit", +] + [[package]] name = "gstreamer" -version = "0.25.2" +version = "0.25.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28ca0c594cac4e86f5444aaa767c7bb810340c0710667a6467d3ead248e35e84" +checksum = "ab4527e1b9bae8d29ce137bde5b8eec8ae8f78f13ad00fc6e70cbe227d6ad027" dependencies = [ "cfg-if", "futures-channel", @@ -208,7 +414,36 @@ dependencies = [ "pastey", "pin-project-lite", "smallvec", - "thiserror", + "thiserror 2.0.18", +] + +[[package]] +name = "gstreamer-audio" +version = "0.25.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f97532c3e21287a6e94563a328cce89367324acf8a4489291b40ede2e39163a0" +dependencies = [ + "cfg-if", + "glib", + "gstreamer", + "gstreamer-audio-sys", + "gstreamer-base", + "libc", + "smallvec", +] + +[[package]] +name = "gstreamer-audio-sys" +version = "0.25.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2abfb81a073c5c8fb115a3639f47b6430f669ee9fa29123bb0116c9ef59d7d16" +dependencies = [ + "glib-sys", + "gobject-sys", + "gstreamer-base-sys", + "gstreamer-sys", + "libc", + "system-deps", ] [[package]] @@ -238,6 +473,56 @@ dependencies = [ "system-deps", ] +[[package]] +name = "gstreamer-net" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abcad04d471a4f2c859ef1287f22581b07064e357cc89dfa415ffc99b2a3193d" +dependencies = [ + "gio", + "glib", + "gstreamer", + "gstreamer-net-sys", +] + +[[package]] +name = "gstreamer-net-sys" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bcefb342a98ffca0b106bc9fea13d5df15879e1613b4dc652a6ec1960c32584c" +dependencies = [ + "gio-sys", + "glib-sys", + "gstreamer-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gstreamer-rtp" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ea01e76ad8fd2e688a4f8a166060c9f9ba13dd6355ad7e22dbb793325d14411" +dependencies = [ + "glib", + "gstreamer", + "gstreamer-rtp-sys", + "libc", +] + +[[package]] +name = "gstreamer-rtp-sys" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adf8df32469d863ce4375a978233a40efec206c5256bf0c14ee42bb745d8a793" +dependencies = [ + "glib-sys", + "gstreamer-base-sys", + "gstreamer-sys", + "libc", + "system-deps", +] + [[package]] name = "gstreamer-sdp" version = "0.25.2" @@ -287,7 +572,7 @@ dependencies = [ "gstreamer-base", "gstreamer-video-sys", "libc", - "thiserror", + "thiserror 2.0.18", ] [[package]] @@ -342,6 +627,36 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + [[package]] name = "indexmap" version = "2.14.0" @@ -354,9 +669,9 @@ dependencies = [ [[package]] name = "itertools" -version = "0.14.0" +version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +checksum = "8b4baf93f58d4425749ca49a51c50ebab072c5df6994d08fed93541c331481dc" dependencies = [ "either", ] @@ -367,6 +682,17 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" +[[package]] +name = "js-sys" +version = "0.3.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + [[package]] name = "kstring" version = "2.0.2" @@ -382,6 +708,12 @@ version = "0.2.186" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" +[[package]] +name = "log" +version = "0.4.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" + [[package]] name = "memchr" version = "2.8.0" @@ -394,6 +726,21 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "956787520e75e9bd233246045d19f42fb73242759cc57fba9611d940ae96d4b0" +[[package]] +name = "no_std_io2" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "418abd1b6d34fbf6cae440dc874771b0525a604428704c76e48b29a5e67b8003" +dependencies = [ + "memchr", +] + +[[package]] +name = "num-conv" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" + [[package]] name = "num-integer" version = "0.1.46" @@ -422,11 +769,18 @@ dependencies = [ "autocfg", ] +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + [[package]] name = "opennow-streamer" version = "0.1.0" dependencies = [ "base64", + "gst-plugin-rtp", "gstreamer", "gstreamer-sdp", "gstreamer-video", @@ -463,6 +817,12 @@ version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + [[package]] name = "proc-macro2" version = "1.0.106" @@ -481,6 +841,29 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rand" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80" +dependencies = [ + "chacha20", + "getrandom", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" + [[package]] name = "regex" version = "1.12.3" @@ -510,6 +893,31 @@ version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" +[[package]] +name = "rtcp-types" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c081c846edea632bb47332fada9d4ac2fdf54d84beaf547fc947b58489e5f619" +dependencies = [ + "thiserror 2.0.18", +] + +[[package]] +name = "rtp-types" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7bb90df8268abfe08452ef2dae9e867a54edfdaa71b3127ef47d8b031f77ac73" +dependencies = [ + "smallvec", + "thiserror 1.0.69", +] + +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + [[package]] name = "serde" version = "1.0.228" @@ -562,6 +970,12 @@ dependencies = [ "serde_core", ] +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + [[package]] name = "slab" version = "0.4.12" @@ -610,13 +1024,33 @@ version = "0.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df7f62577c25e07834649fc3b39fafdc597c0a3527dc1c60129201ccfcbaa50c" +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + [[package]] name = "thiserror" version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" dependencies = [ - "thiserror-impl", + "thiserror-impl 2.0.18", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", ] [[package]] @@ -630,6 +1064,47 @@ dependencies = [ "syn", ] +[[package]] +name = "time" +version = "0.3.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e1d5e639ff6bab73cb6885cc7e7b1de96c3f32c68ec55f3952614bec1092244" +dependencies = [ + "deranged", + "num-conv", + "powerfmt", + "serde_core", + "time-core", +] + +[[package]] +name = "time-core" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" + +[[package]] +name = "tokio" +version = "1.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed" +dependencies = [ + "pin-project-lite", +] + +[[package]] +name = "tokio-util" +version = "0.7.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "494815d09bf52b5548659851081238f0ca39ff638363907596da739561c62c52" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + [[package]] name = "toml" version = "1.1.2+spec-1.1.0" @@ -654,6 +1129,18 @@ dependencies = [ "serde_core", ] +[[package]] +name = "toml_edit" +version = "0.25.13+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6975367e4d2ef766d86af01ffad14b622fecc8d4357a998fbc4deb6e9bacaf9b" +dependencies = [ + "indexmap", + "toml_datetime", + "toml_parser", + "winnow", +] + [[package]] name = "toml_parser" version = "1.1.2+spec-1.1.0" @@ -681,12 +1168,110 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "03c2856837ef78f57382f06b2b8563a2f512f7185d732608fd9176cb3b8edf0e" +[[package]] +name = "wasm-bindgen" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "windows-link" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + [[package]] name = "windows-sys" version = "0.61.2" @@ -701,6 +1286,9 @@ name = "winnow" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2ee1708bef14716a11bae175f579062d4554d95be2c6829f518df847b7b3fdd0" +dependencies = [ + "memchr", +] [[package]] name = "zmij" diff --git a/native/opennow-streamer/Cargo.toml b/native/opennow-streamer/Cargo.toml index dd1977260..229d7e2d8 100644 --- a/native/opennow-streamer/Cargo.toml +++ b/native/opennow-streamer/Cargo.toml @@ -11,6 +11,7 @@ gstreamer = [ "dep:gstreamer-sdp", "dep:gstreamer-video", "dep:gstreamer-webrtc", + "dep:gst-plugin-rtp", "gstreamer-webrtc/v1_22", ] @@ -23,3 +24,6 @@ gstreamer-webrtc = { version = "0.25.0", optional = true } regex = "1" serde = { version = "1", features = ["derive"] } serde_json = "1" + +[target.'cfg(target_os = "linux")'.dependencies] +gst-plugin-rtp = { version = "0.15.3", optional = true } diff --git a/native/opennow-streamer/bin/concrt140.dll b/native/opennow-streamer/bin/concrt140.dll new file mode 100644 index 000000000..830dfaeaf Binary files /dev/null and b/native/opennow-streamer/bin/concrt140.dll differ diff --git a/native/opennow-streamer/bin/gio-2.0-0.dll b/native/opennow-streamer/bin/gio-2.0-0.dll new file mode 100644 index 000000000..56235522e Binary files /dev/null and b/native/opennow-streamer/bin/gio-2.0-0.dll differ diff --git a/native/opennow-streamer/bin/glib-2.0-0.dll b/native/opennow-streamer/bin/glib-2.0-0.dll new file mode 100644 index 000000000..a9e79cd53 Binary files /dev/null and b/native/opennow-streamer/bin/glib-2.0-0.dll differ diff --git a/native/opennow-streamer/bin/gmodule-2.0-0.dll b/native/opennow-streamer/bin/gmodule-2.0-0.dll new file mode 100644 index 000000000..f9712b7cd Binary files /dev/null and b/native/opennow-streamer/bin/gmodule-2.0-0.dll differ diff --git a/native/opennow-streamer/bin/gobject-2.0-0.dll b/native/opennow-streamer/bin/gobject-2.0-0.dll new file mode 100644 index 000000000..33f81a469 Binary files /dev/null and b/native/opennow-streamer/bin/gobject-2.0-0.dll differ diff --git a/native/opennow-streamer/bin/gstreamer-1.0-0.dll b/native/opennow-streamer/bin/gstreamer-1.0-0.dll new file mode 100644 index 000000000..66afb1858 Binary files /dev/null and b/native/opennow-streamer/bin/gstreamer-1.0-0.dll differ diff --git a/native/opennow-streamer/bin/gthread-2.0-0.dll b/native/opennow-streamer/bin/gthread-2.0-0.dll new file mode 100644 index 000000000..e13a20a88 Binary files /dev/null and b/native/opennow-streamer/bin/gthread-2.0-0.dll differ diff --git a/native/opennow-streamer/bin/intl-8.dll b/native/opennow-streamer/bin/intl-8.dll new file mode 100644 index 000000000..d37f3a60f Binary files /dev/null and b/native/opennow-streamer/bin/intl-8.dll differ diff --git a/native/opennow-streamer/bin/msvcp140.dll b/native/opennow-streamer/bin/msvcp140.dll new file mode 100644 index 000000000..5153fb087 Binary files /dev/null and b/native/opennow-streamer/bin/msvcp140.dll differ diff --git a/native/opennow-streamer/bin/msvcp140_1.dll b/native/opennow-streamer/bin/msvcp140_1.dll new file mode 100644 index 000000000..fe6169ee5 Binary files /dev/null and b/native/opennow-streamer/bin/msvcp140_1.dll differ diff --git a/native/opennow-streamer/bin/msvcp140_2.dll b/native/opennow-streamer/bin/msvcp140_2.dll new file mode 100644 index 000000000..967be8237 Binary files /dev/null and b/native/opennow-streamer/bin/msvcp140_2.dll differ diff --git a/native/opennow-streamer/bin/msvcp140_atomic_wait.dll b/native/opennow-streamer/bin/msvcp140_atomic_wait.dll new file mode 100644 index 000000000..01f8e9a36 Binary files /dev/null and b/native/opennow-streamer/bin/msvcp140_atomic_wait.dll differ diff --git a/native/opennow-streamer/bin/msvcp140_codecvt_ids.dll b/native/opennow-streamer/bin/msvcp140_codecvt_ids.dll new file mode 100644 index 000000000..63214b8a8 Binary files /dev/null and b/native/opennow-streamer/bin/msvcp140_codecvt_ids.dll differ diff --git a/native/opennow-streamer/bin/orc-0.4-0.dll b/native/opennow-streamer/bin/orc-0.4-0.dll new file mode 100644 index 000000000..5270f9f69 Binary files /dev/null and b/native/opennow-streamer/bin/orc-0.4-0.dll differ diff --git a/native/opennow-streamer/bin/pcre2-8-0.dll b/native/opennow-streamer/bin/pcre2-8-0.dll new file mode 100644 index 000000000..092953e23 Binary files /dev/null and b/native/opennow-streamer/bin/pcre2-8-0.dll differ diff --git a/native/opennow-streamer/bin/vcruntime140.dll b/native/opennow-streamer/bin/vcruntime140.dll new file mode 100644 index 000000000..c2f509d20 Binary files /dev/null and b/native/opennow-streamer/bin/vcruntime140.dll differ diff --git a/native/opennow-streamer/bin/vcruntime140_1.dll b/native/opennow-streamer/bin/vcruntime140_1.dll new file mode 100644 index 000000000..64cd24630 Binary files /dev/null and b/native/opennow-streamer/bin/vcruntime140_1.dll differ diff --git a/native/opennow-streamer/src/backend.rs b/native/opennow-streamer/src/backend.rs index 9f0646324..8e1d7e0b6 100644 --- a/native/opennow-streamer/src/backend.rs +++ b/native/opennow-streamer/src/backend.rs @@ -557,6 +557,7 @@ mod tests { session: SessionInfo { session_id: "session-1".to_owned(), server_ip: "80-250-97-40.cloudmatchbeta.nvidiagrid.net".to_owned(), + ice_servers: Vec::new(), media_connection_info: Some(MediaConnectionInfo { ip: "10.0.0.7".to_owned(), port: 49003, @@ -576,6 +577,7 @@ mod tests { native_transition_diagnostics: None, }, shortcuts: NativeStreamerShortcutBindings::default(), + nvst_video: None, } } diff --git a/native/opennow-streamer/src/gstreamer_backend.rs b/native/opennow-streamer/src/gstreamer_backend.rs index 8664afb6d..dcbe36685 100644 --- a/native/opennow-streamer/src/gstreamer_backend.rs +++ b/native/opennow-streamer/src/gstreamer_backend.rs @@ -4,8 +4,9 @@ use crate::backend::{ web_rtc_media_connection_info, }; use crate::gstreamer_config::{ - resolve_d3d_fullscreen_sink, resolve_present_max_fps, NATIVE_D3D_FULLSCREEN_ENV, - NATIVE_PRESENT_MAX_FPS_ENV, PRESENT_LIMITER_AUTO_SENTINEL, + resolve_d3d_fullscreen_sink, resolve_present_max_fps, use_internal_renderer, + NATIVE_D3D_FULLSCREEN_ENV, NATIVE_PRESENT_MAX_FPS_ENV, PRESENT_LIMITER_AUTO_SENTINEL, + PRESENT_LIMITER_VRR_SENTINEL, }; use crate::gstreamer_platform::{clear_native_shortcut_bindings, set_native_shortcut_bindings}; use crate::gstreamer_pipeline::{ @@ -135,7 +136,10 @@ impl NativeStreamerBackend for GstreamerBackend { }; let session_id = context.session.session_id.clone(); - let pipeline = match GstreamerPipeline::build(self.event_sender.clone()) { + let pipeline = match GstreamerPipeline::build( + self.event_sender.clone(), + &context.session.ice_servers, + ) { Ok(pipeline) => pipeline, Err(message) => { return BackendReply { @@ -165,6 +169,78 @@ impl NativeStreamerBackend for GstreamerBackend { self.remote_description_set = false; let webrtc_name = pipeline.webrtc_name(); self.pipeline = Some(pipeline); + + let mut events = vec![Event::Status { + status: "ready", + message: Some(format!( + "GStreamer backend selected for session {session_id}; {} pipeline is ready.", + webrtc_name + )), + }]; + + if let Some(nvst) = self + .active_context + .as_ref() + .and_then(|ctx| ctx.nvst_video.clone()) + { + let fallback_codec = self + .active_context + .as_ref() + .map(|ctx| ctx.settings.codec.as_str().to_owned()) + .unwrap_or_else(|| "H265".to_owned()); + let requested_fps = self + .active_context + .as_ref() + .map(|ctx| ctx.settings.fps); + let d3d_fullscreen = resolve_d3d_fullscreen_sink( + self.active_context + .as_ref() + .map(|ctx| ctx.settings.enable_cloud_gsync) + .unwrap_or(false), + ); + let cloud_gsync_enabled = self + .active_context + .as_ref() + .map(|ctx| ctx.settings.enable_cloud_gsync) + .unwrap_or(false); + let present_max_fps = resolve_present_max_fps(cloud_gsync_enabled); + if let Some(pipeline) = self.pipeline.as_mut() { + pipeline.set_present_max_fps(present_max_fps); + pipeline.set_d3d_fullscreen_sink(d3d_fullscreen); + if let Some(ctx) = self.active_context.as_ref() { + let bitrate_kbps = ctx.settings.max_bitrate_mbps.saturating_mul(1000); + pipeline.configure_stats(ctx, bitrate_kbps); + } + match pipeline.attach_nvst_video( + nvst, + &fallback_codec, + requested_fps.filter(|fps| *fps > 0), + d3d_fullscreen, + ) { + Ok(()) => events.push(Event::Log { + level: "info", + message: "NVST classic UDP video receive scaffold attached (hybrid WebRTC input)." + .to_owned(), + }), + Err(message) => { + events.push(Event::Error { + code: "nvst-video-attach-failed".to_owned(), + message: message.clone(), + }); + return BackendReply { + events, + response: Some(Response::Error { + id: Some(id), + code: "nvst-video-attach-failed".to_owned(), + message, + }), + should_continue: true, + }; + } + } + } + } + if let (Some(surface), Some(pipeline)) = (self.render_surface.clone(), self.pipeline.as_ref()) { @@ -172,13 +248,7 @@ impl NativeStreamerBackend for GstreamerBackend { } BackendReply { - events: vec![Event::Status { - status: "ready", - message: Some(format!( - "GStreamer backend selected for session {session_id}; {} pipeline is ready.", - webrtc_name - )), - }], + events, response: Some(Response::Ok { id }), should_continue: true, } @@ -226,13 +296,17 @@ impl NativeStreamerBackend for GstreamerBackend { }; }; - let present_max_fps = resolve_present_max_fps(context.settings.fps); + let present_max_fps = resolve_present_max_fps(context.settings.enable_cloud_gsync); + // Internal child-surface mode never uses exclusive D3D fullscreen present. let d3d_fullscreen_sink = resolve_d3d_fullscreen_sink(context.settings.enable_cloud_gsync); set_native_shortcut_bindings(&context.shortcuts); pipeline.set_present_max_fps(present_max_fps); pipeline.set_d3d_fullscreen_sink(d3d_fullscreen_sink); pipeline.configure_stats(&context, prepared.nvst_params.max_bitrate_kbps); - if present_max_fps > 0 && present_max_fps != PRESENT_LIMITER_AUTO_SENTINEL { + if present_max_fps > 0 + && present_max_fps != PRESENT_LIMITER_AUTO_SENTINEL + && present_max_fps != PRESENT_LIMITER_VRR_SENTINEL + { events.push(Event::Log { level: "info", message: format!( @@ -240,6 +314,28 @@ impl NativeStreamerBackend for GstreamerBackend { context.settings.fps ), }); + } else if present_max_fps == PRESENT_LIMITER_AUTO_SENTINEL { + events.push(Event::Log { + level: "info", + message: format!( + "Native present limiter auto mode for {} fps stream (D3D11 caps to display Hz when stream fps exceeds it); set {NATIVE_PRESENT_MAX_FPS_ENV}=0 to disable.", + context.settings.fps + ), + }); + } else if present_max_fps == PRESENT_LIMITER_VRR_SENTINEL { + events.push(Event::Log { + level: "info", + message: format!( + "Native VRR present limiter auto mode for {} fps stream (caps below the display refresh ceiling when needed).", + context.settings.fps + ), + }); + } else { + events.push(Event::Log { + level: "info", + message: "Native present limiter disabled for uncapped VSync-off presentation." + .to_owned(), + }); } if d3d_fullscreen_sink { events.push(Event::Log { @@ -248,6 +344,12 @@ impl NativeStreamerBackend for GstreamerBackend { "Native D3D fullscreen presentation is enabled for Cloud G-Sync/VRR; set {NATIVE_D3D_FULLSCREEN_ENV}=0 to disable." ), }); + } else if use_internal_renderer() { + events.push(Event::Log { + level: "info", + message: "Native Internal renderer keeps exclusive D3D fullscreen off (child HWND present; sync=false, depth-1 post-decode queue)." + .to_owned(), + }); } let answer_sdp = match pipeline.negotiate_answer( @@ -465,17 +567,19 @@ impl NativeStreamerBackend for GstreamerBackend { #[cfg(test)] mod tests { use super::*; - use crate::gstreamer_config::{automatic_present_max_fps, PRESENT_LIMITER_AUTO_SENTINEL}; + use crate::gstreamer_config::PRESENT_LIMITER_AUTO_SENTINEL; use crate::gstreamer_input::parse_input_handshake_version; use crate::gstreamer_liveness::{ caps_framerate_summary, sink_stats_summary, VideoStallAction, VideoStallTracker, }; use crate::gstreamer_pipeline::{ - configure_stats_overlay_element, effective_present_max_fps, format_video_chain_selection, + backend_runs_on_platform, configure_stats_overlay_element, + default_rtp_video_api_priority, effective_present_max_fps, format_video_chain_selection, + init_gstreamer, preferred_rtp_video_apis_for, resolve_gstreamer_stun_server, rtp_video_chain_definition, RtpVideoApi, RtpVideoChainRole, }; use crate::gstreamer_transitions::resolve_queue_mode; - use crate::protocol::{NativeQueueMode, StreamSettings, VideoCodec}; + use crate::protocol::{IceServer, NativeQueueMode, StreamSettings, VideoCodec}; use crate::sdp::IceCredentials; use gst::prelude::*; use gstreamer as gst; @@ -483,8 +587,40 @@ mod tests { #[test] fn builds_and_stops_webrtc_pipeline() { - let pipeline = GstreamerPipeline::build(None).expect("GStreamer webrtcbin pipeline"); + let pipeline = GstreamerPipeline::build(None, &[]).expect("GStreamer webrtcbin pipeline"); assert_eq!(pipeline.webrtc.name(), "opennow-webrtcbin"); + assert_eq!( + pipeline.webrtc.property::("stun-server"), + "stun://stun2.l.google.com:19302" + ); + pipeline.stop().expect("pipeline stops"); + } + + #[test] + fn configures_session_stun_server_for_gstreamer() { + let servers = vec![ + IceServer { + urls: vec!["turn:relay.example.test:3478".to_owned()], + username: None, + credential: None, + }, + IceServer { + urls: vec!["stun:192.0.2.10:19302".to_owned()], + username: None, + credential: None, + }, + ]; + + assert_eq!( + resolve_gstreamer_stun_server(&servers), + "stun://192.0.2.10:19302" + ); + let pipeline = + GstreamerPipeline::build(None, &servers).expect("GStreamer webrtcbin pipeline"); + assert_eq!( + pipeline.webrtc.property::("stun-server"), + "stun://192.0.2.10:19302" + ); pipeline.stop().expect("pipeline stops"); } @@ -508,7 +644,8 @@ mod tests { #[test] fn defers_gfn_uuid_ice_password_until_actual_ice_stream_exists() { - let mut pipeline = GstreamerPipeline::build(None).expect("GStreamer webrtcbin pipeline"); + let mut pipeline = + GstreamerPipeline::build(None, &[]).expect("GStreamer webrtcbin pipeline"); let credentials = IceCredentials { ufrag: "2efecf37".to_owned(), pwd: "26b335b8-6cb2-4c18-96d0-963e5e586c9a".to_owned(), @@ -524,7 +661,8 @@ mod tests { #[test] fn remote_ice_credential_restore_after_remote_description_does_not_probe_fake_streams() { - let mut pipeline = GstreamerPipeline::build(None).expect("GStreamer webrtcbin pipeline"); + let mut pipeline = + GstreamerPipeline::build(None, &[]).expect("GStreamer webrtcbin pipeline"); let sdp = concat!( "v=0\r\n", "o=- 4373647202393833435 2 IN IP4 127.0.0.1\r\n", @@ -600,6 +738,13 @@ mod tests { assert!(capabilities.supports_input); } + #[test] + #[cfg(target_os = "linux")] + fn bundles_av1_rtp_depayloading() { + init_gstreamer().expect("GStreamer initializes"); + assert!(gst::ElementFactory::find("rtpav1depay").is_some()); + } + #[test] fn parses_input_handshake_versions() { assert_eq!( @@ -670,17 +815,48 @@ mod tests { assert!(vaapi.iter().any(|spec| spec.factory == "videoconvert")); assert_eq!(vaapi.last().map(|spec| spec.factory), Some("glimagesink")); + let nvdec = rtp_video_chain_definition("AV1", RtpVideoApi::Nvdec).expect("NVDEC AV1"); + assert_eq!(nvdec[3].factory, "nvav1dec"); + assert!(nvdec.iter().any(|spec| spec.factory == "videoconvert")); + assert_eq!(nvdec.last().map(|spec| spec.factory), Some("glimagesink")); + let v4l2 = rtp_video_chain_definition("H265", RtpVideoApi::V4L2).expect("V4L2 H265"); assert_eq!(v4l2[3].factory, "v4l2slh265dec"); - assert!(v4l2.iter().any(|spec| spec.factory == "videoconvert")); + assert!(!v4l2.iter().any(|spec| spec.factory == "videoconvert")); + + let v4l2_av1 = + rtp_video_chain_definition("AV1", RtpVideoApi::V4L2).expect("V4L2 AV1"); + assert_eq!(v4l2_av1[3].factory, "v4l2slav1dec"); let vulkan = rtp_video_chain_definition("H265", RtpVideoApi::Vulkan).expect("Vulkan H265"); - assert_eq!(vulkan[3].factory, "vulkanh265dec"); - assert!(vulkan - .iter() - .any(|spec| spec.factory == "vulkancolorconvert")); - assert_eq!(vulkan.last().map(|spec| spec.factory), Some("vulkansink")); - assert!(rtp_video_chain_definition("AV1", RtpVideoApi::Vulkan).is_none()); + #[cfg(target_os = "windows")] + { + assert_eq!(vulkan[3].factory, "d3d12h265dec"); + assert!(!vulkan.iter().any(|spec| spec.factory == "vulkanh265dec")); + // Default Internal renderer: Electron cannot composite vulkansink. + assert_eq!( + vulkan.last().map(|spec| spec.factory), + Some("d3d12videosink") + ); + assert!(vulkan.iter().any(|spec| { + spec.role == RtpVideoChainRole::StatsOverlay && spec.factory == "dwritetextoverlay" + })); + assert!(!vulkan.iter().any(|spec| spec.factory == "vulkanupload")); + } + #[cfg(not(target_os = "windows"))] + { + assert_eq!(vulkan[3].factory, "vulkanh265dec"); + assert!(vulkan + .iter() + .any(|spec| spec.factory == "vulkancolorconvert")); + assert_eq!(vulkan.last().map(|spec| spec.factory), Some("vulkansink")); + } + let vulkan_av1 = + rtp_video_chain_definition("AV1", RtpVideoApi::Vulkan).expect("Vulkan AV1"); + #[cfg(target_os = "windows")] + assert_eq!(vulkan_av1[3].factory, "d3d12av1dec"); + #[cfg(not(target_os = "windows"))] + assert_eq!(vulkan_av1[3].factory, "vulkanav1dec"); let software = rtp_video_chain_definition("H264", RtpVideoApi::Software).expect("software H264"); @@ -692,6 +868,38 @@ mod tests { ); } + #[test] + fn exposes_vulkan_on_windows_and_linux_only() { + assert!(backend_runs_on_platform(RtpVideoApi::Vulkan, "windows")); + assert!(backend_runs_on_platform(RtpVideoApi::Vulkan, "linux")); + assert!(!backend_runs_on_platform(RtpVideoApi::Vulkan, "macos")); + assert!(!backend_runs_on_platform(RtpVideoApi::Vulkan, "other")); + } + + #[test] + fn explicit_linux_backend_selection_does_not_fall_back() { + assert_eq!( + preferred_rtp_video_apis_for("nvdec", Some(120)), + vec![RtpVideoApi::Nvdec] + ); + assert_eq!( + preferred_rtp_video_apis_for("vaapi", Some(120)), + vec![RtpVideoApi::Vaapi] + ); + assert_eq!( + preferred_rtp_video_apis_for("v4l2", Some(120)), + vec![RtpVideoApi::V4L2] + ); + assert_eq!( + preferred_rtp_video_apis_for("vulkan", Some(240)), + vec![RtpVideoApi::Vulkan] + ); + assert_eq!( + preferred_rtp_video_apis_for("vk", Some(120)), + vec![RtpVideoApi::Vulkan] + ); + } + #[test] #[cfg(target_os = "windows")] fn windows_default_video_api_prefers_d3d12_for_high_fps() { @@ -714,15 +922,37 @@ mod tests { } #[test] - fn automatic_present_limiter_uses_display_refresh_below_requested_fps() { - assert_eq!(automatic_present_max_fps(240, Some(165)), 165); - assert_eq!(automatic_present_max_fps(240, Some(240)), 0); - assert_eq!(automatic_present_max_fps(240, Some(1)), 0); - assert_eq!(automatic_present_max_fps(240, None), 0); + #[cfg(all(target_os = "linux", target_arch = "aarch64"))] + fn linux_arm64_prefers_v4l2_for_raspberry_pi_and_arm_devices() { + assert_eq!( + default_rtp_video_api_priority(Some(60)), + vec![ + RtpVideoApi::V4L2, + RtpVideoApi::Nvdec, + RtpVideoApi::Vaapi, + RtpVideoApi::Vulkan, + RtpVideoApi::Software, + ] + ); } #[test] - fn automatic_present_limiter_only_targets_d3d11() { + #[cfg(all(target_os = "linux", not(target_arch = "aarch64")))] + fn linux_desktop_prefers_vendor_decoders_before_generic_paths() { + assert_eq!( + default_rtp_video_api_priority(Some(120)), + vec![ + RtpVideoApi::Nvdec, + RtpVideoApi::Vaapi, + RtpVideoApi::Vulkan, + RtpVideoApi::V4L2, + RtpVideoApi::Software, + ] + ); + } + + #[test] + fn automatic_present_limiter_targets_d3d_present_paths() { assert_eq!( effective_present_max_fps( PRESENT_LIMITER_AUTO_SENTINEL, @@ -739,7 +969,7 @@ mod tests { RtpVideoApi::D3D12, Some(165) ), - 0 + 165 ); assert_eq!( effective_present_max_fps(144, Some(240), RtpVideoApi::D3D12, Some(165)), @@ -749,6 +979,24 @@ mod tests { effective_present_max_fps(0, Some(240), RtpVideoApi::D3D11, Some(165)), 0 ); + assert_eq!( + effective_present_max_fps( + PRESENT_LIMITER_VRR_SENTINEL, + Some(240), + RtpVideoApi::D3D11, + Some(165) + ), + 162 + ); + assert_eq!( + effective_present_max_fps( + PRESENT_LIMITER_VRR_SENTINEL, + Some(120), + RtpVideoApi::D3D11, + Some(165) + ), + 0 + ); } #[test] diff --git a/native/opennow-streamer/src/gstreamer_config.rs b/native/opennow-streamer/src/gstreamer_config.rs index 5eb74654d..0b08f8284 100644 --- a/native/opennow-streamer/src/gstreamer_config.rs +++ b/native/opennow-streamer/src/gstreamer_config.rs @@ -1,3 +1,7 @@ +// Always compiled so present-policy unit tests run without the optional +// `gstreamer` feature; production callers live behind that feature. +#![allow(dead_code)] + pub(crate) const EXTERNAL_RENDERER_ENV: &str = "OPENNOW_NATIVE_EXTERNAL_RENDERER"; pub(crate) const NATIVE_VIDEO_API_ENV: &str = "OPENNOW_NATIVE_VIDEO_API"; pub(crate) const NATIVE_VIDEO_BACKEND_ENV: &str = "OPENNOW_NATIVE_VIDEO_BACKEND"; @@ -5,6 +9,8 @@ pub(crate) const NATIVE_ZERO_COPY_ENV: &str = "OPENNOW_NATIVE_ZERO_COPY"; pub(crate) const NATIVE_PRESENT_MAX_FPS_ENV: &str = "OPENNOW_NATIVE_PRESENT_MAX_FPS"; pub(crate) const NATIVE_D3D_FULLSCREEN_ENV: &str = "OPENNOW_NATIVE_D3D_FULLSCREEN"; pub(crate) const PRESENT_LIMITER_AUTO_SENTINEL: u32 = u32::MAX; +pub(crate) const PRESENT_LIMITER_VRR_SENTINEL: u32 = u32::MAX - 1; +const VRR_REFRESH_HEADROOM_FPS: u32 = 3; pub(crate) fn use_external_renderer_window() -> bool { std::env::var(EXTERNAL_RENDERER_ENV) @@ -14,7 +20,12 @@ pub(crate) fn use_external_renderer_window() -> bool { "0" | "false" | "no" | "off" ) }) - .unwrap_or(true) + // Default to the internal child-surface renderer (single Electron window). + .unwrap_or(false) +} + +pub(crate) fn use_internal_renderer() -> bool { + !use_external_renderer_window() } pub(crate) fn requested_video_backend() -> String { @@ -34,7 +45,7 @@ pub(crate) fn zero_copy_requested() -> bool { ) } -pub(crate) fn resolve_present_max_fps(requested_fps: u32) -> u32 { +pub(crate) fn resolve_present_max_fps(cloud_gsync_enabled: bool) -> u32 { if let Ok(value) = std::env::var(NATIVE_PRESENT_MAX_FPS_ENV) { let value = value.trim().to_ascii_lowercase(); if value == "0" || value == "off" || value == "false" || value == "unlimited" { @@ -47,8 +58,11 @@ pub(crate) fn resolve_present_max_fps(requested_fps: u32) -> u32 { return fps; } } - let _ = requested_fps; - PRESENT_LIMITER_AUTO_SENTINEL + if cloud_gsync_enabled { + PRESENT_LIMITER_VRR_SENTINEL + } else { + 0 + } } pub(crate) fn automatic_present_max_fps(requested_fps: u32, display_hz: Option) -> u32 { @@ -57,16 +71,105 @@ pub(crate) fn automatic_present_max_fps(requested_fps: u32, display_hz: Option) -> u32 { + display_hz + .filter(|display_hz| *display_hz >= 30 && *display_hz <= requested_fps) + .map(|display_hz| display_hz.saturating_sub(VRR_REFRESH_HEADROOM_FPS)) + .unwrap_or(0) +} + pub(crate) fn resolve_d3d_fullscreen_sink(cloud_gsync_enabled: bool) -> bool { - if let Ok(value) = std::env::var(NATIVE_D3D_FULLSCREEN_ENV) { + resolve_d3d_fullscreen_sink_for( + use_internal_renderer(), + cloud_gsync_enabled, + std::env::var(NATIVE_D3D_FULLSCREEN_ENV).ok(), + ) +} + +/// Pure policy for exclusive D3D fullscreen present. +/// +/// Internal (child HWND) always stays windowed — exclusive fullscreen fights +/// Electron parenting. External may enable it for Cloud G-Sync/VRR, or via +/// `OPENNOW_NATIVE_D3D_FULLSCREEN`. +pub(crate) fn resolve_d3d_fullscreen_sink_for( + internal_renderer: bool, + cloud_gsync_enabled: bool, + env_override: Option, +) -> bool { + if internal_renderer { + return false; + } + + if let Some(value) = env_override { let value = value.trim().to_ascii_lowercase(); - if value == "1" || value == "on" || value == "true" || value == "yes" { + if matches!(value.as_str(), "1" | "on" | "true" | "yes") { return true; } - if value == "0" || value == "off" || value == "false" || value == "no" { + if matches!(value.as_str(), "0" | "off" | "false" | "no") { return false; } } cloud_gsync_enabled } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn automatic_present_limiter_uses_display_refresh_below_requested_fps() { + assert_eq!(automatic_present_max_fps(240, Some(165)), 165); + assert_eq!(automatic_present_max_fps(240, Some(240)), 0); + assert_eq!(automatic_present_max_fps(240, Some(1)), 0); + assert_eq!(automatic_present_max_fps(240, None), 0); + } + + #[test] + fn default_present_policy_is_uncapped_without_vrr() { + assert_eq!(resolve_present_max_fps(false), 0); + assert_eq!( + resolve_present_max_fps(true), + PRESENT_LIMITER_VRR_SENTINEL + ); + } + + #[test] + fn vrr_present_limiter_stays_below_refresh_ceiling() { + assert_eq!(vrr_present_max_fps(240, Some(165)), 162); + assert_eq!(vrr_present_max_fps(165, Some(165)), 162); + assert_eq!(vrr_present_max_fps(120, Some(165)), 0); + assert_eq!(vrr_present_max_fps(240, None), 0); + } + + #[test] + fn internal_renderer_never_enables_exclusive_d3d_fullscreen() { + assert!(!resolve_d3d_fullscreen_sink_for(true, true, None)); + assert!(!resolve_d3d_fullscreen_sink_for( + true, + true, + Some("1".to_owned()) + )); + assert!(!resolve_d3d_fullscreen_sink_for( + true, + false, + Some("on".to_owned()) + )); + } + + #[test] + fn external_renderer_follows_cloud_gsync_and_env_for_d3d_fullscreen() { + assert!(resolve_d3d_fullscreen_sink_for(false, true, None)); + assert!(!resolve_d3d_fullscreen_sink_for(false, false, None)); + assert!(resolve_d3d_fullscreen_sink_for( + false, + false, + Some("1".to_owned()) + )); + assert!(!resolve_d3d_fullscreen_sink_for( + false, + true, + Some("0".to_owned()) + )); + } +} diff --git a/native/opennow-streamer/src/gstreamer_liveness.rs b/native/opennow-streamer/src/gstreamer_liveness.rs index 8d38d4fb4..fbc2fc5f5 100644 --- a/native/opennow-streamer/src/gstreamer_liveness.rs +++ b/native/opennow-streamer/src/gstreamer_liveness.rs @@ -291,6 +291,13 @@ impl VideoLivenessState { } pub(crate) fn set_stats_overlay(&self, overlay: Option) { + if let Some(element) = overlay.as_ref() { + set_property_if_supported( + element, + "visible", + self.stats_overlay_visible.load(Ordering::Relaxed), + ); + } if let Ok(mut current) = self.stats_overlay.lock() { *current = overlay; } @@ -1431,7 +1438,7 @@ pub(crate) fn watch_first_sink_buffer( let message = if use_external_renderer_window() { "Native video frames reached the external low-latency GStreamer renderer window." } else { - "Native video frames reached the embedded low-latency GStreamer sink." + "Native video frames reached the internal child-surface GStreamer renderer." }; let _ = event_sender.send(Event::Status { status: "streaming", diff --git a/native/opennow-streamer/src/gstreamer_pipeline.rs b/native/opennow-streamer/src/gstreamer_pipeline.rs index 5e98285d5..0a2854a41 100644 --- a/native/opennow-streamer/src/gstreamer_pipeline.rs +++ b/native/opennow-streamer/src/gstreamer_pipeline.rs @@ -1,9 +1,9 @@ use crate::gstreamer_backend::send_log; use crate::gstreamer_config::{ automatic_present_max_fps, requested_video_backend, use_external_renderer_window, - zero_copy_requested, EXTERNAL_RENDERER_ENV, NATIVE_D3D_FULLSCREEN_ENV, - NATIVE_PRESENT_MAX_FPS_ENV, NATIVE_VIDEO_API_ENV, NATIVE_VIDEO_BACKEND_ENV, - PRESENT_LIMITER_AUTO_SENTINEL, + use_internal_renderer, vrr_present_max_fps, zero_copy_requested, EXTERNAL_RENDERER_ENV, + NATIVE_D3D_FULLSCREEN_ENV, NATIVE_PRESENT_MAX_FPS_ENV, NATIVE_VIDEO_API_ENV, + NATIVE_VIDEO_BACKEND_ENV, PRESENT_LIMITER_AUTO_SENTINEL, PRESENT_LIMITER_VRR_SENTINEL, }; #[cfg(target_os = "windows")] use crate::gstreamer_input::NativeWindowInputBridge; @@ -17,14 +17,19 @@ use crate::gstreamer_liveness::{ watch_video_sink_caps_transitions, watch_video_sink_rate, VideoLivenessMonitor, }; use crate::gstreamer_platform::{ - apply_render_surface_to_video_sink, primary_display_refresh_hz, - release_native_input_capture, start_external_renderer_window_guard, + primary_display_refresh_hz, release_native_input_capture, start_external_renderer_window_guard, update_external_renderer_surface, }; +#[cfg(target_os = "windows")] +use crate::gstreamer_platform::arm_internal_child_input; use crate::gstreamer_transitions::DEFAULT_VIDEO_QUEUE_DEPTH; +use crate::internal_renderer::InternalRenderer; +use crate::nvst_video::{ + annexb_appsrc_caps, spawn_nvst_udp_receive, NvstVideoReceiveHandle, +}; use crate::protocol::{ - Event, IceCandidatePayload, NativeRenderSurface, NativeStreamerSessionContext, - NativeVideoBackendCapability, NativeVideoCodecCapability, + Event, IceCandidatePayload, IceServer, NativeRenderSurface, NativeStreamerSessionContext, + NativeVideoBackendCapability, NativeVideoCodecCapability, NvstVideoSession, }; use crate::sdp::IceCredentials; use gst::glib; @@ -32,14 +37,15 @@ use gst::prelude::*; use gstreamer as gst; use gstreamer_sdp as gst_sdp; use gstreamer_webrtc as gst_webrtc; -use std::collections::HashSet; +use std::collections::{HashMap, HashSet}; use std::ffi::CString; use std::sync::atomic::{AtomicBool, AtomicU32, Ordering}; use std::sync::mpsc::Sender; -use std::sync::{Arc, Mutex}; +use std::sync::{Arc, Mutex, OnceLock}; use std::thread; const WEBRTC_LATENCY_MS: u32 = 2; +const DEFAULT_GFN_STUN_SERVER: &str = "stun://stun2.l.google.com:19302"; const VIDEO_COMPRESSED_QUEUE_MAX_BUFFERS: u32 = 6; pub(crate) const VIDEO_QUEUE_MAX_BUFFERS: u32 = DEFAULT_VIDEO_QUEUE_DEPTH; const AUDIO_QUEUE_MAX_BUFFERS: u32 = 2; @@ -86,6 +92,7 @@ pub(crate) enum RtpVideoApi { D3D11, D3D12, VideoToolbox, + Nvdec, Vaapi, V4L2, Vulkan, @@ -98,6 +105,7 @@ impl RtpVideoApi { Self::D3D11 => "D3D11", Self::D3D12 => "D3D12", Self::VideoToolbox => "VideoToolbox", + Self::Nvdec => "NVIDIA NVDEC", Self::Vaapi => "VAAPI", Self::V4L2 => "V4L2", Self::Vulkan => "Vulkan", @@ -110,6 +118,7 @@ impl RtpVideoApi { Self::D3D11 => "d3d11", Self::D3D12 => "d3d12", Self::VideoToolbox => "videotoolbox", + Self::Nvdec => "nvdec", Self::Vaapi => "vaapi", Self::V4L2 => "v4l2", Self::Vulkan => "vulkan", @@ -121,7 +130,9 @@ impl RtpVideoApi { match self { Self::D3D11 | Self::D3D12 => "windows", Self::VideoToolbox => "macos", - Self::Vaapi | Self::V4L2 | Self::Vulkan => "linux", + Self::Nvdec | Self::Vaapi | Self::V4L2 => "linux", + Self::Vulkan if current_platform_label() == "windows" => "windows", + Self::Vulkan => "linux", Self::Software => "cross-platform", } } @@ -135,6 +146,10 @@ impl RtpVideoApi { Self::D3D12 => zero_copy_requested().then_some("video/x-raw(memory:D3D12Memory)"), Self::VideoToolbox => zero_copy_requested().then_some("video/x-raw(memory:GLMemory)"), Self::Vaapi => zero_copy_requested().then_some("video/x-raw(memory:VAMemory)"), + // Linux: keep Vulkan images in-GPU. Windows uses a DXVA→upload hybrid + // (vulkanh264dec currently SIGSEGVs on NVIDIA Windows), so skip a hard + // VulkanImage capsfilter on that path. + Self::Vulkan if cfg!(target_os = "windows") => None, Self::Vulkan => Some("video/x-raw(memory:VulkanImage)"), _ => None, } @@ -143,11 +158,16 @@ impl RtpVideoApi { fn post_decode_converter_factory(self) -> Option<&'static str> { match self { Self::D3D11 | Self::D3D12 => None, + // Windows Vulkan present chain inserts download/convert/upload explicitly. + Self::Vulkan if cfg!(target_os = "windows") => None, Self::Vulkan => Some("vulkancolorconvert"), Self::VideoToolbox | Self::Vaapi if zero_copy_requested() => None, // Non-D3D hardware decoders are not guaranteed to negotiate directly with every // platform sink. Keep these paths reliable with an explicit raw-video conversion stage. - Self::VideoToolbox | Self::Vaapi | Self::V4L2 | Self::Software => Some("videoconvert"), + Self::VideoToolbox | Self::Nvdec | Self::Vaapi | Self::Software => Some("videoconvert"), + // V4L2 stateless decoders expose DMABuf on devices such as Raspberry Pi. + // Let glimagesink import it directly instead of forcing a CPU copy. + Self::V4L2 => None, } } @@ -163,6 +183,7 @@ impl RtpVideoApi { Self::D3D11 => "d3d11videosink", Self::D3D12 => "d3d12videosink", Self::VideoToolbox => "glimagesink", + Self::Nvdec => "glimagesink", Self::Vaapi => "glimagesink", Self::V4L2 => "glimagesink", Self::Vulkan => "vulkansink", @@ -179,13 +200,24 @@ impl RtpVideoApi { (Self::D3D12, "H264") => Some("d3d12h264dec"), (Self::D3D12, "AV1") => Some("d3d12av1dec"), (Self::VideoToolbox, "H265" | "HEVC" | "H264") => Some("vtdec_hw"), + (Self::Nvdec, "H265" | "HEVC") => Some("nvh265dec"), + (Self::Nvdec, "H264") => Some("nvh264dec"), + (Self::Nvdec, "AV1") => Some("nvav1dec"), (Self::Vaapi, "H265" | "HEVC") => Some("vah265dec"), (Self::Vaapi, "H264") => Some("vah264dec"), (Self::Vaapi, "AV1") => Some("vaav1dec"), (Self::V4L2, "H265" | "HEVC") => Some("v4l2slh265dec"), (Self::V4L2, "H264") => Some("v4l2slh264dec"), + (Self::V4L2, "AV1") => Some("v4l2slav1dec"), + // vulkanh264dec/vulkanh265dec SIGSEGV on current NVIDIA Windows drivers; + // use DXVA decode (prefer D3D12) and either D3D present (Internal) or + // upload into Vulkan (External). + (Self::Vulkan, "H265" | "HEVC") if cfg!(target_os = "windows") => Some("d3d12h265dec"), + (Self::Vulkan, "H264") if cfg!(target_os = "windows") => Some("d3d12h264dec"), + (Self::Vulkan, "AV1") if cfg!(target_os = "windows") => Some("d3d12av1dec"), (Self::Vulkan, "H265" | "HEVC") => Some("vulkanh265dec"), (Self::Vulkan, "H264") => Some("vulkanh264dec"), + (Self::Vulkan, "AV1") => Some("vulkanav1dec"), (Self::Software, "H265" | "HEVC") => Some("avdec_h265"), (Self::Software, "H264") => Some("avdec_h264"), (Self::Software, "AV1") => Some("avdec_av1"), @@ -200,7 +232,18 @@ impl RtpVideoApi { (Self::Vaapi, "AV1") => &["vaapiav1dec"], (Self::V4L2, "H265" | "HEVC") => &["v4l2h265dec"], (Self::V4L2, "H264") => &["v4l2h264dec"], + (Self::V4L2, "AV1") => &["v4l2av1dec"], (Self::VideoToolbox, "H265" | "HEVC" | "H264") => &["vtdec"], + (Self::Vulkan, "H265" | "HEVC") if cfg!(target_os = "windows") => { + &["d3d11h265dec", "nvh265dec"] + } + (Self::Vulkan, "H264") if cfg!(target_os = "windows") => { + &["d3d11h264dec", "nvh264dec"] + } + (Self::Vulkan, "AV1") if cfg!(target_os = "windows") => { + &["d3d11av1dec", "nvav1dec"] + } + (Self::Software, "AV1") => &["dav1ddec", "av1dec"], _ => &[], } } @@ -208,10 +251,25 @@ impl RtpVideoApi { fn sink_fallback_factories(self) -> &'static [&'static str] { match self { Self::VideoToolbox => &["osxvideosink", "autovideosink"], - Self::Vaapi | Self::V4L2 => { - &["waylandsink", "ximagesink", "xvimagesink", "autovideosink"] + // Prefer X11-capable sinks first: Internal Linux embeds via GstVideoOverlay + // into an X11 child. waylandsink cannot paint into that handle. + Self::Nvdec | Self::Vaapi | Self::V4L2 => { + &["ximagesink", "xvimagesink", "glimagesink", "waylandsink", "autovideosink"] } - Self::Software => &["glimagesink", "waylandsink", "ximagesink", "xvimagesink"], + Self::Software => &["ximagesink", "xvimagesink", "glimagesink", "waylandsink"], + _ => &[], + } + } + + /// Sinks that can bind to the Internal X11 child via GstVideoOverlay. + fn internal_x11_sink_candidates(self) -> &'static [&'static str] { + match self { + Self::Nvdec | Self::Vaapi | Self::V4L2 | Self::Software => { + &["glimagesink", "ximagesink", "xvimagesink"] + } + // vulkansink implements GstVideoOverlay on Linux, so it can bind + // directly to the X11 child while retaining VulkanImage memory. + Self::Vulkan => &["vulkansink"], _ => &[], } } @@ -246,15 +304,31 @@ impl RtpVideoChainSpec { } } -#[derive(Clone, Debug, Default)] +#[derive(Clone, Debug)] pub(crate) struct GstreamerRenderState { surface: Arc>>, video_sink: Arc>>, + internal_renderer: Arc, external_renderer_logged: Arc, + internal_renderer_logged: Arc, external_window_guard_started: Arc, external_window_guard_stop: Arc, } +impl Default for GstreamerRenderState { + fn default() -> Self { + Self { + surface: Arc::new(Mutex::new(None)), + video_sink: Arc::new(Mutex::new(None)), + internal_renderer: Arc::new(InternalRenderer::new()), + external_renderer_logged: Arc::new(AtomicBool::new(false)), + internal_renderer_logged: Arc::new(AtomicBool::new(false)), + external_window_guard_started: Arc::new(AtomicBool::new(false)), + external_window_guard_stop: Arc::new(AtomicBool::new(false)), + } + } +} + impl GstreamerRenderState { fn set_surface(&self, surface: NativeRenderSurface, event_sender: &Option>) { if let Ok(mut current) = self.surface.lock() { @@ -265,18 +339,23 @@ impl GstreamerRenderState { fn set_video_sink(&self, sink: gst::Element, event_sender: &Option>) { if let Ok(mut current) = self.video_sink.lock() { - *current = Some(sink); + *current = Some(sink.clone()); + } + if use_internal_renderer() { + if let Err(message) = self.internal_renderer.set_video_sink(sink) { + send_log(event_sender, "warn", message); + } } self.apply(event_sender); } fn apply(&self, event_sender: &Option>) { - let sink = self.video_sink.lock().ok().and_then(|sink| sink.clone()); - let Some(sink) = sink else { - return; - }; - if use_external_renderer_window() { + let sink_ready = self.video_sink.lock().ok().and_then(|sink| sink.clone()); + let Some(_sink) = sink_ready else { + return; + }; + if let Some(surface) = self.surface.lock().ok().and_then(|surface| surface.clone()) { update_external_renderer_surface(&surface); } @@ -296,21 +375,47 @@ impl GstreamerRenderState { event_sender, "info", format!( - "Using external native GStreamer renderer window; set {EXTERNAL_RENDERER_ENV}=0 to retry Electron HWND embedding." + "Using external native GStreamer renderer window; set {EXTERNAL_RENDERER_ENV}=0 for the internal child-surface renderer." ), ); } return; } + let sink_ready = self.video_sink.lock().ok().and_then(|sink| sink.clone()); + let Some(_sink) = sink_ready else { + return; + }; + let surface = self.surface.lock().ok().and_then(|surface| surface.clone()); let Some(surface) = surface else { return; }; - if let Err(message) = apply_render_surface_to_video_sink(&sink, &surface) { + if !self.internal_renderer_logged.swap(true, Ordering::SeqCst) { + send_log( + event_sender, + "info", + format!( + "Using internal native child-surface renderer; set {EXTERNAL_RENDERER_ENV}=1 for the floating GStreamer window." + ), + ); + } + + if let Err(message) = self.internal_renderer.apply_surface(&surface) { send_log(event_sender, "warn", message); } + + // Keep ClipCursor / capture rect aligned with the StreamView hole, and + // (re)arm RawInput if the child HWND was recreated on parent change. + #[cfg(target_os = "windows")] + { + update_external_renderer_surface(&surface); + let hwnd = self.internal_renderer.child_handle(); + if hwnd != 0 { + let _ = arm_internal_child_input(hwnd); + } + } } fn stop_external_renderer_window_guard(&self) { @@ -319,6 +424,12 @@ impl GstreamerRenderState { self.external_window_guard_started .store(false, Ordering::SeqCst); } + + fn destroy_internal_renderer(&self) { + self.internal_renderer.destroy(); + self.internal_renderer_logged + .store(false, Ordering::SeqCst); + } } #[derive(Debug)] @@ -332,14 +443,19 @@ pub(crate) struct GstreamerPipeline { render_state: GstreamerRenderState, present_max_fps: Arc, d3d_fullscreen_sink: Arc, + /// When true, WebRTC RTP video pads are ignored (classic NVST UDP owns video). + skip_webrtc_video: Arc, + nvst_receive: Option, video_liveness: VideoLivenessMonitor, event_sender: Option>, pub(crate) original_remote_ice_credentials: Option, - original_remote_ice_credentials_restored: bool, } impl GstreamerPipeline { - pub(crate) fn build(event_sender: Option>) -> Result { + pub(crate) fn build( + event_sender: Option>, + ice_servers: &[IceServer], + ) -> Result { init_gstreamer()?; let pipeline = gst::Pipeline::new(); @@ -349,6 +465,13 @@ impl GstreamerPipeline { .build() .map_err(|error| format!("Failed to create webrtcbin: {error}"))?; configure_webrtc_low_latency(&webrtc); + let stun_server = resolve_gstreamer_stun_server(ice_servers); + webrtc.set_property("stun-server", &stun_server); + send_log( + &event_sender, + "info", + format!("Configured GStreamer ICE with STUN server {stun_server}."), + ); let input_state = GstreamerInputState::default(); let render_state = GstreamerRenderState::default(); @@ -364,6 +487,7 @@ impl GstreamerPipeline { ); let present_max_fps = Arc::new(AtomicU32::new(0)); let d3d_fullscreen_sink = Arc::new(AtomicBool::new(false)); + let skip_webrtc_video = Arc::new(AtomicBool::new(false)); wire_incoming_media_sink( &pipeline, &webrtc, @@ -371,6 +495,7 @@ impl GstreamerPipeline { render_state.clone(), present_max_fps.clone(), d3d_fullscreen_sink.clone(), + skip_webrtc_video.clone(), video_liveness.clone(), ); @@ -391,10 +516,11 @@ impl GstreamerPipeline { render_state, present_max_fps, d3d_fullscreen_sink, + skip_webrtc_video, + nvst_receive: None, video_liveness, event_sender, original_remote_ice_credentials: None, - original_remote_ice_credentials_restored: false, }) } @@ -424,6 +550,238 @@ impl GstreamerPipeline { self.video_liveness.configure(context, target_bitrate_kbps); } + /// Attach classic NVST UDP video: appsrc → parse → decoder → sink, plus UDP recv thread. + /// Keeps webrtcbin for SCTP input; ignores WebRTC RTP video pads. + pub(crate) fn attach_nvst_video( + &mut self, + session: NvstVideoSession, + fallback_codec: &str, + requested_fps: Option, + d3d_fullscreen_sink: bool, + ) -> Result<(), String> { + if self.nvst_receive.is_some() { + return Ok(()); + } + + let codec = session + .codec + .as_deref() + .filter(|c| !c.is_empty()) + .unwrap_or(fallback_codec); + let codec_upper = codec.to_ascii_uppercase(); + let encoding = match codec_upper.as_str() { + "H264" => "H264", + "H265" | "HEVC" => "H265", + other => { + return Err(format!( + "NVST classic UDP video scaffold supports H264/H265, got {other}" + )); + } + }; + + self.skip_webrtc_video.store(true, Ordering::SeqCst); + + let (video_api, mut specs) = rtp_video_chain_specs(encoding, requested_fps).ok_or_else(|| { + format!( + "NVST Annex-B decode chain unavailable for {encoding}; install GStreamer plugins or set {NATIVE_VIDEO_BACKEND_ENV}=software." + ) + })?; + // Drop RTP depayloader — appsrc feeds assembled Annex-B AUs. + specs.retain(|spec| spec.role != RtpVideoChainRole::Depayloader); + if specs + .first() + .is_none_or(|spec| spec.role != RtpVideoChainRole::Parser) + { + return Err(format!( + "NVST video chain for {encoding} is missing a parser after depayloader removal." + )); + } + + let caps_str = annexb_appsrc_caps(encoding); + let appsrc = gst::ElementFactory::make("appsrc") + .name("nvst-annexb") + .build() + .map_err(|error| format!("Failed to create nvst-annexb appsrc: {error}"))?; + let caps = caps_str + .parse::() + .map_err(|error| format!("Invalid NVST appsrc caps: {error}"))?; + appsrc.set_property("caps", &caps); + set_property_if_supported(&appsrc, "is-live", true); + set_property_from_str_if_supported(&appsrc, "format", "time"); + set_property_if_supported(&appsrc, "block", false); + set_property_if_supported(&appsrc, "max-bytes", 0u64); + set_property_from_str_if_supported(&appsrc, "stream-type", "stream"); + + let streaming_reported = Arc::new(AtomicBool::new(false)); + let mut elements: Vec = Vec::with_capacity(specs.len() + 1); + + let result = (|| -> Result<(), String> { + send_log( + &self.event_sender, + "info", + format!( + "Attaching NVST classic UDP video ({encoding}) via appsrc Annex-B → {}; {}", + video_api.label(), + format_video_chain_selection(encoding, video_api, &specs) + ), + ); + + let configured_present_max_fps = self.present_max_fps.load(Ordering::SeqCst); + let effective = effective_present_max_fps( + configured_present_max_fps, + requested_fps, + video_api, + primary_display_refresh_hz(), + ); + self.present_max_fps.store(effective, Ordering::SeqCst); + + self.pipeline + .add(&appsrc) + .map_err(|error| format!("Failed to add NVST appsrc: {error}"))?; + elements.push(appsrc.clone()); + + for spec in &specs { + let element = make_element(spec.factory)?; + configure_rtp_video_chain_element( + &element, + spec.clone(), + video_api, + d3d_fullscreen_sink, + ); + if spec.role == RtpVideoChainRole::StatsOverlay { + self.video_liveness.set_stats_overlay(Some(element.clone())); + } + self.pipeline.add(&element).map_err(|error| { + format!( + "Failed to add {} for NVST {encoding} video chain: {error}", + spec.factory + ) + })?; + elements.push(element); + } + + for pair in elements.windows(2) { + pair[0].link(&pair[1]).map_err(|error| { + format!( + "Failed to link {} -> {} for NVST {encoding}: {error:?}", + element_factory_name(&pair[0]), + element_factory_name(&pair[1]) + ) + })?; + } + + let sink = elements + .last() + .ok_or_else(|| format!("NVST {encoding} video chain has no sink."))?; + if let Some(post_decode_queue) = + specs + .iter() + .zip(elements.iter().skip(1)) + .find_map(|(spec, element)| { + (spec.role == RtpVideoChainRole::PostDecodeQueue).then_some(element) + }) + { + self.video_liveness + .set_post_decode_queue(post_decode_queue.clone()); + watch_video_decoded_rate( + post_decode_queue, + &self.event_sender, + Some(self.video_liveness.clone()), + ); + } + if let Some(pre_decode_queue) = + specs + .iter() + .zip(elements.iter().skip(1)) + .find_map(|(spec, element)| { + (spec.role == RtpVideoChainRole::PreDecodeQueue).then_some(element) + }) + { + self.video_liveness + .set_pre_decode_queue(pre_decode_queue.clone()); + } + if let Some(parser) = specs.iter().zip(elements.iter().skip(1)).find_map( + |(spec, element)| (spec.role == RtpVideoChainRole::Parser).then_some(element), + ) { + watch_video_caps_transitions( + parser, + "parser", + &self.event_sender, + self.video_liveness.clone(), + ); + } + if let Some(decoder) = specs.iter().zip(elements.iter().skip(1)).find_map( + |(spec, element)| (spec.role == RtpVideoChainRole::Decoder).then_some(element), + ) { + self.video_liveness.set_decoder(decoder.clone()); + watch_video_caps_transitions( + decoder, + "decoder", + &self.event_sender, + self.video_liveness.clone(), + ); + } + + self.render_state + .set_video_sink(sink.clone(), &self.event_sender); + install_present_limiter( + sink, + self.present_max_fps.clone(), + &self.event_sender, + Some(self.video_liveness.clone()), + ); + watch_video_sink_caps_transitions( + sink, + &self.event_sender, + Some(self.video_liveness.clone()), + ); + watch_first_sink_buffer(sink, "video", &self.event_sender, &streaming_reported); + watch_video_sink_rate( + sink, + &self.event_sender, + Some(self.video_liveness.clone()), + ); + + for element in &elements { + element.sync_state_with_parent().map_err(|error| { + format!("Failed to sync NVST {encoding} video-chain element state: {error}") + })?; + } + + self.video_liveness.update_hardware_acceleration(format!( + "GStreamer {} (NVST UDP)", + video_api.label() + )); + self.video_liveness.start( + self.pipeline.clone(), + sink.clone(), + self.event_sender.clone(), + ); + + let handle = spawn_nvst_udp_receive( + session, + appsrc, + self.event_sender.clone(), + )?; + self.nvst_receive = Some(handle); + + // Ensure pipeline can run the appsrc branch even before WebRTC offer. + let _ = self.pipeline.set_state(gst::State::Playing); + + Ok(()) + })(); + + if result.is_err() { + self.skip_webrtc_video.store(false, Ordering::SeqCst); + for element in &elements { + let _ = element.set_state(gst::State::Null); + let _ = self.pipeline.remove(element); + } + } + + result + } + fn ensure_input_data_channels( &mut self, partial_reliable_threshold_ms: u32, @@ -447,6 +805,8 @@ impl GstreamerPipeline { #[cfg(target_os = "windows")] fn ensure_native_window_input_bridge(&mut self) { + // Win32 RawInput: floating external window OR internal child HWND. + // Electron click-through across a topmost D3D sibling is unreliable. if self.native_window_input_bridge.is_some() { return; } @@ -459,10 +819,23 @@ impl GstreamerPipeline { input_channels, self.event_sender.clone(), )); + if use_internal_renderer() { + let hwnd = self.render_state.internal_renderer.child_handle(); + if hwnd != 0 && arm_internal_child_input(hwnd) { + send_log( + &self.event_sender, + "info", + "Armed RawInput capture on the internal child HWND.".to_owned(), + ); + } + } } #[cfg(not(target_os = "windows"))] fn ensure_native_window_input_bridge(&mut self) { + if use_internal_renderer() { + return; + } send_log( &self.event_sender, "warn", @@ -506,10 +879,6 @@ impl GstreamerPipeline { &mut self, stage: &str, ) -> Result { - if self.original_remote_ice_credentials_restored { - return Ok(true); - } - let Some(credentials) = self.original_remote_ice_credentials.clone() else { return Ok(false); }; @@ -587,7 +956,6 @@ impl GstreamerPipeline { return Ok(false); } - self.original_remote_ice_credentials_restored = true; send_log( &self.event_sender, "info", @@ -761,8 +1129,13 @@ impl GstreamerPipeline { } pub(crate) fn stop(mut self) -> Result<(), String> { + if let Some(handle) = self.nvst_receive.take() { + handle.stop(); + } + self.skip_webrtc_video.store(false, Ordering::SeqCst); self.video_liveness.set_stats_overlay_visible(false); self.render_state.stop_external_renderer_window_guard(); + self.render_state.destroy_internal_renderer(); #[cfg(target_os = "windows")] if let Some(mut bridge) = self.native_window_input_bridge.take() { bridge.stop(); @@ -776,6 +1149,22 @@ impl GstreamerPipeline { } } +pub(crate) fn resolve_gstreamer_stun_server(ice_servers: &[IceServer]) -> String { + ice_servers + .iter() + .flat_map(|server| server.urls.iter()) + .find_map(|url| { + let url = url.trim(); + if url.starts_with("stun://") { + Some(url.to_owned()) + } else { + url.strip_prefix("stun:") + .map(|endpoint| format!("stun://{endpoint}")) + } + }) + .unwrap_or_else(|| DEFAULT_GFN_STUN_SERVER.to_owned()) +} + fn nice_stream_from_ice_transport( transport: &gst_webrtc::WebRTCICETransport, ) -> Option { @@ -802,7 +1191,25 @@ fn nice_stream_from_ice_transport( } pub(crate) fn init_gstreamer() -> Result<(), String> { - gst::init().map_err(|error| format!("Failed to initialize GStreamer: {error}")) + gst::init().map_err(|error| format!("Failed to initialize GStreamer: {error}"))?; + #[cfg(target_os = "linux")] + { + static RTP_PLUGIN_REGISTRATION: OnceLock> = OnceLock::new(); + RTP_PLUGIN_REGISTRATION + .get_or_init(|| { + if gst::ElementFactory::find("rtpav1depay").is_some() { + return Ok(()); + } + gstrsrtp::plugin_register_static().map_err(|error| { + format!("Failed to register the bundled AV1 RTP plugin: {error}") + }) + }) + .clone() + } + #[cfg(not(target_os = "linux"))] + { + Ok(()) + } } pub(crate) fn set_property_if_supported>( @@ -856,6 +1263,9 @@ pub(crate) fn configure_queue(element: &gst::Element, max_buffers: u32, leaky_do } pub(crate) fn configure_sink_for_low_latency(element: &gst::Element) { + // GFN-aligned present: never clock-sync or QoS-throttle the sink. Latency + // comes from decode + a depth-1 leaky post-decode queue + optional present + // limiter, not from GstBaseSink pacing. set_property_if_supported(element, "sync", false); set_property_if_supported(element, "async", false); set_property_if_supported(element, "qos", false); @@ -869,6 +1279,29 @@ pub(crate) fn configure_sink_for_low_latency(element: &gst::Element) { set_property_if_supported(element, "force-aspect-ratio", true); } +/// Configure d3d11/d3d12videosink for low-latency Internal/External present. +/// +/// GStreamer docs: `fullscreen` is ignored unless `fullscreen-toggle-mode` +/// includes `property`. Internal always keeps exclusive fullscreen off (caller +/// passes `d3d_fullscreen_sink=false`); External + Cloud G-Sync may enable it. +pub(crate) fn configure_d3d_video_sink(element: &gst::Element, d3d_fullscreen_sink: bool) { + configure_sink_for_low_latency(element); + // d3d12 only: attaching the swapchain directly to an external HWND can turn + // a present stall into upstream decode backpressure on the child-surface path. + set_property_if_supported(element, "direct-swapchain", false); + set_property_if_supported(element, "error-on-closed", false); + // RawInput owns mouse/keyboard; do not let the sink emit GstNavigation events. + set_property_if_supported(element, "enable-navigation-events", false); + set_property_if_supported(element, "fullscreen-on-alt-enter", false); + if d3d_fullscreen_sink { + set_property_from_str_if_supported(element, "fullscreen-toggle-mode", "property"); + set_property_if_supported(element, "fullscreen", true); + } else { + set_property_from_str_if_supported(element, "fullscreen-toggle-mode", "none"); + set_property_if_supported(element, "fullscreen", false); + } +} + pub(crate) fn configure_stats_overlay_element(element: &gst::Element) { set_property_if_supported(element, "visible", false); set_property_if_supported(element, "text", ""); @@ -1156,6 +1589,7 @@ fn wire_incoming_media_sink( render_state: GstreamerRenderState, present_max_fps: Arc, d3d_fullscreen_sink: Arc, + skip_webrtc_video: Arc, video_liveness: VideoLivenessMonitor, ) { let pipeline = pipeline.downgrade(); @@ -1179,6 +1613,21 @@ fn wire_incoming_media_sink( } if let Some(encoding) = rtp_video_encoding(src_pad) { + if skip_webrtc_video.load(Ordering::SeqCst) { + send_log( + &event_sender, + "info", + format!( + "Ignoring WebRTC RTP video pad ({encoding}); NVST classic UDP owns video." + ), + ); + if let Err(error) = + link_decoded_media_to_fakesink(&pipeline, src_pad, "ignored webrtc video") + { + send_log(&event_sender, "debug", error); + } + return; + } match link_rtp_video_pad( &pipeline, src_pad, @@ -1352,6 +1801,12 @@ pub(crate) fn rtp_video_chain_definition( video_api: RtpVideoApi, ) -> Option> { let codec = encoding.to_ascii_uppercase(); + + #[cfg(target_os = "windows")] + if video_api == RtpVideoApi::Vulkan { + return windows_vulkan_present_chain_definition(codec.as_str()); + } + let mut specs = vec![ RtpVideoChainSpec::new( rtp_video_depayloader_factory(codec.as_str())?, @@ -1399,12 +1854,109 @@ pub(crate) fn rtp_video_chain_definition( Some(specs) } +/// Windows Vulkan path. +/// +/// Electron Internal hole-punch only composites DXGI swapchains on the child HWND. +/// A Win32 Vulkan surface on that HWND (or a GSTVULKAN child of it) presents black +/// even though vulkansink reports rendered frames. So: +/// - Internal: DXVA decode + `d3d12videosink` (D3D11 fallback; visible in Electron) +/// - External: DXVA decode + convert/upload + `vulkansink` (true Vulkan present) +/// +/// Native `vulkanh264dec` currently access-violates under NVIDIA Windows drivers. +#[cfg(target_os = "windows")] +fn windows_vulkan_present_chain_definition(codec: &str) -> Option> { + if use_internal_renderer() { + windows_vulkan_internal_present_chain_definition(codec) + } else { + windows_vulkan_external_present_chain_definition(codec) + } +} + +/// Internal Electron path: DXVA + D3D12 present (D3D11 fallback; DXGI hole-punch). +#[cfg(target_os = "windows")] +fn windows_vulkan_internal_present_chain_definition(codec: &str) -> Option> { + let decoder = RtpVideoApi::Vulkan.decoder_factory(codec)?; + let prefer_d3d12 = decoder.starts_with("d3d12"); + let sink = if prefer_d3d12 { + "d3d12videosink" + } else { + "d3d11videosink" + }; + let memory_api = if prefer_d3d12 { + RtpVideoApi::D3D12 + } else { + RtpVideoApi::D3D11 + }; + let mut specs = vec![ + RtpVideoChainSpec::new( + rtp_video_depayloader_factory(codec)?, + RtpVideoChainRole::Depayloader, + ), + RtpVideoChainSpec::new(rtp_video_parser_factory(codec)?, RtpVideoChainRole::Parser), + RtpVideoChainSpec::new("queue", RtpVideoChainRole::PreDecodeQueue), + RtpVideoChainSpec::new(decoder, RtpVideoChainRole::Decoder), + ]; + if let Some(memory_caps) = memory_api.memory_caps() { + specs.push(RtpVideoChainSpec::with_caps( + "capsfilter", + RtpVideoChainRole::PostDecodeCapsFilter, + memory_caps, + )); + } + specs.push(RtpVideoChainSpec::new( + "dwritetextoverlay", + RtpVideoChainRole::StatsOverlay, + )); + specs.push(RtpVideoChainSpec::new( + "queue", + RtpVideoChainRole::PostDecodeQueue, + )); + specs.push(RtpVideoChainSpec::new(sink, RtpVideoChainRole::Sink)); + Some(specs) +} + +/// External / capability path: DXVA + vulkanupload + vulkansink. +#[cfg(target_os = "windows")] +fn windows_vulkan_external_present_chain_definition(codec: &str) -> Option> { + let decoder = RtpVideoApi::Vulkan.decoder_factory(codec)?; + Some(vec![ + RtpVideoChainSpec::new( + rtp_video_depayloader_factory(codec)?, + RtpVideoChainRole::Depayloader, + ), + RtpVideoChainSpec::new(rtp_video_parser_factory(codec)?, RtpVideoChainRole::Parser), + RtpVideoChainSpec::new("queue", RtpVideoChainRole::PreDecodeQueue), + RtpVideoChainSpec::new(decoder, RtpVideoChainRole::Decoder), + // Composite diagnostics while frames are still in the DXVA/D3D path. + // dwritetextoverlay cannot consume VulkanImage memory after upload. + RtpVideoChainSpec::new("dwritetextoverlay", RtpVideoChainRole::StatsOverlay), + RtpVideoChainSpec::new("d3d11download", RtpVideoChainRole::PostDecodeConverter), + RtpVideoChainSpec::new("videoconvert", RtpVideoChainRole::PostDecodeConverter), + RtpVideoChainSpec::with_caps( + "capsfilter", + RtpVideoChainRole::PostDecodeCapsFilter, + "video/x-raw,format=RGBA", + ), + RtpVideoChainSpec::new("vulkanupload", RtpVideoChainRole::PostDecodeConverter), + RtpVideoChainSpec::new("queue", RtpVideoChainRole::PostDecodeQueue), + RtpVideoChainSpec::new(RtpVideoApi::Vulkan.sink_factory(), RtpVideoChainRole::Sink), + ]) +} + fn preferred_rtp_video_apis(requested_fps: Option) -> Vec { let requested = requested_video_backend(); - match requested.as_str() { + preferred_rtp_video_apis_for(requested.as_str(), requested_fps) +} + +pub(crate) fn preferred_rtp_video_apis_for( + requested: &str, + requested_fps: Option, +) -> Vec { + match requested { "d3d11" => vec![RtpVideoApi::D3D11], "d3d12" => vec![RtpVideoApi::D3D12], "videotoolbox" | "vt" => vec![RtpVideoApi::VideoToolbox], + "nvdec" | "nvcodec" | "nvidia" => vec![RtpVideoApi::Nvdec], "vaapi" | "va" => vec![RtpVideoApi::Vaapi], "v4l2" | "v4l2stateless" => vec![RtpVideoApi::V4L2], "vulkan" | "vk" => vec![RtpVideoApi::Vulkan], @@ -1419,11 +1971,27 @@ pub(crate) fn effective_present_max_fps( video_api: RtpVideoApi, display_hz: Option, ) -> u32 { + if configured_present_max_fps == PRESENT_LIMITER_VRR_SENTINEL { + if !matches!(video_api, RtpVideoApi::D3D11 | RtpVideoApi::D3D12) { + return 0; + } + return requested_fps + .filter(|fps| *fps > 0) + .map(|fps| vrr_present_max_fps(fps, display_hz)) + .unwrap_or(0); + } + if configured_present_max_fps != PRESENT_LIMITER_AUTO_SENTINEL { return configured_present_max_fps; } - if !matches!(video_api, RtpVideoApi::D3D11) { + // D3D11/D3D12 present (and Internal Vulkan→D3D) need the auto limiter so + // stream fps above display Hz does not stall the DXGI present path. + if !matches!(video_api, RtpVideoApi::D3D11 | RtpVideoApi::D3D12) + && !(cfg!(target_os = "windows") + && video_api == RtpVideoApi::Vulkan + && use_internal_renderer()) + { return 0; } @@ -1459,6 +2027,7 @@ pub(crate) fn default_rtp_video_api_priority(requested_fps: Option) -> Vec< let _ = requested_fps; vec![ RtpVideoApi::V4L2, + RtpVideoApi::Nvdec, RtpVideoApi::Vaapi, RtpVideoApi::Vulkan, RtpVideoApi::Software, @@ -1468,6 +2037,7 @@ pub(crate) fn default_rtp_video_api_priority(requested_fps: Option) -> Vec< { let _ = requested_fps; vec![ + RtpVideoApi::Nvdec, RtpVideoApi::Vaapi, RtpVideoApi::Vulkan, RtpVideoApi::V4L2, @@ -1503,6 +2073,8 @@ fn rtp_video_chain_specs( spec.factory = sink; } } + align_windows_vulkan_download_factory(&mut specs, decoder); + align_windows_vulkan_internal_present(&mut specs, decoder); insert_requested_fps_capssetter(&mut specs, requested_fps); specs.retain(|spec| { spec.role != RtpVideoChainRole::StatsOverlay @@ -1512,6 +2084,88 @@ fn rtp_video_chain_specs( }) } +fn align_windows_vulkan_download_factory(specs: &mut Vec, decoder: &str) { + #[cfg(target_os = "windows")] + { + let download = if decoder.starts_with("d3d12") { + Some("d3d12download") + } else if decoder.starts_with("d3d11") { + Some("d3d11download") + } else if decoder.starts_with("nv") { + // NVDEC Windows outputs system memory in our bundle; skip D3D download. + None + } else { + return; + }; + + match download { + Some(factory) => { + if let Some(spec) = specs.iter_mut().find(|spec| { + spec.role == RtpVideoChainRole::PostDecodeConverter + && (spec.factory == "d3d11download" || spec.factory == "d3d12download") + }) { + spec.factory = factory; + } + } + None => { + specs.retain(|spec| { + !(spec.role == RtpVideoChainRole::PostDecodeConverter + && (spec.factory == "d3d11download" || spec.factory == "d3d12download")) + }); + } + } + } + #[cfg(not(target_os = "windows"))] + { + let _ = (specs, decoder); + } +} + +/// Keep Internal Vulkan→D3D present matched to the selected DXVA decoder family. +#[cfg(target_os = "windows")] +fn align_windows_vulkan_internal_present(specs: &mut Vec, decoder: &str) { + if !use_internal_renderer() { + return; + } + let has_d3d_present = specs.iter().any(|spec| { + spec.role == RtpVideoChainRole::Sink + && (spec.factory == "d3d11videosink" || spec.factory == "d3d12videosink") + }); + if !has_d3d_present { + return; + } + + let (sink, memory_caps) = if decoder.starts_with("d3d12") + && gst::ElementFactory::find("d3d12videosink").is_some() + { + ("d3d12videosink", RtpVideoApi::D3D12.memory_caps()) + } else if decoder.starts_with("d3d11") + && gst::ElementFactory::find("d3d11videosink").is_some() + { + ("d3d11videosink", RtpVideoApi::D3D11.memory_caps()) + } else { + return; + }; + + if let Some(spec) = specs + .iter_mut() + .find(|spec| spec.role == RtpVideoChainRole::Sink) + { + spec.factory = sink; + } + if let Some(spec) = specs + .iter_mut() + .find(|spec| spec.role == RtpVideoChainRole::PostDecodeCapsFilter) + { + if let Some(caps) = memory_caps { + spec.caps = Some(caps.to_owned()); + } + } +} + +#[cfg(not(target_os = "windows"))] +fn align_windows_vulkan_internal_present(_specs: &mut Vec, _decoder: &str) {} + fn insert_requested_fps_capssetter(specs: &mut Vec, requested_fps: Option) { let Some(fps) = requested_fps.filter(|fps| *fps > 0) else { return; @@ -1519,6 +2173,26 @@ fn insert_requested_fps_capssetter(specs: &mut Vec, requested if gst::ElementFactory::find("capssetter").is_none() { return; } + // Windows Vulkan hybrid / D3D present: forcing plain video/x-raw onto a D3D + // memory pad breaks caps negotiation. + if specs.iter().any(|spec| { + matches!( + spec.factory, + "d3d11download" + | "d3d12download" + | "vulkanupload" + | "d3d11videosink" + | "d3d12videosink" + | "d3d11h264dec" + | "d3d11h265dec" + | "d3d11av1dec" + | "d3d12h264dec" + | "d3d12h265dec" + | "d3d12av1dec" + ) + }) { + return; + } let Some(decoder_index) = specs .iter() .position(|spec| spec.role == RtpVideoChainRole::Decoder) @@ -1540,10 +2214,60 @@ fn select_decoder_factory(video_api: RtpVideoApi, codec: &str) -> Option<&'stati let primary = video_api.decoder_factory(codec)?; std::iter::once(primary) .chain(video_api.fallback_decoder_factories(codec).iter().copied()) - .find(|factory| gst::ElementFactory::find(factory).is_some()) + .find(|factory| decoder_factory_usable(factory)) +} + +fn decoder_factory_usable(factory: &'static str) -> bool { + static DECODER_PROBES: OnceLock>> = OnceLock::new(); + let probes = DECODER_PROBES.get_or_init(|| Mutex::new(HashMap::new())); + if let Ok(probes) = probes.lock() { + if let Some(usable) = probes.get(factory) { + return *usable; + } + } + + let usable = gst::ElementFactory::make(factory) + .build() + .ok() + .is_some_and(|decoder| { + let usable = decoder.set_state(gst::State::Ready).is_ok(); + let _ = decoder.set_state(gst::State::Null); + usable + }); + if let Ok(mut probes) = probes.lock() { + probes.insert(factory, usable); + } + usable } fn select_sink_factory(video_api: RtpVideoApi) -> Option<&'static str> { + // Internal Linux: never pick waylandsink for the X11 child overlay path. + #[cfg(target_os = "linux")] + if use_internal_renderer() { + let internal = video_api.internal_x11_sink_candidates(); + if let Some(factory) = internal + .iter() + .copied() + .find(|factory| gst::ElementFactory::find(factory).is_some()) + { + return Some(factory); + } + } + + // Internal Windows + Vulkan: Electron hole-punch cannot composite Win32 Vulkan + // swapchains; present with D3D12 (D3D11 fallback) VideoOverlay instead. + #[cfg(target_os = "windows")] + if use_internal_renderer() && video_api == RtpVideoApi::Vulkan { + return ["d3d12videosink", "d3d11videosink"] + .into_iter() + .find(|factory| gst::ElementFactory::find(factory).is_some()); + } + + select_capability_sink_factory(video_api) +} + +/// Sink advertised in capabilities / used when not overriding for Internal present. +fn select_capability_sink_factory(video_api: RtpVideoApi) -> Option<&'static str> { std::iter::once(video_api.sink_factory()) .chain(video_api.sink_fallback_factories().iter().copied()) .find(|factory| gst::ElementFactory::find(factory).is_some()) @@ -1560,6 +2284,7 @@ fn all_rtp_video_apis() -> &'static [RtpVideoApi] { RtpVideoApi::D3D12, RtpVideoApi::D3D11, RtpVideoApi::VideoToolbox, + RtpVideoApi::Nvdec, RtpVideoApi::Vaapi, RtpVideoApi::V4L2, RtpVideoApi::Vulkan, @@ -1591,7 +2316,17 @@ pub(crate) fn current_platform_label() -> &'static str { } fn backend_runs_on_current_platform(video_api: RtpVideoApi) -> bool { - video_api.platform() == current_platform_label() || video_api.platform() == "cross-platform" + backend_runs_on_platform(video_api, current_platform_label()) +} + +pub(crate) fn backend_runs_on_platform(video_api: RtpVideoApi, platform: &str) -> bool { + match video_api { + RtpVideoApi::D3D11 | RtpVideoApi::D3D12 => platform == "windows", + RtpVideoApi::VideoToolbox => platform == "macos", + RtpVideoApi::Nvdec | RtpVideoApi::Vaapi | RtpVideoApi::V4L2 => platform == "linux", + RtpVideoApi::Vulkan => matches!(platform, "windows" | "linux"), + RtpVideoApi::Software => true, + } } pub(crate) fn native_video_backend_capabilities() -> Vec { @@ -1604,8 +2339,10 @@ pub(crate) fn native_video_backend_capabilities() -> Vec NativeVideoBackendCapability { let platform_supported = backend_runs_on_current_platform(video_api); + // Advertise the true API sink (vulkansink). Internal Windows Vulkan may present + // via d3d12/d3d11videosink at session time for Electron hole-punch compatibility. let sink_factory = platform_supported - .then(|| select_sink_factory(video_api)) + .then(|| select_capability_sink_factory(video_api)) .flatten(); let codecs = all_video_codec_labels() .iter() @@ -1658,7 +2395,22 @@ fn native_video_codec_capability( let decoder = platform_supported .then(|| select_decoder_factory(video_api, codec)) .flatten(); - let definition = rtp_video_chain_definition(codec, video_api); + // Capability checks the External Vulkan present chain so vulkansink/vulkanupload + // must be present even when Internal sessions present via D3D11. + let definition = { + #[cfg(target_os = "windows")] + { + if video_api == RtpVideoApi::Vulkan { + windows_vulkan_external_present_chain_definition(codec) + } else { + rtp_video_chain_definition(codec, video_api) + } + } + #[cfg(not(target_os = "windows"))] + { + rtp_video_chain_definition(codec, video_api) + } + }; let available = platform_supported && sink.is_some() && decoder.is_some() @@ -1718,16 +2470,21 @@ fn zero_copy_modes_for_backend(video_api: RtpVideoApi) -> Vec { RtpVideoApi::D3D11 => vec!["D3D11Memory".to_owned()], RtpVideoApi::D3D12 => vec!["D3D12Memory".to_owned()], RtpVideoApi::VideoToolbox => vec!["GLMemory".to_owned()], + RtpVideoApi::Nvdec => Vec::new(), RtpVideoApi::Vaapi => vec!["VAMemory".to_owned()], + // Linux keeps decoded frames as VulkanImage. Windows uses DXVA→upload, + // so there is no end-to-end VulkanImage zero-copy path yet. + RtpVideoApi::Vulkan if cfg!(target_os = "windows") => Vec::new(), RtpVideoApi::Vulkan => vec!["VulkanImage".to_owned()], - RtpVideoApi::V4L2 | RtpVideoApi::Software => Vec::new(), + RtpVideoApi::V4L2 => vec!["DMABuf".to_owned()], + RtpVideoApi::Software => Vec::new(), } } fn configure_rtp_video_chain_element( element: &gst::Element, spec: RtpVideoChainSpec, - _video_api: RtpVideoApi, + video_api: RtpVideoApi, d3d_fullscreen_sink: bool, ) { match spec.role { @@ -1780,13 +2537,11 @@ fn configure_rtp_video_chain_element( configure_queue_for_low_latency(element, "video"); } RtpVideoChainRole::Sink => { - configure_sink_for_low_latency(element); - // Direct swapchain can turn a window/present stall into upstream decode backpressure. - set_property_if_supported(element, "direct-swapchain", false); - set_property_if_supported(element, "error-on-closed", false); - set_property_if_supported(element, "fullscreen", d3d_fullscreen_sink); - set_property_if_supported(element, "fullscreen-on-alt-enter", false); - set_property_from_str_if_supported(element, "fullscreen-toggle-mode", "none"); + if matches!(video_api, RtpVideoApi::D3D11 | RtpVideoApi::D3D12) { + configure_d3d_video_sink(element, d3d_fullscreen_sink); + } else { + configure_sink_for_low_latency(element); + } } } } @@ -1839,8 +2594,10 @@ fn link_rtp_video_pad( present_max_fps.store(effective_present_max_fps, Ordering::SeqCst); if effective_present_max_fps > 0 { let reason = if configured_present_max_fps == PRESENT_LIMITER_AUTO_SENTINEL { - "auto-enabled for the D3D11 path to prevent display-rate present backpressure" + "auto-enabled for the D3D present path to prevent display-rate present backpressure" .to_owned() + } else if configured_present_max_fps == PRESENT_LIMITER_VRR_SENTINEL { + "kept below the display refresh ceiling for VRR".to_owned() } else { format!("configured by {NATIVE_PRESENT_MAX_FPS_ENV}") }; @@ -2007,9 +2764,15 @@ pub(crate) fn format_video_chain_selection( .unwrap_or("unknown"); let converter = specs .iter() - .find(|spec| spec.role == RtpVideoChainRole::PostDecodeConverter) + .filter(|spec| spec.role == RtpVideoChainRole::PostDecodeConverter) .map(|spec| spec.factory) - .unwrap_or("none"); + .collect::>() + .join("+"); + let converter = if converter.is_empty() { + "none".to_owned() + } else { + converter + }; let memory = specs .iter() .find(|spec| spec.role == RtpVideoChainRole::PostDecodeCapsFilter) @@ -2024,9 +2787,19 @@ pub(crate) fn format_video_chain_selection( } else { "software" }; - + let path_note = if cfg!(target_os = "windows") && video_api == RtpVideoApi::Vulkan { + if sink == "d3d12videosink" { + " (DXVA decode + D3D12 present; Electron cannot composite Win32 vulkansink — use External for true Vulkan present)" + } else if sink == "d3d11videosink" { + " (DXVA decode + D3D11 present; Electron cannot composite Win32 vulkansink — use External for true Vulkan present)" + } else { + " (DXVA decode + Vulkan present; native vulkanh264dec is unstable on Windows)" + } + } else { + "" + }; format!( - "Selected native {acceleration} video path for RTP {encoding}: backend={}, decoder={decoder}, converter={converter}, renderer={sink}, memory={memory}.", + "Selected native {acceleration} video path for RTP {encoding}: backend={}, decoder={decoder}, converter={converter}, renderer={sink}, memory={memory}{path_note}.", video_api.label() ) } @@ -2115,12 +2888,15 @@ fn link_decoded_media_pad( fn video_sink_factories() -> Vec<(&'static str, Option)> { #[cfg(target_os = "windows")] { - if gst::ElementFactory::find("d3d11videosink").is_some() { + let d3d_sink = ["d3d12videosink", "d3d11videosink"] + .into_iter() + .find(|factory| gst::ElementFactory::find(factory).is_some()); + if let Some(sink) = d3d_sink { let mut factories = vec![("queue", None)]; if gst::ElementFactory::find("dwritetextoverlay").is_some() { factories.push(("dwritetextoverlay", None)); } - factories.push(("d3d11videosink", Some(false))); + factories.push((sink, Some(false))); return factories; } } @@ -2165,7 +2941,12 @@ fn link_media_chain( } } if sync_property.is_some() || factory.ends_with("sink") { - configure_sink_for_low_latency(&element); + if factory == "d3d11videosink" || factory == "d3d12videosink" { + // Fallback decodebin path: never exclusive-fullscreen (Internal default). + configure_d3d_video_sink(&element, false); + } else { + configure_sink_for_low_latency(&element); + } } pipeline .add(&element) diff --git a/native/opennow-streamer/src/gstreamer_platform.rs b/native/opennow-streamer/src/gstreamer_platform.rs index 5525b8148..1a936b760 100644 --- a/native/opennow-streamer/src/gstreamer_platform.rs +++ b/native/opennow-streamer/src/gstreamer_platform.rs @@ -1,1626 +1,1705 @@ -#[cfg(target_os = "windows")] -use crate::gstreamer_backend::send_log; -#[cfg(target_os = "windows")] -use crate::protocol::NativeRenderRect; -use crate::protocol::{Event, NativeRenderSurface, NativeStreamerShortcutBindings}; -#[cfg(target_os = "windows")] -use gst_video::prelude::*; -use gstreamer as gst; -#[cfg(target_os = "windows")] -use gstreamer_video as gst_video; -#[cfg(target_os = "windows")] -use std::ffi::c_void; -use std::sync::atomic::AtomicBool; -#[cfg(target_os = "windows")] -use std::sync::atomic::Ordering; -use std::sync::mpsc::Sender; -use std::sync::Arc; -#[cfg(target_os = "windows")] -use std::thread; -#[cfg(target_os = "windows")] -use std::time::Duration; - -#[cfg(target_os = "windows")] -fn parse_window_handle(value: &str) -> Result { - let trimmed = value.trim(); - let hex = trimmed - .strip_prefix("0x") - .or_else(|| trimmed.strip_prefix("0X")); - let parsed = if let Some(hex) = hex { - usize::from_str_radix(hex, 16) - } else { - trimmed.parse::() - } - .map_err(|error| format!("Invalid native render window handle {value:?}: {error}"))?; - - if parsed == 0 { - return Err("Native render window handle is zero.".to_owned()); - } - - Ok(parsed) -} - -#[cfg(target_os = "windows")] -fn normalized_render_rect(rect: Option<&NativeRenderRect>) -> NativeRenderRect { - let Some(rect) = rect else { - return NativeRenderRect { - x: 0, - y: 0, - width: 2, - height: 2, - }; - }; - - NativeRenderRect { - x: rect.x.max(0), - y: rect.y.max(0), - width: rect.width.max(2), - height: rect.height.max(2), - } -} - -#[cfg(target_os = "windows")] -pub(crate) fn start_external_renderer_window_guard( - event_sender: Option>, - stop: Arc, -) { - thread::spawn(move || { - let mut logged = false; - while !stop.load(Ordering::SeqCst) { - if stop.load(Ordering::SeqCst) { - break; - } - - let configured = unsafe { win32_renderer_window::protect_process_renderer_window() }; - if configured && !logged { - send_log( - &event_sender, - "info", - "Configured external native renderer window for fullscreen DX11 input capture." - .to_owned(), - ); - logged = true; - } - thread::sleep(if logged { - Duration::from_millis(500) - } else { - Duration::from_millis(100) - }); - } - }); -} - -#[cfg(not(target_os = "windows"))] -pub(crate) fn start_external_renderer_window_guard( - _event_sender: Option>, - _stop: Arc, -) { -} - -#[cfg(target_os = "windows")] -pub(crate) fn set_native_shortcut_bindings(bindings: &NativeStreamerShortcutBindings) { - unsafe { - win32_renderer_window::set_shortcut_bindings(bindings.clone()); - } -} - -#[cfg(not(target_os = "windows"))] -pub(crate) fn set_native_shortcut_bindings(_bindings: &NativeStreamerShortcutBindings) {} - -#[cfg(target_os = "windows")] -pub(crate) fn clear_native_shortcut_bindings() { - unsafe { - win32_renderer_window::clear_shortcut_bindings(); - } -} - -#[cfg(not(target_os = "windows"))] -pub(crate) fn clear_native_shortcut_bindings() {} - -#[cfg(target_os = "windows")] -pub(crate) fn release_native_input_capture() { - unsafe { - win32_renderer_window::release_current_input_capture(); - } -} - -#[cfg(not(target_os = "windows"))] -pub(crate) fn release_native_input_capture() {} - -#[cfg(target_os = "windows")] -pub(crate) fn update_external_renderer_surface(surface: &NativeRenderSurface) { - let target = surface - .window_handle - .as_deref() - .and_then(|window_handle| parse_window_handle(window_handle).ok()) - .and_then(|window_handle| { - surface - .visible - .then_some(()) - .and(surface.rect.as_ref()) - .map(|rect| (window_handle, normalized_render_rect(Some(rect)))) - }); - - unsafe { - win32_renderer_window::set_render_target_surface(target); - } -} - -#[cfg(not(target_os = "windows"))] -pub(crate) fn update_external_renderer_surface(_surface: &NativeRenderSurface) {} - -#[cfg(target_os = "windows")] -pub(crate) mod win32_renderer_window { - use crate::gstreamer_input::NativeWindowInputEvent; - use crate::protocol::NativeRenderRect; - use crate::protocol::{NativeStreamerShortcutAction, NativeStreamerShortcutBindings}; - use crate::shortcuts::NativeShortcutMatcher; - use std::collections::{HashMap, HashSet}; - use std::ffi::c_void; - use std::ptr::{null, null_mut}; - use std::sync::atomic::{AtomicU64, Ordering}; - use std::sync::mpsc::Sender; - use std::sync::{Mutex, OnceLock}; - use std::thread; - use std::time::{Duration, Instant}; - - type Bool = i32; - type Dword = u32; - type Hcursor = *mut c_void; - type Hmonitor = *mut c_void; - type Hrawinput = *mut c_void; - type Hwnd = *mut c_void; - type Lparam = isize; - type Lresult = isize; - type Uint = u32; - type Wparam = usize; - - const GWL_STYLE: i32 = -16; - const GWL_EXSTYLE: i32 = -20; - const GWLP_WNDPROC: i32 = -4; - const GW_OWNER: Uint = 4; - const HTCLIENT: isize = 1; - const HWND_NOTOPMOST: Hwnd = -2isize as Hwnd; - const MA_ACTIVATE: isize = 1; - const MONITOR_DEFAULTTONEAREST: Dword = 0x0000_0002; - const RID_INPUT: Uint = 0x1000_0003; - const RIDEV_REMOVE: Dword = 0x0000_0001; - const RIDEV_NOLEGACY: Dword = 0x0000_0030; - const RIDEV_CAPTUREMOUSE: Dword = 0x0000_0200; - const RIM_TYPEMOUSE: Dword = 0; - const RIM_TYPEKEYBOARD: Dword = 1; - const RI_KEY_BREAK: u16 = 0x0001; - const RI_KEY_E0: u16 = 0x0002; - const RI_KEY_E1: u16 = 0x0004; - const RI_MOUSE_LEFT_BUTTON_DOWN: u16 = 0x0001; - const RI_MOUSE_LEFT_BUTTON_UP: u16 = 0x0002; - const RI_MOUSE_RIGHT_BUTTON_DOWN: u16 = 0x0004; - const RI_MOUSE_RIGHT_BUTTON_UP: u16 = 0x0008; - const RI_MOUSE_MIDDLE_BUTTON_DOWN: u16 = 0x0010; - const RI_MOUSE_MIDDLE_BUTTON_UP: u16 = 0x0020; - const RI_MOUSE_BUTTON_4_DOWN: u16 = 0x0040; - const RI_MOUSE_BUTTON_4_UP: u16 = 0x0080; - const RI_MOUSE_BUTTON_5_DOWN: u16 = 0x0100; - const RI_MOUSE_BUTTON_5_UP: u16 = 0x0200; - const RI_MOUSE_WHEEL: u16 = 0x0400; - const VK_SHIFT: u16 = 0x10; - const VK_ESCAPE: u16 = 0x1B; - const VK_V: u16 = 0x56; - const VK_TAB: u16 = 0x09; - const VK_CONTROL: u16 = 0x11; - const VK_MENU: u16 = 0x12; - const VK_CAPITAL: i32 = 0x14; - const VK_NUMLOCK: i32 = 0x90; - const VK_SCROLL: i32 = 0x91; - const VK_LSHIFT: u16 = 0xA0; - const VK_RSHIFT: u16 = 0xA1; - const VK_LCONTROL: u16 = 0xA2; - const VK_RCONTROL: u16 = 0xA3; - const VK_LMENU: u16 = 0xA4; - const VK_RMENU: u16 = 0xA5; - const VK_LWIN: u16 = 0x5B; - const VK_RWIN: u16 = 0x5C; - const WM_INPUT: Uint = 0x00FF; - const WM_NCHITTEST: Uint = 0x0084; - const WM_MOUSEACTIVATE: Uint = 0x0021; - const WM_SETCURSOR: Uint = 0x0020; - const WM_KILLFOCUS: Uint = 0x0008; - const WM_ACTIVATE: Uint = 0x0006; - const WA_INACTIVE: usize = 0; - const WM_KEYDOWN: Uint = 0x0100; - const WM_KEYUP: Uint = 0x0101; - const WM_SYSKEYDOWN: Uint = 0x0104; - const WM_SYSKEYUP: Uint = 0x0105; - const WM_LBUTTONDOWN: Uint = 0x0201; - const WM_LBUTTONUP: Uint = 0x0202; - const WM_RBUTTONDOWN: Uint = 0x0204; - const WM_RBUTTONUP: Uint = 0x0205; - const WM_MBUTTONDOWN: Uint = 0x0207; - const WM_MBUTTONUP: Uint = 0x0208; - const WM_XBUTTONDOWN: Uint = 0x020B; - const WM_XBUTTONUP: Uint = 0x020C; - const XBUTTON1: u16 = 0x0001; - const XBUTTON2: u16 = 0x0002; - const WS_CAPTION: isize = 0x00C0_0000; - const WS_MAXIMIZEBOX: isize = 0x0001_0000; - const WS_MINIMIZEBOX: isize = 0x0002_0000; - const WS_SYSMENU: isize = 0x0008_0000; - const WS_THICKFRAME: isize = 0x0004_0000; - const WS_EX_NOACTIVATE: isize = 0x0800_0000; - const WS_EX_TOOLWINDOW: isize = 0x0000_0080; - const WS_EX_TRANSPARENT: isize = 0x0000_0020; - const SWP_NOSIZE: u32 = 0x0001; - const SWP_NOMOVE: u32 = 0x0002; - const SWP_NOACTIVATE: u32 = 0x0010; - const SWP_FRAMECHANGED: u32 = 0x0020; - const SW_MINIMIZE: i32 = 6; - const ESCAPE_SCANCODE: u16 = 0x0001; - const ESCAPE_HOLD_TO_MINIMIZE: Duration = Duration::from_secs(5); - - struct EnumState { - process_id: u32, - candidates: Vec, - } - - #[derive(Clone, Copy)] - struct WindowCandidate { - hwnd: Hwnd, - area: i64, - } - - #[derive(Clone, Copy)] - struct RenderTargetSurface { - hwnd: isize, - client_rect: Rect, - } - - #[repr(C)] - #[derive(Clone, Copy)] - struct Rect { - left: i32, - top: i32, - right: i32, - bottom: i32, - } - - #[repr(C)] - #[derive(Clone, Copy)] - struct Point { - x: i32, - y: i32, - } - - #[repr(C)] - struct MonitorInfo { - cb_size: Dword, - rc_monitor: Rect, - rc_work: Rect, - dw_flags: Dword, - } - - #[repr(C)] - struct RawInputDevice { - us_usage_page: u16, - us_usage: u16, - dw_flags: Dword, - hwnd_target: Hwnd, - } - - #[repr(C)] - struct RawInputHeader { - dw_type: Dword, - dw_size: Dword, - h_device: *mut c_void, - w_param: Wparam, - } - - #[repr(C)] - #[derive(Clone, Copy)] - struct RawMouse { - us_flags: u16, - buttons: u32, - ul_raw_buttons: u32, - l_last_x: i32, - l_last_y: i32, - ul_extra_information: u32, - } - - #[repr(C)] - #[derive(Clone, Copy)] - struct RawKeyboard { - make_code: u16, - flags: u16, - reserved: u16, - vkey: u16, - message: Uint, - extra_information: u32, - } - - #[derive(Clone, Copy)] - struct PressedKey { - keycode: u16, - scancode: u16, - suppressed: bool, - } - - #[derive(Clone, Copy)] - struct EscapeKeyPress { - scancode: u16, - hold_timer_armed: bool, - } - - static INPUT_EVENT_SENDER: OnceLock>>> = - OnceLock::new(); - static ORIGINAL_WNDPROCS: OnceLock>> = OnceLock::new(); - static CAPTURED_HWND: OnceLock>> = OnceLock::new(); - static PROTECTED_HWND: OnceLock>> = OnceLock::new(); - static PRESSED_KEYS: OnceLock>> = OnceLock::new(); - static LAST_LOCK_KEYS_STATE: OnceLock> = OnceLock::new(); - static LEGACY_SUPPRESSED_KEYS: OnceLock>> = OnceLock::new(); - static STARTED_AT: OnceLock = OnceLock::new(); - static ESCAPE_HOLD_HWND: OnceLock>> = OnceLock::new(); - static ESCAPE_HOLD_TOKEN: OnceLock = OnceLock::new(); - static ESCAPE_KEY_PRESS: OnceLock>> = OnceLock::new(); - static SHORTCUT_MATCHER: OnceLock> = OnceLock::new(); - static RENDER_TARGET_SURFACE: OnceLock>> = OnceLock::new(); - - #[link(name = "user32")] - unsafe extern "system" { - fn CallWindowProcW( - previous: isize, - hwnd: Hwnd, - message: Uint, - wparam: Wparam, - lparam: Lparam, - ) -> Lresult; - fn ClientToScreen(hwnd: Hwnd, point: *mut Point) -> Bool; - fn ClipCursor(rect: *const Rect) -> Bool; - fn DefWindowProcW(hwnd: Hwnd, message: Uint, wparam: Wparam, lparam: Lparam) -> Lresult; - fn EnumWindows( - callback: Option Bool>, - lparam: Lparam, - ) -> Bool; - fn GetMonitorInfoW(monitor: Hmonitor, info: *mut MonitorInfo) -> Bool; - fn GetRawInputData( - raw_input: Hrawinput, - command: Uint, - data: *mut c_void, - size: *mut u32, - header_size: u32, - ) -> u32; - fn GetKeyState(virtual_key: i32) -> i16; - fn GetWindow(hwnd: Hwnd, command: Uint) -> Hwnd; - fn GetWindowLongPtrW(hwnd: Hwnd, index: i32) -> isize; - fn GetWindowRect(hwnd: Hwnd, rect: *mut Rect) -> Bool; - fn GetWindowThreadProcessId(hwnd: Hwnd, process_id: *mut u32) -> u32; - fn IsIconic(hwnd: Hwnd) -> Bool; - fn IsWindowVisible(hwnd: Hwnd) -> Bool; - fn MonitorFromWindow(hwnd: Hwnd, flags: Dword) -> Hmonitor; - fn RegisterRawInputDevices(devices: *const RawInputDevice, count: u32, size: u32) -> Bool; - fn ReleaseCapture() -> Bool; - fn SetCapture(hwnd: Hwnd) -> Hwnd; - fn SetCursor(cursor: Hcursor) -> Hcursor; - fn SetFocus(hwnd: Hwnd) -> Hwnd; - fn SetForegroundWindow(hwnd: Hwnd) -> Bool; - fn SetWindowLongPtrW(hwnd: Hwnd, index: i32, new_long: isize) -> isize; - fn SetWindowPos( - hwnd: Hwnd, - insert_after: Hwnd, - x: i32, - y: i32, - cx: i32, - cy: i32, - flags: u32, - ) -> Bool; - fn ShowWindow(hwnd: Hwnd, command: i32) -> Bool; - fn ShowCursor(show: Bool) -> i32; - } - - #[link(name = "kernel32")] - unsafe extern "system" { - fn GetCurrentProcessId() -> u32; - } - - pub unsafe fn set_render_target_surface(target: Option<(usize, NativeRenderRect)>) { - let target_surface = target.map(|(window_handle, rect)| RenderTargetSurface { - hwnd: window_handle as isize, - client_rect: Rect { - left: rect.x, - top: rect.y, - right: rect.x.saturating_add(rect.width.max(2)), - bottom: rect.y.saturating_add(rect.height.max(2)), - }, - }); - let slot = RENDER_TARGET_SURFACE.get_or_init(|| Mutex::new(None)); - if let Ok(mut current) = slot.lock() { - *current = target_surface; - } - } - - pub unsafe fn set_input_event_sender(sender: Option>) { - let slot = INPUT_EVENT_SENDER.get_or_init(|| Mutex::new(None)); - if let Ok(mut current) = slot.lock() { - *current = sender; - } - } - - pub unsafe fn set_shortcut_bindings(bindings: NativeStreamerShortcutBindings) { - let matcher = SHORTCUT_MATCHER.get_or_init(|| Mutex::new(NativeShortcutMatcher::default())); - if let Ok(mut current) = matcher.lock() { - *current = NativeShortcutMatcher::from_bindings(&bindings); - } - } - - pub unsafe fn clear_shortcut_bindings() { - let matcher = SHORTCUT_MATCHER.get_or_init(|| Mutex::new(NativeShortcutMatcher::default())); - if let Ok(mut current) = matcher.lock() { - *current = NativeShortcutMatcher::default(); - } - } - - pub unsafe fn release_current_input_capture() { - let Some(captured) = CAPTURED_HWND - .get() - .and_then(|captured| captured.lock().ok().and_then(|captured| *captured)) - else { - unregister_raw_input_devices(); - return; - }; - - release_input_capture(captured as Hwnd); - } - - pub unsafe fn protect_process_renderer_window() -> bool { - let mut state = EnumState { - process_id: GetCurrentProcessId(), - candidates: Vec::new(), - }; - EnumWindows( - Some(collect_renderer_window_candidate), - &mut state as *mut EnumState as Lparam, - ); - - let Some(candidate) = state - .candidates - .into_iter() - .max_by_key(|candidate| candidate.area) - else { - return false; - }; - - protect_renderer_window(candidate.hwnd) - } - - unsafe extern "system" fn collect_renderer_window_candidate( - hwnd: Hwnd, - lparam: Lparam, - ) -> Bool { - let state = &mut *(lparam as *mut EnumState); - let mut process_id = 0; - GetWindowThreadProcessId(hwnd, &mut process_id); - if process_id != state.process_id || IsWindowVisible(hwnd) == 0 || IsIconic(hwnd) != 0 { - return 1; - } - - if !GetWindow(hwnd, GW_OWNER).is_null() { - return 1; - } - - let ex_style = GetWindowLongPtrW(hwnd, GWL_EXSTYLE); - if (ex_style & (WS_EX_TOOLWINDOW | WS_EX_NOACTIVATE)) != 0 { - return 1; - } - - let mut rect = Rect { - left: 0, - top: 0, - right: 0, - bottom: 0, - }; - if GetWindowRect(hwnd, &mut rect) == 0 { - return 1; - } - let width = rect.right.saturating_sub(rect.left); - let height = rect.bottom.saturating_sub(rect.top); - if width < 320 || height < 180 { - return 1; - } - - state.candidates.push(WindowCandidate { - hwnd, - area: i64::from(width) * i64::from(height), - }); - 1 - } - - unsafe fn protect_renderer_window(hwnd: Hwnd) -> bool { - let protected_slot = PROTECTED_HWND.get_or_init(|| Mutex::new(None)); - if let Ok(mut protected) = protected_slot.lock() { - *protected = Some(hwnd as isize); - } - - let mut configured = false; - let current = GetWindowLongPtrW(hwnd, GWL_EXSTYLE); - let desired = current & !(WS_EX_NOACTIVATE | WS_EX_TRANSPARENT); - if desired != current { - SetWindowLongPtrW(hwnd, GWL_EXSTYLE, desired); - configured = true; - } - - let current_style = GetWindowLongPtrW(hwnd, GWL_STYLE); - let fullscreen_style = current_style - & !(WS_CAPTION | WS_THICKFRAME | WS_MINIMIZEBOX | WS_MAXIMIZEBOX | WS_SYSMENU); - if fullscreen_style != current_style { - SetWindowLongPtrW(hwnd, GWL_STYLE, fullscreen_style); - configured = true; - } - - if install_input_wndproc(hwnd) { - SetForegroundWindow(hwnd); - SetFocus(hwnd); - configured = true; - } - - if let Some(rect) = target_renderer_rect().or_else(|| monitor_rect_for_window(hwnd)) { - SetWindowPos( - hwnd, - HWND_NOTOPMOST, - rect.left, - rect.top, - rect.right.saturating_sub(rect.left).max(2), - rect.bottom.saturating_sub(rect.top).max(2), - SWP_NOACTIVATE | SWP_FRAMECHANGED, - ); - configured = true; - } else { - SetWindowPos( - hwnd, - HWND_NOTOPMOST, - 0, - 0, - 0, - 0, - SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE | SWP_FRAMECHANGED, - ); - } - - configured - } - - unsafe fn render_rect_to_screen_rect(hwnd: Hwnd, rect: Rect) -> Option { - if hwnd.is_null() { - return None; - } - let mut origin = Point { - x: rect.left, - y: rect.top, - }; - if ClientToScreen(hwnd, &mut origin) == 0 { - return None; - } - let width = rect.right.saturating_sub(rect.left).max(2); - let height = rect.bottom.saturating_sub(rect.top).max(2); - - Some(Rect { - left: origin.x, - top: origin.y, - right: origin.x.saturating_add(width), - bottom: origin.y.saturating_add(height), - }) - } - - unsafe fn install_input_wndproc(hwnd: Hwnd) -> bool { - let key = hwnd as isize; - let map = ORIGINAL_WNDPROCS.get_or_init(|| Mutex::new(HashMap::new())); - let Ok(mut map) = map.lock() else { - return false; - }; - if map.contains_key(&key) { - return false; - } - - let previous = SetWindowLongPtrW(hwnd, GWLP_WNDPROC, renderer_window_wndproc as isize); - if previous == 0 { - return false; - } - map.insert(key, previous); - true - } - - unsafe extern "system" fn renderer_window_wndproc( - hwnd: Hwnd, - message: Uint, - wparam: Wparam, - lparam: Lparam, - ) -> Lresult { - if message == WM_NCHITTEST { - return HTCLIENT; - } - if message == WM_MOUSEACTIVATE { - begin_input_capture(hwnd); - return MA_ACTIVATE; - } - if message == WM_SETCURSOR && is_input_captured(hwnd) { - SetCursor(null_mut()); - return 1; - } - if message == WM_INPUT { - handle_raw_input(lparam as Hrawinput); - return 0; - } - let handled_legacy_shortcut = is_keyboard_message(message) - && handle_legacy_shortcut_keyboard(message, wparam, lparam); - if handled_legacy_shortcut { - return 0; - } - if is_escape_keyboard_message(message, wparam) { - if !is_input_captured(hwnd) { - begin_input_capture(hwnd); - } - handle_legacy_escape_keyboard(message, lparam); - return 0; - } - if message == WM_KILLFOCUS || (message == WM_ACTIVATE && (wparam & 0xffff) == WA_INACTIVE) { - release_input_capture(hwnd); - } - if let Some((button, pressed)) = legacy_mouse_button(message, wparam) { - let was_captured = is_input_captured(hwnd); - if pressed && !was_captured { - begin_input_capture(hwnd); - emit_input_event(NativeWindowInputEvent::MouseButton { - pressed, - button, - timestamp_us: timestamp_us(), - }); - } - } - - let key = hwnd as isize; - let previous = ORIGINAL_WNDPROCS - .get() - .and_then(|map| map.lock().ok().and_then(|map| map.get(&key).copied())); - if let Some(previous) = previous { - return CallWindowProcW(previous, hwnd, message, wparam, lparam); - } - - DefWindowProcW(hwnd, message, wparam, lparam) - } - - unsafe fn monitor_rect_for_window(hwnd: Hwnd) -> Option { - let monitor = MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST); - if monitor.is_null() { - return None; - } - - let mut info = MonitorInfo { - cb_size: std::mem::size_of::() as Dword, - rc_monitor: Rect { - left: 0, - top: 0, - right: 0, - bottom: 0, - }, - rc_work: Rect { - left: 0, - top: 0, - right: 0, - bottom: 0, - }, - dw_flags: 0, - }; - if GetMonitorInfoW(monitor, &mut info) == 0 { - return None; - } - - Some(info.rc_monitor) - } - - unsafe fn target_renderer_rect() -> Option { - let target = RENDER_TARGET_SURFACE - .get() - .and_then(|surface| surface.lock().ok().and_then(|surface| *surface))?; - let hwnd = target.hwnd as Hwnd; - render_rect_to_screen_rect(hwnd, target.client_rect) - .or_else(|| monitor_rect_for_window(hwnd)) - } - - unsafe fn begin_input_capture(hwnd: Hwnd) { - SetForegroundWindow(hwnd); - SetFocus(hwnd); - SetCapture(hwnd); - register_raw_input_devices(hwnd); - if let Some(rect) = target_renderer_rect().or_else(|| monitor_rect_for_window(hwnd)) { - ClipCursor(&rect); - } - hide_cursor(); - emit_input_capture_changed(true); - - let slot = CAPTURED_HWND.get_or_init(|| Mutex::new(None)); - if let Ok(mut captured) = slot.lock() { - *captured = Some(hwnd as isize); - } - sync_lock_keys_state(true); - } - - unsafe fn release_input_capture(hwnd: Hwnd) { - cancel_escape_hold_to_minimize_timer(); - clear_escape_key_press(); - let slot = CAPTURED_HWND.get_or_init(|| Mutex::new(None)); - let mut should_release = false; - if let Ok(mut captured) = slot.lock() { - should_release = captured.is_some_and(|captured| captured == hwnd as isize); - if should_release { - *captured = None; - } - } - - if !should_release { - return; - } - - release_pressed_keys(); - ReleaseCapture(); - ClipCursor(null()); - show_cursor(); - emit_input_capture_changed(false); - unregister_raw_input_devices(); - SetWindowPos( - hwnd, - HWND_NOTOPMOST, - 0, - 0, - 0, - 0, - SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE, - ); - } - - fn is_input_captured(hwnd: Hwnd) -> bool { - CAPTURED_HWND - .get() - .and_then(|captured| captured.lock().ok().and_then(|captured| *captured)) - .is_some_and(|captured| captured == hwnd as isize) - } - - fn captured_hwnd() -> Option { - CAPTURED_HWND - .get() - .and_then(|captured| captured.lock().ok().and_then(|captured| *captured)) - } - - fn protected_hwnd() -> Option { - PROTECTED_HWND - .get() - .and_then(|captured| captured.lock().ok().and_then(|captured| *captured)) - } - - unsafe fn start_escape_hold_to_minimize_timer() { - let Some(hwnd) = captured_hwnd() else { - return; - }; - - let token = ESCAPE_HOLD_TOKEN - .get_or_init(|| AtomicU64::new(0)) - .fetch_add(1, Ordering::SeqCst) - .wrapping_add(1); - let slot = ESCAPE_HOLD_HWND.get_or_init(|| Mutex::new(None)); - if let Ok(mut held_hwnd) = slot.lock() { - *held_hwnd = Some(hwnd); - } - - thread::spawn(move || { - thread::sleep(ESCAPE_HOLD_TO_MINIMIZE); - unsafe { - minimize_window_if_escape_still_held(hwnd, token); - } - }); - } - - fn cancel_escape_hold_to_minimize_timer() { - ESCAPE_HOLD_TOKEN - .get_or_init(|| AtomicU64::new(0)) - .fetch_add(1, Ordering::SeqCst); - let slot = ESCAPE_HOLD_HWND.get_or_init(|| Mutex::new(None)); - if let Ok(mut held_hwnd) = slot.lock() { - *held_hwnd = None; - } - } - - unsafe fn minimize_window_if_escape_still_held(hwnd: isize, token: u64) { - let current_token = ESCAPE_HOLD_TOKEN - .get_or_init(|| AtomicU64::new(0)) - .load(Ordering::SeqCst); - if current_token != token { - return; - } - - let still_held = ESCAPE_HOLD_HWND - .get() - .and_then(|held_hwnd| held_hwnd.lock().ok().and_then(|held_hwnd| *held_hwnd)) - .is_some_and(|held_hwnd| held_hwnd == hwnd); - if !still_held { - return; - } - - let hwnd = hwnd as Hwnd; - release_input_capture(hwnd); - ShowWindow(hwnd, SW_MINIMIZE); - } - - unsafe fn register_raw_input_devices(hwnd: Hwnd) -> bool { - let devices = [ - RawInputDevice { - us_usage_page: 0x01, - us_usage: 0x02, - dw_flags: RIDEV_NOLEGACY | RIDEV_CAPTUREMOUSE, - hwnd_target: hwnd, - }, - RawInputDevice { - us_usage_page: 0x01, - us_usage: 0x06, - dw_flags: RIDEV_NOLEGACY, - hwnd_target: hwnd, - }, - ]; - - RegisterRawInputDevices( - devices.as_ptr(), - devices.len() as u32, - std::mem::size_of::() as u32, - ) != 0 - } - - unsafe fn unregister_raw_input_devices() -> bool { - let devices = [ - RawInputDevice { - us_usage_page: 0x01, - us_usage: 0x02, - dw_flags: RIDEV_REMOVE, - hwnd_target: null_mut(), - }, - RawInputDevice { - us_usage_page: 0x01, - us_usage: 0x06, - dw_flags: RIDEV_REMOVE, - hwnd_target: null_mut(), - }, - ]; - - RegisterRawInputDevices( - devices.as_ptr(), - devices.len() as u32, - std::mem::size_of::() as u32, - ) != 0 - } - - unsafe fn handle_raw_input(raw_input: Hrawinput) { - let mut size = 0u32; - let header_size = std::mem::size_of::() as u32; - let query = GetRawInputData(raw_input, RID_INPUT, null_mut(), &mut size, header_size); - if query == u32::MAX || size < header_size { - return; - } - - let mut buffer = vec![0u8; size as usize]; - let read = GetRawInputData( - raw_input, - RID_INPUT, - buffer.as_mut_ptr() as *mut c_void, - &mut size, - header_size, - ); - if read == u32::MAX || read == 0 || buffer.len() < header_size as usize { - return; - } - - let header = &*(buffer.as_ptr() as *const RawInputHeader); - let data = buffer.as_ptr().add(std::mem::size_of::()); - match header.dw_type { - RIM_TYPEMOUSE => handle_raw_mouse(&*(data as *const RawMouse)), - RIM_TYPEKEYBOARD => handle_raw_keyboard(&*(data as *const RawKeyboard)), - _ => {} - } - } - - unsafe fn handle_raw_mouse(raw: &RawMouse) { - if CAPTURED_HWND - .get() - .and_then(|captured| captured.lock().ok().and_then(|captured| *captured)) - .is_none() - { - return; - } - - let timestamp_us = timestamp_us(); - let dx = clamp_i32_to_i16(raw.l_last_x); - let dy = clamp_i32_to_i16(raw.l_last_y); - if dx != 0 || dy != 0 { - emit_input_event(NativeWindowInputEvent::MouseMove { - dx, - dy, - timestamp_us, - }); - } - - let button_flags = (raw.buttons & 0xffff) as u16; - let button_data = (raw.buttons >> 16) as u16; - emit_raw_mouse_button_events(button_flags, timestamp_us); - if (button_flags & RI_MOUSE_WHEEL) != 0 { - emit_input_event(NativeWindowInputEvent::MouseWheel { - delta: button_data as i16, - timestamp_us, - }); - } - } - - unsafe fn emit_raw_mouse_button_events(flags: u16, timestamp_us: u64) { - let pairs = [ - (RI_MOUSE_LEFT_BUTTON_DOWN, 1, true), - (RI_MOUSE_LEFT_BUTTON_UP, 1, false), - (RI_MOUSE_MIDDLE_BUTTON_DOWN, 2, true), - (RI_MOUSE_MIDDLE_BUTTON_UP, 2, false), - (RI_MOUSE_RIGHT_BUTTON_DOWN, 3, true), - (RI_MOUSE_RIGHT_BUTTON_UP, 3, false), - (RI_MOUSE_BUTTON_4_DOWN, 4, true), - (RI_MOUSE_BUTTON_4_UP, 4, false), - (RI_MOUSE_BUTTON_5_DOWN, 5, true), - (RI_MOUSE_BUTTON_5_UP, 5, false), - ]; - - for (flag, button, pressed) in pairs { - if (flags & flag) != 0 { - emit_input_event(NativeWindowInputEvent::MouseButton { - pressed, - button, - timestamp_us, - }); - } - } - } - - unsafe fn handle_raw_keyboard(raw: &RawKeyboard) { - if raw.vkey == 0xff { - return; - } - - let pressed = match raw.message { - WM_KEYDOWN | WM_SYSKEYDOWN => true, - WM_KEYUP | WM_SYSKEYUP => false, - _ => (raw.flags & RI_KEY_BREAK) == 0, - }; - let keycode = normalize_virtual_key(raw.vkey, raw.make_code, raw.flags); - let mut scancode = normalize_scancode(raw.make_code, raw.flags); - if keycode == VK_ESCAPE && scancode == 0 { - scancode = ESCAPE_SCANCODE; - } - if keycode == 0 || scancode == 0 { - return; - } - handle_keyboard_state(keycode, scancode, pressed); - } - - unsafe fn handle_legacy_escape_keyboard(message: Uint, lparam: Lparam) { - let pressed = matches!(message, WM_KEYDOWN | WM_SYSKEYDOWN); - let mut scancode = legacy_keyboard_scancode(lparam); - if scancode == 0 { - scancode = ESCAPE_SCANCODE; - } - handle_keyboard_state(VK_ESCAPE, scancode, pressed); - } - - fn is_escape_keyboard_message(message: Uint, wparam: Wparam) -> bool { - matches!(message, WM_KEYDOWN | WM_KEYUP | WM_SYSKEYDOWN | WM_SYSKEYUP) - && (wparam as u16) == VK_ESCAPE - } - - fn is_keyboard_message(message: Uint) -> bool { - matches!(message, WM_KEYDOWN | WM_KEYUP | WM_SYSKEYDOWN | WM_SYSKEYUP) - } - - fn legacy_keyboard_scancode(lparam: Lparam) -> u16 { - let scancode = ((lparam >> 16) & 0xff) as u16; - if scancode == 0 { - return 0; - } - if ((lparam >> 24) & 0x01) != 0 { - 0xe000 | scancode - } else { - scancode - } - } - - unsafe fn handle_legacy_shortcut_keyboard( - message: Uint, - wparam: Wparam, - lparam: Lparam, - ) -> bool { - let keycode = wparam as u16; - if keycode == VK_ESCAPE { - return false; - } - - let pressed = matches!(message, WM_KEYDOWN | WM_SYSKEYDOWN); - let scancode = legacy_keyboard_scancode(lparam); - let key_id = if scancode == 0 { keycode } else { scancode }; - let suppressed_keys = LEGACY_SUPPRESSED_KEYS.get_or_init(|| Mutex::new(HashSet::new())); - let Ok(mut suppressed_keys) = suppressed_keys.lock() else { - return false; - }; - - if !pressed { - return suppressed_keys.remove(&key_id); - } - - if suppressed_keys.contains(&key_id) { - return true; - } - - let modifiers = current_legacy_modifier_flags(); - if pressed && is_clipboard_paste_shortcut(keycode, modifiers) { - suppressed_keys.insert(key_id); - drop(suppressed_keys); - emit_clipboard_paste_request(); - return true; - } - - let Some(action) = shortcut_action_for_keypress(keycode, scancode, modifiers) else { - return false; - }; - - suppressed_keys.insert(key_id); - drop(suppressed_keys); - handle_shortcut_action(action); - true - } - - unsafe fn handle_keyboard_state(keycode: u16, scancode: u16, pressed: bool) { - sync_lock_keys_state(false); - - let keys = PRESSED_KEYS.get_or_init(|| Mutex::new(HashMap::new())); - let Ok(mut keys) = keys.lock() else { - return; - }; - if pressed && keycode == VK_TAB && is_alt_modifier_down(&keys) { - drop(keys); - release_current_input_capture(); - return; - } - if keycode == VK_ESCAPE { - drop(keys); - handle_escape_keyboard_state(scancode, pressed); - return; - } - let previous = keys.get(&scancode).copied(); - if pressed { - if previous.is_some() { - return; - } - let modifiers = pressed_key_modifier_flags(&keys, keycode); - if is_clipboard_paste_shortcut(keycode, modifiers) { - keys.insert( - scancode, - PressedKey { - keycode, - scancode, - suppressed: true, - }, - ); - drop(keys); - emit_clipboard_paste_request(); - return; - } - if let Some(action) = shortcut_action_for_keypress(keycode, scancode, modifiers) { - keys.insert( - scancode, - PressedKey { - keycode, - scancode, - suppressed: true, - }, - ); - drop(keys); - handle_shortcut_action(action); - return; - } - keys.insert( - scancode, - PressedKey { - keycode, - scancode, - suppressed: false, - }, - ); - } else if let Some(previous) = keys.remove(&scancode) { - if previous.suppressed { - return; - } - } - let modifiers = pressed_key_modifier_flags(&keys, keycode); - drop(keys); - - emit_input_event(NativeWindowInputEvent::Key { - pressed, - keycode, - scancode, - modifiers, - timestamp_us: timestamp_us(), - }); - } - - unsafe fn handle_escape_keyboard_state(scancode: u16, pressed: bool) { - let slot = ESCAPE_KEY_PRESS.get_or_init(|| Mutex::new(None)); - let Ok(mut escape_press) = slot.lock() else { - return; - }; - - if pressed { - let should_start_hold_timer = if let Some(current) = escape_press.as_mut() { - let should_start = !current.hold_timer_armed && captured_hwnd().is_some(); - if should_start { - current.hold_timer_armed = true; - } - should_start - } else { - let hold_timer_armed = captured_hwnd().is_some(); - *escape_press = Some(EscapeKeyPress { - scancode, - hold_timer_armed, - }); - hold_timer_armed - }; - drop(escape_press); - if should_start_hold_timer { - start_escape_hold_to_minimize_timer(); - } - return; - } - - let Some(escape_press) = escape_press.take() else { - cancel_escape_hold_to_minimize_timer(); - return; - }; - let scancode = escape_press.scancode; - - cancel_escape_hold_to_minimize_timer(); - send_escape_tap(scancode); - } - - fn clear_escape_key_press() { - let slot = ESCAPE_KEY_PRESS.get_or_init(|| Mutex::new(None)); - if let Ok(mut escape_press) = slot.lock() { - *escape_press = None; - } - } - - fn send_escape_tap(scancode: u16) { - let keydown_timestamp_us = timestamp_us(); - emit_input_event(NativeWindowInputEvent::Key { - pressed: true, - keycode: VK_ESCAPE, - scancode, - modifiers: 0, - timestamp_us: keydown_timestamp_us, - }); - emit_input_event(NativeWindowInputEvent::Key { - pressed: false, - keycode: VK_ESCAPE, - scancode, - modifiers: 0, - timestamp_us: timestamp_us(), - }); - } - - unsafe fn release_pressed_keys() { - let keys = PRESSED_KEYS.get_or_init(|| Mutex::new(HashMap::new())); - let Ok(mut keys) = keys.lock() else { - return; - }; - let pressed = keys.values().copied().collect::>(); - keys.clear(); - drop(keys); - - let timestamp_us = timestamp_us(); - for key in pressed { - if key.suppressed { - continue; - } - emit_input_event(NativeWindowInputEvent::Key { - pressed: false, - keycode: key.keycode, - scancode: key.scancode, - modifiers: 0, - timestamp_us, - }); - } - } - - fn normalize_virtual_key(vkey: u16, make_code: u16, flags: u16) -> u16 { - match vkey { - VK_SHIFT => match make_code { - 0x36 => VK_RSHIFT, - _ => VK_LSHIFT, - }, - VK_CONTROL => { - if (flags & RI_KEY_E0) != 0 { - VK_RCONTROL - } else { - VK_LCONTROL - } - } - VK_MENU => { - if (flags & RI_KEY_E0) != 0 { - VK_RMENU - } else { - VK_LMENU - } - } - _ => vkey, - } - } - - fn normalize_scancode(make_code: u16, flags: u16) -> u16 { - if make_code == 0 { - return 0; - } - if (flags & RI_KEY_E0) != 0 { - 0xe000 | make_code - } else if (flags & RI_KEY_E1) != 0 { - 0xe100 | make_code - } else { - make_code - } - } - - /// Lock-key bitmask for INPUT_LOCK_KEYS_SYNC (official GFN iS() on desktop Windows). - unsafe fn lock_keys_sync_state() -> u8 { - let mut state = 0x10; - if (GetKeyState(VK_CAPITAL) & 0x0001) != 0 { - state |= 0x01; - } - state |= 0x20; - state |= 0x40; - if (GetKeyState(VK_NUMLOCK) & 0x0001) != 0 { - state |= 0x02; - } - if (GetKeyState(VK_SCROLL) & 0x0001) != 0 { - state |= 0x04; - } - state - } - - unsafe fn sync_lock_keys_state(force: bool) { - let state = lock_keys_sync_state(); - let slot = LAST_LOCK_KEYS_STATE.get_or_init(|| Mutex::new(0)); - let Ok(mut last) = slot.lock() else { - return; - }; - if !force && *last == state { - return; - } - *last = state; - drop(last); - emit_input_event(NativeWindowInputEvent::LockKeysSync { state }); - } - - /// Per-key modifier byte from tracked pressed keys (official GFN yS()/Cb()). - /// Lock keys sync separately via INPUT_LOCK_KEYS_SYNC, not here. - unsafe fn pressed_key_modifier_flags(keys: &HashMap, active_keycode: u16) -> u16 { - let mut modifiers = 0u16; - let mut shift_tracked = false; - let mut control_tracked = false; - let mut alt_tracked = false; - let mut win_tracked = false; - - for key in keys.values() { - if key.keycode == active_keycode { - continue; - } - match key.keycode { - VK_LSHIFT | VK_RSHIFT | VK_SHIFT => { - shift_tracked = true; - modifiers |= 0x01; - } - VK_LCONTROL | VK_RCONTROL | VK_CONTROL => { - control_tracked = true; - modifiers |= 0x02; - } - VK_LMENU | VK_RMENU | VK_MENU => { - alt_tracked = true; - modifiers |= 0x04; - } - VK_LWIN | VK_RWIN => { - win_tracked = true; - modifiers |= 0x08; - } - _ => {} - } - } - - if !matches!(active_keycode, VK_LSHIFT | VK_RSHIFT | VK_SHIFT) - && !shift_tracked - && (is_key_down(VK_SHIFT) || is_key_down(VK_LSHIFT) || is_key_down(VK_RSHIFT)) - { - modifiers |= 0x01; - } - if !matches!(active_keycode, VK_LCONTROL | VK_RCONTROL | VK_CONTROL) - && !control_tracked - && (is_key_down(VK_CONTROL) || is_key_down(VK_LCONTROL) || is_key_down(VK_RCONTROL)) - { - modifiers |= 0x02; - } - if !matches!(active_keycode, VK_LMENU | VK_RMENU | VK_MENU) - && !alt_tracked - && (is_key_down(VK_MENU) || is_key_down(VK_LMENU) || is_key_down(VK_RMENU)) - { - modifiers |= 0x04; - } - if !matches!(active_keycode, VK_LWIN | VK_RWIN) - && !win_tracked - && (is_key_down(VK_LWIN) || is_key_down(VK_RWIN)) - { - modifiers |= 0x08; - } - - modifiers - } - - /// Legacy fallback for shortcut matching before a key enters the pressed-key map. - unsafe fn keyboard_modifier_flags(active_keycode: u16) -> u16 { - let keys = PRESSED_KEYS.get_or_init(|| Mutex::new(HashMap::new())); - if let Ok(keys) = keys.lock() { - if !keys.is_empty() { - return pressed_key_modifier_flags(&keys, active_keycode); - } - } - - let mut modifiers = 0u16; - if !matches!(active_keycode, VK_LSHIFT | VK_RSHIFT | VK_SHIFT) - && (is_key_down(VK_SHIFT) || is_key_down(VK_LSHIFT) || is_key_down(VK_RSHIFT)) - { - modifiers |= 0x01; - } - if !matches!(active_keycode, VK_LCONTROL | VK_RCONTROL | VK_CONTROL) - && (is_key_down(VK_CONTROL) || is_key_down(VK_LCONTROL) || is_key_down(VK_RCONTROL)) - { - modifiers |= 0x02; - } - if !matches!(active_keycode, VK_LMENU | VK_RMENU | VK_MENU) - && (is_key_down(VK_MENU) || is_key_down(VK_LMENU) || is_key_down(VK_RMENU)) - { - modifiers |= 0x04; - } - if !matches!(active_keycode, VK_LWIN | VK_RWIN) - && (is_key_down(VK_LWIN) || is_key_down(VK_RWIN)) - { - modifiers |= 0x08; - } - modifiers - } - - unsafe fn current_legacy_modifier_flags() -> u16 { - let mut modifiers = 0u16; - if is_key_down(VK_SHIFT) || is_key_down(VK_LSHIFT) || is_key_down(VK_RSHIFT) { - modifiers |= 0x01; - } - if is_key_down(VK_CONTROL) || is_key_down(VK_LCONTROL) || is_key_down(VK_RCONTROL) { - modifiers |= 0x02; - } - if is_key_down(VK_MENU) || is_key_down(VK_LMENU) || is_key_down(VK_RMENU) { - modifiers |= 0x04; - } - if is_key_down(VK_LWIN) || is_key_down(VK_RWIN) { - modifiers |= 0x08; - } - if (GetKeyState(VK_CAPITAL) & 0x0001) != 0 { - modifiers |= 0x10; - } - if (GetKeyState(VK_NUMLOCK) & 0x0001) != 0 { - modifiers |= 0x20; - } - modifiers - } - - unsafe fn is_key_down(keycode: u16) -> bool { - ((GetKeyState(keycode as i32) as u16) & 0x8000) != 0 - } - - unsafe fn is_alt_modifier_down(keys: &HashMap) -> bool { - keys.values() - .any(|key| matches!(key.keycode, VK_LMENU | VK_RMENU | VK_MENU)) - || ((GetKeyState(VK_MENU as i32) as u16) & 0x8000) != 0 - } - - fn shortcut_action_for_keypress( - keycode: u16, - scancode: u16, - modifiers: u16, - ) -> Option { - SHORTCUT_MATCHER - .get() - .and_then(|matcher| matcher.lock().ok()) - .and_then(|matcher| matcher.match_keydown(keycode, scancode, modifiers)) - } - - unsafe fn handle_shortcut_action(action: NativeStreamerShortcutAction) { - match action { - NativeStreamerShortcutAction::TogglePointerLock => { - if let Some(hwnd) = captured_hwnd().or_else(protected_hwnd) { - let hwnd = hwnd as Hwnd; - if is_input_captured(hwnd) { - release_input_capture(hwnd); - } else { - begin_input_capture(hwnd); - } - } - } - _ => { - if shortcut_action_releases_input_capture(action) { - release_current_input_capture(); - } - emit_input_event(NativeWindowInputEvent::Shortcut { action }); - } - } - } - - fn shortcut_action_releases_input_capture(action: NativeStreamerShortcutAction) -> bool { - matches!( - action, - NativeStreamerShortcutAction::ToggleFullscreen - | NativeStreamerShortcutAction::StopStream - ) - } - - fn legacy_mouse_button(message: Uint, wparam: Wparam) -> Option<(u8, bool)> { - match message { - WM_LBUTTONDOWN => Some((1, true)), - WM_LBUTTONUP => Some((1, false)), - WM_MBUTTONDOWN => Some((2, true)), - WM_MBUTTONUP => Some((2, false)), - WM_RBUTTONDOWN => Some((3, true)), - WM_RBUTTONUP => Some((3, false)), - WM_XBUTTONDOWN | WM_XBUTTONUP => { - let xbutton = ((wparam >> 16) & 0xffff) as u16; - let button = match xbutton { - XBUTTON1 => 4, - XBUTTON2 => 5, - _ => return None, - }; - Some((button, message == WM_XBUTTONDOWN)) - } - _ => None, - } - } - - fn emit_input_event(event: NativeWindowInputEvent) { - let Some(sender) = INPUT_EVENT_SENDER - .get() - .and_then(|sender| sender.lock().ok().and_then(|sender| sender.clone())) - else { - return; - }; - let _ = sender.send(event); - } - - fn is_clipboard_paste_shortcut(keycode: u16, modifiers: u16) -> bool { - keycode == VK_V - && ((modifiers & 0x02) != 0 || unsafe { is_ctrl_modifier_down() }) - && (modifiers & 0x04) == 0 - } - - unsafe fn is_ctrl_modifier_down() -> bool { - is_key_down(VK_CONTROL) || is_key_down(VK_LCONTROL) || is_key_down(VK_RCONTROL) - } - - fn emit_clipboard_paste_request() { - let Some(sender) = INPUT_EVENT_SENDER - .get() - .and_then(|sender| sender.lock().ok().and_then(|sender| sender.clone())) - else { - return; - }; - let _ = sender.send(NativeWindowInputEvent::ClipboardPaste); - } - - fn emit_input_capture_changed(captured: bool) { - let Some(sender) = INPUT_EVENT_SENDER - .get() - .and_then(|sender| sender.lock().ok().and_then(|sender| sender.clone())) - else { - return; - }; - let _ = sender.send(NativeWindowInputEvent::InputCaptureChanged { captured }); - } - - fn clamp_i32_to_i16(value: i32) -> i16 { - value.clamp(i16::MIN as i32, i16::MAX as i32) as i16 - } - - fn timestamp_us() -> u64 { - STARTED_AT - .get_or_init(Instant::now) - .elapsed() - .as_micros() - .min(u128::from(u64::MAX)) as u64 - } - - unsafe fn hide_cursor() { - while ShowCursor(0) >= 0 {} - } - - unsafe fn show_cursor() { - while ShowCursor(1) < 0 {} - } -} - -#[cfg(target_os = "windows")] -pub(crate) fn apply_render_surface_to_video_sink( - sink: &gst::Element, - surface: &NativeRenderSurface, -) -> Result<(), String> { - let Some(window_handle) = surface.window_handle.as_deref() else { - return Ok(()); - }; - - let handle = parse_window_handle(window_handle)?; - let overlay = sink - .clone() - .dynamic_cast::() - .map_err(|_| { - format!( - "Native render sink {} does not implement GstVideoOverlay.", - sink.name() - ) - })?; - let rect = normalized_render_rect(surface.visible.then_some(()).and(surface.rect.as_ref())); - - unsafe { - overlay.set_window_handle(handle); - } - overlay.handle_events(false); - overlay - .set_render_rectangle(rect.x, rect.y, rect.width, rect.height) - .map_err(|error| format!("Failed to set native render rectangle: {error}"))?; - overlay.expose(); - Ok(()) -} - -#[cfg(not(target_os = "windows"))] -pub(crate) fn apply_render_surface_to_video_sink( - _sink: &gst::Element, - _surface: &NativeRenderSurface, -) -> Result<(), String> { - Ok(()) -} - -#[cfg(target_os = "windows")] -pub(crate) fn primary_display_refresh_hz() -> Option { - const VREFRESH: i32 = 116; - - #[link(name = "user32")] - extern "system" { - fn GetDC(hwnd: *mut c_void) -> *mut c_void; - fn ReleaseDC(hwnd: *mut c_void, hdc: *mut c_void) -> i32; - } - - #[link(name = "gdi32")] - extern "system" { - fn GetDeviceCaps(hdc: *mut c_void, index: i32) -> i32; - } - - let hdc = unsafe { GetDC(std::ptr::null_mut()) }; - if hdc.is_null() { - return None; - } - - let refresh = unsafe { GetDeviceCaps(hdc, VREFRESH) }; - unsafe { - ReleaseDC(std::ptr::null_mut(), hdc); - } - - (refresh > 1).then_some(refresh as u32) -} - -#[cfg(not(target_os = "windows"))] -pub(crate) fn primary_display_refresh_hz() -> Option { - None -} +#[cfg(target_os = "windows")] +use crate::gstreamer_backend::send_log; +#[cfg(target_os = "windows")] +use crate::protocol::NativeRenderRect; +use crate::protocol::{Event, NativeRenderSurface, NativeStreamerShortcutBindings}; +#[cfg(target_os = "windows")] +use std::ffi::c_void; +use std::sync::atomic::AtomicBool; +#[cfg(target_os = "windows")] +use std::sync::atomic::Ordering; +use std::sync::mpsc::Sender; +use std::sync::Arc; +#[cfg(target_os = "windows")] +use std::thread; +#[cfg(target_os = "windows")] +use std::time::Duration; + +#[cfg(target_os = "windows")] +fn parse_window_handle(value: &str) -> Result { + let trimmed = value.trim(); + let hex = trimmed + .strip_prefix("0x") + .or_else(|| trimmed.strip_prefix("0X")); + let parsed = if let Some(hex) = hex { + usize::from_str_radix(hex, 16) + } else { + trimmed.parse::() + } + .map_err(|error| format!("Invalid native render window handle {value:?}: {error}"))?; + + if parsed == 0 { + return Err("Native render window handle is zero.".to_owned()); + } + + Ok(parsed) +} + +#[cfg(target_os = "windows")] +fn normalized_render_rect(rect: Option<&NativeRenderRect>) -> NativeRenderRect { + let Some(rect) = rect else { + return NativeRenderRect { + x: 0, + y: 0, + width: 2, + height: 2, + }; + }; + + NativeRenderRect { + x: rect.x.max(0), + y: rect.y.max(0), + width: rect.width.max(2), + height: rect.height.max(2), + } +} + +#[cfg(target_os = "windows")] +pub(crate) fn start_external_renderer_window_guard( + event_sender: Option>, + stop: Arc, +) { + thread::spawn(move || { + let mut logged = false; + while !stop.load(Ordering::SeqCst) { + if stop.load(Ordering::SeqCst) { + break; + } + + let configured = unsafe { win32_renderer_window::protect_process_renderer_window() }; + if configured && !logged { + send_log( + &event_sender, + "info", + "Configured external native renderer window for fullscreen DX11 input capture." + .to_owned(), + ); + logged = true; + } + thread::sleep(if logged { + Duration::from_millis(500) + } else { + Duration::from_millis(100) + }); + } + }); +} + +#[cfg(not(target_os = "windows"))] +pub(crate) fn start_external_renderer_window_guard( + _event_sender: Option>, + _stop: Arc, +) { +} + +#[cfg(target_os = "windows")] +pub(crate) fn set_native_shortcut_bindings(bindings: &NativeStreamerShortcutBindings) { + unsafe { + win32_renderer_window::set_shortcut_bindings(bindings.clone()); + } +} + +#[cfg(not(target_os = "windows"))] +pub(crate) fn set_native_shortcut_bindings(_bindings: &NativeStreamerShortcutBindings) {} + +#[cfg(target_os = "windows")] +pub(crate) fn clear_native_shortcut_bindings() { + unsafe { + win32_renderer_window::clear_shortcut_bindings(); + } +} + +#[cfg(not(target_os = "windows"))] +pub(crate) fn clear_native_shortcut_bindings() {} + +#[cfg(target_os = "windows")] +pub(crate) fn release_native_input_capture() { + unsafe { + win32_renderer_window::release_current_input_capture(); + } +} + +#[cfg(not(target_os = "windows"))] +pub(crate) fn release_native_input_capture() {} + +#[cfg(target_os = "windows")] +pub(crate) fn arm_internal_child_input(hwnd: usize) -> bool { + unsafe { win32_renderer_window::arm_internal_child_input(hwnd) } +} + +#[cfg(target_os = "windows")] +pub(crate) fn update_external_renderer_surface(surface: &NativeRenderSurface) { + let target = surface + .window_handle + .as_deref() + .and_then(|window_handle| parse_window_handle(window_handle).ok()) + .and_then(|window_handle| { + surface + .visible + .then_some(()) + .and(surface.rect.as_ref()) + .map(|rect| (window_handle, normalized_render_rect(Some(rect)))) + }); + + unsafe { + win32_renderer_window::set_render_target_surface(target); + } +} + +#[cfg(not(target_os = "windows"))] +pub(crate) fn update_external_renderer_surface(_surface: &NativeRenderSurface) {} + +#[cfg(target_os = "windows")] +pub(crate) mod win32_renderer_window { + use crate::gstreamer_input::NativeWindowInputEvent; + use crate::protocol::NativeRenderRect; + use crate::protocol::{NativeStreamerShortcutAction, NativeStreamerShortcutBindings}; + use crate::shortcuts::NativeShortcutMatcher; + use std::collections::{HashMap, HashSet}; + use std::ffi::c_void; + use std::ptr::{null, null_mut}; + use std::sync::atomic::{AtomicU64, Ordering}; + use std::sync::mpsc::Sender; + use std::sync::{Mutex, OnceLock}; + use std::thread; + use std::time::{Duration, Instant}; + + type Bool = i32; + type Dword = u32; + type Hcursor = *mut c_void; + type Hmonitor = *mut c_void; + type Hrawinput = *mut c_void; + type Hwnd = *mut c_void; + type Lparam = isize; + type Lresult = isize; + type Uint = u32; + type Wparam = usize; + + const GWL_STYLE: i32 = -16; + const GWL_EXSTYLE: i32 = -20; + const GWLP_WNDPROC: i32 = -4; + const GW_OWNER: Uint = 4; + const HTCLIENT: isize = 1; + const HWND_NOTOPMOST: Hwnd = -2isize as Hwnd; + const MA_ACTIVATE: isize = 1; + const MONITOR_DEFAULTTONEAREST: Dword = 0x0000_0002; + const RID_INPUT: Uint = 0x1000_0003; + const RIDEV_REMOVE: Dword = 0x0000_0001; + const RIDEV_NOLEGACY: Dword = 0x0000_0030; + // Receive WM_INPUT even when this HWND is not foreground. Required for the + // internal child surface: Electron stays the top-level foreground window, + // so keyboard RawInput never arrives without INPUTSINK. Mouse still works + // via RIDEV_CAPTUREMOUSE alone. + const RIDEV_INPUTSINK: Dword = 0x0000_0100; + const RIDEV_CAPTUREMOUSE: Dword = 0x0000_0200; + const RIM_TYPEMOUSE: Dword = 0; + const RIM_TYPEKEYBOARD: Dword = 1; + const RI_KEY_BREAK: u16 = 0x0001; + const RI_KEY_E0: u16 = 0x0002; + const RI_KEY_E1: u16 = 0x0004; + const RI_MOUSE_LEFT_BUTTON_DOWN: u16 = 0x0001; + const RI_MOUSE_LEFT_BUTTON_UP: u16 = 0x0002; + const RI_MOUSE_RIGHT_BUTTON_DOWN: u16 = 0x0004; + const RI_MOUSE_RIGHT_BUTTON_UP: u16 = 0x0008; + const RI_MOUSE_MIDDLE_BUTTON_DOWN: u16 = 0x0010; + const RI_MOUSE_MIDDLE_BUTTON_UP: u16 = 0x0020; + const RI_MOUSE_BUTTON_4_DOWN: u16 = 0x0040; + const RI_MOUSE_BUTTON_4_UP: u16 = 0x0080; + const RI_MOUSE_BUTTON_5_DOWN: u16 = 0x0100; + const RI_MOUSE_BUTTON_5_UP: u16 = 0x0200; + const RI_MOUSE_WHEEL: u16 = 0x0400; + const VK_SHIFT: u16 = 0x10; + const VK_ESCAPE: u16 = 0x1B; + const VK_V: u16 = 0x56; + const VK_TAB: u16 = 0x09; + const VK_CONTROL: u16 = 0x11; + const VK_MENU: u16 = 0x12; + const VK_CAPITAL: i32 = 0x14; + const VK_NUMLOCK: i32 = 0x90; + const VK_SCROLL: i32 = 0x91; + const VK_LSHIFT: u16 = 0xA0; + const VK_RSHIFT: u16 = 0xA1; + const VK_LCONTROL: u16 = 0xA2; + const VK_RCONTROL: u16 = 0xA3; + const VK_LMENU: u16 = 0xA4; + const VK_RMENU: u16 = 0xA5; + const VK_LWIN: u16 = 0x5B; + const VK_RWIN: u16 = 0x5C; + const WM_INPUT: Uint = 0x00FF; + const WM_NCHITTEST: Uint = 0x0084; + const WM_MOUSEACTIVATE: Uint = 0x0021; + const WM_SETCURSOR: Uint = 0x0020; + const WM_KILLFOCUS: Uint = 0x0008; + const WM_ACTIVATE: Uint = 0x0006; + const WA_INACTIVE: usize = 0; + const WM_KEYDOWN: Uint = 0x0100; + const WM_KEYUP: Uint = 0x0101; + const WM_SYSKEYDOWN: Uint = 0x0104; + const WM_SYSKEYUP: Uint = 0x0105; + const WM_LBUTTONDOWN: Uint = 0x0201; + const WM_LBUTTONUP: Uint = 0x0202; + const WM_RBUTTONDOWN: Uint = 0x0204; + const WM_RBUTTONUP: Uint = 0x0205; + const WM_MBUTTONDOWN: Uint = 0x0207; + const WM_MBUTTONUP: Uint = 0x0208; + const WM_XBUTTONDOWN: Uint = 0x020B; + const WM_XBUTTONUP: Uint = 0x020C; + const XBUTTON1: u16 = 0x0001; + const XBUTTON2: u16 = 0x0002; + const WS_CAPTION: isize = 0x00C0_0000; + const WS_MAXIMIZEBOX: isize = 0x0001_0000; + const WS_MINIMIZEBOX: isize = 0x0002_0000; + const WS_SYSMENU: isize = 0x0008_0000; + const WS_THICKFRAME: isize = 0x0004_0000; + const WS_EX_NOACTIVATE: isize = 0x0800_0000; + const WS_EX_TOOLWINDOW: isize = 0x0000_0080; + const WS_EX_TRANSPARENT: isize = 0x0000_0020; + const SWP_NOSIZE: u32 = 0x0001; + const SWP_NOMOVE: u32 = 0x0002; + const SWP_NOACTIVATE: u32 = 0x0010; + const SWP_FRAMECHANGED: u32 = 0x0020; + const SW_MINIMIZE: i32 = 6; + const ESCAPE_SCANCODE: u16 = 0x0001; + const ESCAPE_HOLD_TO_MINIMIZE: Duration = Duration::from_secs(5); + + struct EnumState { + process_id: u32, + candidates: Vec, + } + + #[derive(Clone, Copy)] + struct WindowCandidate { + hwnd: Hwnd, + area: i64, + } + + #[derive(Clone, Copy)] + struct RenderTargetSurface { + hwnd: isize, + client_rect: Rect, + } + + #[repr(C)] + #[derive(Clone, Copy)] + struct Rect { + left: i32, + top: i32, + right: i32, + bottom: i32, + } + + #[repr(C)] + #[derive(Clone, Copy)] + struct Point { + x: i32, + y: i32, + } + + #[repr(C)] + struct MonitorInfo { + cb_size: Dword, + rc_monitor: Rect, + rc_work: Rect, + dw_flags: Dword, + } + + #[repr(C)] + struct RawInputDevice { + us_usage_page: u16, + us_usage: u16, + dw_flags: Dword, + hwnd_target: Hwnd, + } + + #[repr(C)] + struct RawInputHeader { + dw_type: Dword, + dw_size: Dword, + h_device: *mut c_void, + w_param: Wparam, + } + + #[repr(C)] + #[derive(Clone, Copy)] + struct RawMouse { + us_flags: u16, + buttons: u32, + ul_raw_buttons: u32, + l_last_x: i32, + l_last_y: i32, + ul_extra_information: u32, + } + + #[repr(C)] + #[derive(Clone, Copy)] + struct RawKeyboard { + make_code: u16, + flags: u16, + reserved: u16, + vkey: u16, + message: Uint, + extra_information: u32, + } + + #[derive(Clone, Copy)] + struct PressedKey { + keycode: u16, + scancode: u16, + suppressed: bool, + } + + #[derive(Clone, Copy)] + struct EscapeKeyPress { + scancode: u16, + hold_timer_armed: bool, + } + + static INPUT_EVENT_SENDER: OnceLock>>> = + OnceLock::new(); + static ORIGINAL_WNDPROCS: OnceLock>> = OnceLock::new(); + static CAPTURED_HWND: OnceLock>> = OnceLock::new(); + static PROTECTED_HWND: OnceLock>> = OnceLock::new(); + static PRESSED_KEYS: OnceLock>> = OnceLock::new(); + static LAST_LOCK_KEYS_STATE: OnceLock> = OnceLock::new(); + static LEGACY_SUPPRESSED_KEYS: OnceLock>> = OnceLock::new(); + static STARTED_AT: OnceLock = OnceLock::new(); + static ESCAPE_HOLD_HWND: OnceLock>> = OnceLock::new(); + static ESCAPE_HOLD_TOKEN: OnceLock = OnceLock::new(); + static ESCAPE_KEY_PRESS: OnceLock>> = OnceLock::new(); + static SHORTCUT_MATCHER: OnceLock> = OnceLock::new(); + static RENDER_TARGET_SURFACE: OnceLock>> = OnceLock::new(); + + #[link(name = "user32")] + unsafe extern "system" { + fn CallWindowProcW( + previous: isize, + hwnd: Hwnd, + message: Uint, + wparam: Wparam, + lparam: Lparam, + ) -> Lresult; + fn ClientToScreen(hwnd: Hwnd, point: *mut Point) -> Bool; + fn ClipCursor(rect: *const Rect) -> Bool; + fn DefWindowProcW(hwnd: Hwnd, message: Uint, wparam: Wparam, lparam: Lparam) -> Lresult; + fn EnumWindows( + callback: Option Bool>, + lparam: Lparam, + ) -> Bool; + fn GetMonitorInfoW(monitor: Hmonitor, info: *mut MonitorInfo) -> Bool; + fn GetRawInputData( + raw_input: Hrawinput, + command: Uint, + data: *mut c_void, + size: *mut u32, + header_size: u32, + ) -> u32; + fn GetKeyState(virtual_key: i32) -> i16; + fn GetWindow(hwnd: Hwnd, command: Uint) -> Hwnd; + fn GetWindowLongPtrW(hwnd: Hwnd, index: i32) -> isize; + fn GetWindowRect(hwnd: Hwnd, rect: *mut Rect) -> Bool; + fn GetWindowThreadProcessId(hwnd: Hwnd, process_id: *mut u32) -> u32; + fn IsIconic(hwnd: Hwnd) -> Bool; + fn IsWindowVisible(hwnd: Hwnd) -> Bool; + fn MonitorFromWindow(hwnd: Hwnd, flags: Dword) -> Hmonitor; + fn RegisterRawInputDevices(devices: *const RawInputDevice, count: u32, size: u32) -> Bool; + fn ReleaseCapture() -> Bool; + fn SetCapture(hwnd: Hwnd) -> Hwnd; + fn SetCursor(cursor: Hcursor) -> Hcursor; + fn SetFocus(hwnd: Hwnd) -> Hwnd; + fn SetForegroundWindow(hwnd: Hwnd) -> Bool; + fn SetWindowLongPtrW(hwnd: Hwnd, index: i32, new_long: isize) -> isize; + fn SetWindowPos( + hwnd: Hwnd, + insert_after: Hwnd, + x: i32, + y: i32, + cx: i32, + cy: i32, + flags: u32, + ) -> Bool; + fn ShowWindow(hwnd: Hwnd, command: i32) -> Bool; + fn ShowCursor(show: Bool) -> i32; + } + + #[link(name = "kernel32")] + unsafe extern "system" { + fn GetCurrentProcessId() -> u32; + } + + pub unsafe fn set_render_target_surface(target: Option<(usize, NativeRenderRect)>) { + let target_surface = target.map(|(window_handle, rect)| RenderTargetSurface { + hwnd: window_handle as isize, + client_rect: Rect { + left: rect.x, + top: rect.y, + right: rect.x.saturating_add(rect.width.max(2)), + bottom: rect.y.saturating_add(rect.height.max(2)), + }, + }); + let slot = RENDER_TARGET_SURFACE.get_or_init(|| Mutex::new(None)); + if let Ok(mut current) = slot.lock() { + *current = target_surface; + } + } + + pub unsafe fn set_input_event_sender(sender: Option>) { + let input_stopped = sender.is_none(); + let slot = INPUT_EVENT_SENDER.get_or_init(|| Mutex::new(None)); + if let Ok(mut current) = slot.lock() { + *current = sender; + } + if input_stopped { + unregister_raw_input_devices(); + } + } + + pub unsafe fn set_shortcut_bindings(bindings: NativeStreamerShortcutBindings) { + let matcher = SHORTCUT_MATCHER.get_or_init(|| Mutex::new(NativeShortcutMatcher::default())); + if let Ok(mut current) = matcher.lock() { + *current = NativeShortcutMatcher::from_bindings(&bindings); + } + } + + pub unsafe fn clear_shortcut_bindings() { + let matcher = SHORTCUT_MATCHER.get_or_init(|| Mutex::new(NativeShortcutMatcher::default())); + if let Ok(mut current) = matcher.lock() { + *current = NativeShortcutMatcher::default(); + } + } + + pub unsafe fn release_current_input_capture() { + let Some(captured) = CAPTURED_HWND + .get() + .and_then(|captured| captured.lock().ok().and_then(|captured| *captured)) + else { + if !crate::gstreamer_config::use_internal_renderer() { + unregister_raw_input_devices(); + } + return; + }; + + release_input_capture(captured as Hwnd); + } + + /// Arm RawInput on the internal child HWND (sibling of Intermediate D3D). + /// Chains over the child's existing wndproc so SET_BOUNDS still works. + pub unsafe fn arm_internal_child_input(hwnd: usize) -> bool { + if hwnd == 0 { + return false; + } + let hwnd = hwnd as Hwnd; + let protected_slot = PROTECTED_HWND.get_or_init(|| Mutex::new(None)); + if let Ok(mut protected) = protected_slot.lock() { + *protected = Some(hwnd as isize); + } + let wndproc_installed = install_input_wndproc(hwnd); + let keyboard_registered = register_internal_raw_keyboard(hwnd); + wndproc_installed || keyboard_registered + } + + pub unsafe fn protect_process_renderer_window() -> bool { + let mut state = EnumState { + process_id: GetCurrentProcessId(), + candidates: Vec::new(), + }; + EnumWindows( + Some(collect_renderer_window_candidate), + &mut state as *mut EnumState as Lparam, + ); + + let Some(candidate) = state + .candidates + .into_iter() + .max_by_key(|candidate| candidate.area) + else { + return false; + }; + + protect_renderer_window(candidate.hwnd) + } + + unsafe extern "system" fn collect_renderer_window_candidate( + hwnd: Hwnd, + lparam: Lparam, + ) -> Bool { + let state = &mut *(lparam as *mut EnumState); + let mut process_id = 0; + GetWindowThreadProcessId(hwnd, &mut process_id); + if process_id != state.process_id || IsWindowVisible(hwnd) == 0 || IsIconic(hwnd) != 0 { + return 1; + } + + if !GetWindow(hwnd, GW_OWNER).is_null() { + return 1; + } + + let ex_style = GetWindowLongPtrW(hwnd, GWL_EXSTYLE); + if (ex_style & (WS_EX_TOOLWINDOW | WS_EX_NOACTIVATE)) != 0 { + return 1; + } + + let mut rect = Rect { + left: 0, + top: 0, + right: 0, + bottom: 0, + }; + if GetWindowRect(hwnd, &mut rect) == 0 { + return 1; + } + let width = rect.right.saturating_sub(rect.left); + let height = rect.bottom.saturating_sub(rect.top); + if width < 320 || height < 180 { + return 1; + } + + state.candidates.push(WindowCandidate { + hwnd, + area: i64::from(width) * i64::from(height), + }); + 1 + } + + unsafe fn protect_renderer_window(hwnd: Hwnd) -> bool { + let protected_slot = PROTECTED_HWND.get_or_init(|| Mutex::new(None)); + if let Ok(mut protected) = protected_slot.lock() { + *protected = Some(hwnd as isize); + } + + let mut configured = false; + let current = GetWindowLongPtrW(hwnd, GWL_EXSTYLE); + let desired = current & !(WS_EX_NOACTIVATE | WS_EX_TRANSPARENT); + if desired != current { + SetWindowLongPtrW(hwnd, GWL_EXSTYLE, desired); + configured = true; + } + + let current_style = GetWindowLongPtrW(hwnd, GWL_STYLE); + let fullscreen_style = current_style + & !(WS_CAPTION | WS_THICKFRAME | WS_MINIMIZEBOX | WS_MAXIMIZEBOX | WS_SYSMENU); + if fullscreen_style != current_style { + SetWindowLongPtrW(hwnd, GWL_STYLE, fullscreen_style); + configured = true; + } + + if install_input_wndproc(hwnd) { + SetForegroundWindow(hwnd); + SetFocus(hwnd); + configured = true; + } + + if let Some(rect) = target_renderer_rect().or_else(|| monitor_rect_for_window(hwnd)) { + SetWindowPos( + hwnd, + HWND_NOTOPMOST, + rect.left, + rect.top, + rect.right.saturating_sub(rect.left).max(2), + rect.bottom.saturating_sub(rect.top).max(2), + SWP_NOACTIVATE | SWP_FRAMECHANGED, + ); + configured = true; + } else { + SetWindowPos( + hwnd, + HWND_NOTOPMOST, + 0, + 0, + 0, + 0, + SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE | SWP_FRAMECHANGED, + ); + } + + configured + } + + unsafe fn render_rect_to_screen_rect(hwnd: Hwnd, rect: Rect) -> Option { + if hwnd.is_null() { + return None; + } + let mut origin = Point { + x: rect.left, + y: rect.top, + }; + if ClientToScreen(hwnd, &mut origin) == 0 { + return None; + } + let width = rect.right.saturating_sub(rect.left).max(2); + let height = rect.bottom.saturating_sub(rect.top).max(2); + + Some(Rect { + left: origin.x, + top: origin.y, + right: origin.x.saturating_add(width), + bottom: origin.y.saturating_add(height), + }) + } + + unsafe fn install_input_wndproc(hwnd: Hwnd) -> bool { + let key = hwnd as isize; + let map = ORIGINAL_WNDPROCS.get_or_init(|| Mutex::new(HashMap::new())); + let Ok(mut map) = map.lock() else { + return false; + }; + if map.contains_key(&key) { + return false; + } + + let previous = SetWindowLongPtrW(hwnd, GWLP_WNDPROC, renderer_window_wndproc as isize); + if previous == 0 { + return false; + } + map.insert(key, previous); + true + } + + unsafe extern "system" fn renderer_window_wndproc( + hwnd: Hwnd, + message: Uint, + wparam: Wparam, + lparam: Lparam, + ) -> Lresult { + if message == WM_NCHITTEST { + return HTCLIENT; + } + if message == WM_MOUSEACTIVATE { + begin_input_capture(hwnd); + return MA_ACTIVATE; + } + if message == WM_SETCURSOR && is_input_captured(hwnd) { + SetCursor(null_mut()); + return 1; + } + if message == WM_INPUT { + handle_raw_input(lparam as Hrawinput); + return 0; + } + let handled_legacy_shortcut = is_keyboard_message(message) + && handle_legacy_shortcut_keyboard(message, wparam, lparam); + if handled_legacy_shortcut { + return 0; + } + if is_escape_keyboard_message(message, wparam) { + if !is_input_captured(hwnd) { + begin_input_capture(hwnd); + } + handle_legacy_escape_keyboard(message, lparam); + return 0; + } + if message == WM_KILLFOCUS || (message == WM_ACTIVATE && (wparam & 0xffff) == WA_INACTIVE) { + release_input_capture(hwnd); + } + if let Some((button, pressed)) = legacy_mouse_button(message, wparam) { + let was_captured = is_input_captured(hwnd); + if pressed && !was_captured { + begin_input_capture(hwnd); + emit_input_event(NativeWindowInputEvent::MouseButton { + pressed, + button, + timestamp_us: timestamp_us(), + }); + } + } + + let key = hwnd as isize; + let previous = ORIGINAL_WNDPROCS + .get() + .and_then(|map| map.lock().ok().and_then(|map| map.get(&key).copied())); + if let Some(previous) = previous { + return CallWindowProcW(previous, hwnd, message, wparam, lparam); + } + + DefWindowProcW(hwnd, message, wparam, lparam) + } + + unsafe fn monitor_rect_for_window(hwnd: Hwnd) -> Option { + let monitor = MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST); + if monitor.is_null() { + return None; + } + + let mut info = MonitorInfo { + cb_size: std::mem::size_of::() as Dword, + rc_monitor: Rect { + left: 0, + top: 0, + right: 0, + bottom: 0, + }, + rc_work: Rect { + left: 0, + top: 0, + right: 0, + bottom: 0, + }, + dw_flags: 0, + }; + if GetMonitorInfoW(monitor, &mut info) == 0 { + return None; + } + + Some(info.rc_monitor) + } + + unsafe fn target_renderer_rect() -> Option { + let target = RENDER_TARGET_SURFACE + .get() + .and_then(|surface| surface.lock().ok().and_then(|surface| *surface))?; + let hwnd = target.hwnd as Hwnd; + render_rect_to_screen_rect(hwnd, target.client_rect) + .or_else(|| monitor_rect_for_window(hwnd)) + } + + unsafe fn begin_input_capture(hwnd: Hwnd) { + // External floating window: take OS focus so RawInput + ClipCursor work + // without INPUTSINK. Internal child: leave Electron as foreground so its + // shortcut keydown handlers keep working; keyboard arrives via INPUTSINK. + if !crate::gstreamer_config::use_internal_renderer() { + SetForegroundWindow(hwnd); + SetFocus(hwnd); + } + SetCapture(hwnd); + register_raw_input_devices(hwnd); + if let Some(rect) = target_renderer_rect().or_else(|| monitor_rect_for_window(hwnd)) { + ClipCursor(&rect); + } + hide_cursor(); + emit_input_capture_changed(true); + + let slot = CAPTURED_HWND.get_or_init(|| Mutex::new(None)); + if let Ok(mut captured) = slot.lock() { + *captured = Some(hwnd as isize); + } + sync_lock_keys_state(true); + } + + unsafe fn release_input_capture(hwnd: Hwnd) { + cancel_escape_hold_to_minimize_timer(); + clear_escape_key_press(); + let slot = CAPTURED_HWND.get_or_init(|| Mutex::new(None)); + let mut should_release = false; + if let Ok(mut captured) = slot.lock() { + should_release = captured.is_some_and(|captured| captured == hwnd as isize); + if should_release { + *captured = None; + } + } + + if !should_release { + return; + } + + release_pressed_keys(); + ReleaseCapture(); + ClipCursor(null()); + show_cursor(); + emit_input_capture_changed(false); + if crate::gstreamer_config::use_internal_renderer() { + // F10/F8 release relative mouse capture, but the internal stream + // must keep receiving keyboard input (especially Escape) while the + // Electron window remains foreground. + unregister_raw_mouse_device(); + register_internal_raw_keyboard(hwnd); + } else { + unregister_raw_input_devices(); + } + SetWindowPos( + hwnd, + HWND_NOTOPMOST, + 0, + 0, + 0, + 0, + SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE, + ); + } + + fn is_input_captured(hwnd: Hwnd) -> bool { + CAPTURED_HWND + .get() + .and_then(|captured| captured.lock().ok().and_then(|captured| *captured)) + .is_some_and(|captured| captured == hwnd as isize) + } + + fn captured_hwnd() -> Option { + CAPTURED_HWND + .get() + .and_then(|captured| captured.lock().ok().and_then(|captured| *captured)) + } + + fn protected_hwnd() -> Option { + PROTECTED_HWND + .get() + .and_then(|captured| captured.lock().ok().and_then(|captured| *captured)) + } + + unsafe fn start_escape_hold_to_minimize_timer() { + let Some(hwnd) = captured_hwnd() else { + return; + }; + + let token = ESCAPE_HOLD_TOKEN + .get_or_init(|| AtomicU64::new(0)) + .fetch_add(1, Ordering::SeqCst) + .wrapping_add(1); + let slot = ESCAPE_HOLD_HWND.get_or_init(|| Mutex::new(None)); + if let Ok(mut held_hwnd) = slot.lock() { + *held_hwnd = Some(hwnd); + } + + thread::spawn(move || { + thread::sleep(ESCAPE_HOLD_TO_MINIMIZE); + unsafe { + minimize_window_if_escape_still_held(hwnd, token); + } + }); + } + + fn cancel_escape_hold_to_minimize_timer() { + ESCAPE_HOLD_TOKEN + .get_or_init(|| AtomicU64::new(0)) + .fetch_add(1, Ordering::SeqCst); + let slot = ESCAPE_HOLD_HWND.get_or_init(|| Mutex::new(None)); + if let Ok(mut held_hwnd) = slot.lock() { + *held_hwnd = None; + } + } + + unsafe fn minimize_window_if_escape_still_held(hwnd: isize, token: u64) { + let current_token = ESCAPE_HOLD_TOKEN + .get_or_init(|| AtomicU64::new(0)) + .load(Ordering::SeqCst); + if current_token != token { + return; + } + + let still_held = ESCAPE_HOLD_HWND + .get() + .and_then(|held_hwnd| held_hwnd.lock().ok().and_then(|held_hwnd| *held_hwnd)) + .is_some_and(|held_hwnd| held_hwnd == hwnd); + if !still_held { + return; + } + + // Consume the held Escape so key-up does not also send a tap to GFN. + clear_escape_key_press(); + cancel_escape_hold_to_minimize_timer(); + + let hwnd = hwnd as Hwnd; + release_input_capture(hwnd); + + ShowWindow(hwnd, SW_MINIMIZE); + } + + unsafe fn register_raw_input_devices(hwnd: Hwnd) -> bool { + let keyboard_flags = if crate::gstreamer_config::use_internal_renderer() { + // Internal: Electron stays foreground and must keep receiving legacy + // WM_KEYDOWN for UI shortcuts. Do NOT set RIDEV_NOLEGACY on keyboard + // or Electron shortcuts die. INPUTSINK delivers WM_INPUT while Electron + // remains the top-level foreground window. + RIDEV_INPUTSINK + } else { + RIDEV_NOLEGACY + }; + let mouse_flags = if crate::gstreamer_config::use_internal_renderer() { + RIDEV_NOLEGACY | RIDEV_CAPTUREMOUSE | RIDEV_INPUTSINK + } else { + RIDEV_NOLEGACY | RIDEV_CAPTUREMOUSE + }; + let devices = [ + RawInputDevice { + us_usage_page: 0x01, + us_usage: 0x02, + dw_flags: mouse_flags, + hwnd_target: hwnd, + }, + RawInputDevice { + us_usage_page: 0x01, + us_usage: 0x06, + dw_flags: keyboard_flags, + hwnd_target: hwnd, + }, + ]; + + RegisterRawInputDevices( + devices.as_ptr(), + devices.len() as u32, + std::mem::size_of::() as u32, + ) != 0 + } + + unsafe fn register_internal_raw_keyboard(hwnd: Hwnd) -> bool { + let device = RawInputDevice { + us_usage_page: 0x01, + us_usage: 0x06, + dw_flags: RIDEV_INPUTSINK, + hwnd_target: hwnd, + }; + + RegisterRawInputDevices( + &device, + 1, + std::mem::size_of::() as u32, + ) != 0 + } + + unsafe fn unregister_raw_mouse_device() -> bool { + let device = RawInputDevice { + us_usage_page: 0x01, + us_usage: 0x02, + dw_flags: RIDEV_REMOVE, + hwnd_target: null_mut(), + }; + + RegisterRawInputDevices( + &device, + 1, + std::mem::size_of::() as u32, + ) != 0 + } + + unsafe fn unregister_raw_input_devices() -> bool { + let devices = [ + RawInputDevice { + us_usage_page: 0x01, + us_usage: 0x02, + dw_flags: RIDEV_REMOVE, + hwnd_target: null_mut(), + }, + RawInputDevice { + us_usage_page: 0x01, + us_usage: 0x06, + dw_flags: RIDEV_REMOVE, + hwnd_target: null_mut(), + }, + ]; + + RegisterRawInputDevices( + devices.as_ptr(), + devices.len() as u32, + std::mem::size_of::() as u32, + ) != 0 + } + + unsafe fn handle_raw_input(raw_input: Hrawinput) { + let mut size = 0u32; + let header_size = std::mem::size_of::() as u32; + let query = GetRawInputData(raw_input, RID_INPUT, null_mut(), &mut size, header_size); + if query == u32::MAX || size < header_size { + return; + } + + let mut buffer = vec![0u8; size as usize]; + let read = GetRawInputData( + raw_input, + RID_INPUT, + buffer.as_mut_ptr() as *mut c_void, + &mut size, + header_size, + ); + if read == u32::MAX || read == 0 || buffer.len() < header_size as usize { + return; + } + + let header = &*(buffer.as_ptr() as *const RawInputHeader); + let data = buffer.as_ptr().add(std::mem::size_of::()); + match header.dw_type { + RIM_TYPEMOUSE => handle_raw_mouse(&*(data as *const RawMouse)), + RIM_TYPEKEYBOARD => handle_raw_keyboard(&*(data as *const RawKeyboard)), + _ => {} + } + } + + unsafe fn handle_raw_mouse(raw: &RawMouse) { + if CAPTURED_HWND + .get() + .and_then(|captured| captured.lock().ok().and_then(|captured| *captured)) + .is_none() + { + return; + } + + let timestamp_us = timestamp_us(); + let dx = clamp_i32_to_i16(raw.l_last_x); + let dy = clamp_i32_to_i16(raw.l_last_y); + if dx != 0 || dy != 0 { + emit_input_event(NativeWindowInputEvent::MouseMove { + dx, + dy, + timestamp_us, + }); + } + + let button_flags = (raw.buttons & 0xffff) as u16; + let button_data = (raw.buttons >> 16) as u16; + emit_raw_mouse_button_events(button_flags, timestamp_us); + if (button_flags & RI_MOUSE_WHEEL) != 0 { + emit_input_event(NativeWindowInputEvent::MouseWheel { + delta: button_data as i16, + timestamp_us, + }); + } + } + + unsafe fn emit_raw_mouse_button_events(flags: u16, timestamp_us: u64) { + let pairs = [ + (RI_MOUSE_LEFT_BUTTON_DOWN, 1, true), + (RI_MOUSE_LEFT_BUTTON_UP, 1, false), + (RI_MOUSE_MIDDLE_BUTTON_DOWN, 2, true), + (RI_MOUSE_MIDDLE_BUTTON_UP, 2, false), + (RI_MOUSE_RIGHT_BUTTON_DOWN, 3, true), + (RI_MOUSE_RIGHT_BUTTON_UP, 3, false), + (RI_MOUSE_BUTTON_4_DOWN, 4, true), + (RI_MOUSE_BUTTON_4_UP, 4, false), + (RI_MOUSE_BUTTON_5_DOWN, 5, true), + (RI_MOUSE_BUTTON_5_UP, 5, false), + ]; + + for (flag, button, pressed) in pairs { + if (flags & flag) != 0 { + emit_input_event(NativeWindowInputEvent::MouseButton { + pressed, + button, + timestamp_us, + }); + } + } + } + + unsafe fn handle_raw_keyboard(raw: &RawKeyboard) { + if raw.vkey == 0xff { + return; + } + + let pressed = match raw.message { + WM_KEYDOWN | WM_SYSKEYDOWN => true, + WM_KEYUP | WM_SYSKEYUP => false, + _ => (raw.flags & RI_KEY_BREAK) == 0, + }; + let keycode = normalize_virtual_key(raw.vkey, raw.make_code, raw.flags); + let mut scancode = normalize_scancode(raw.make_code, raw.flags); + if keycode == VK_ESCAPE && scancode == 0 { + scancode = ESCAPE_SCANCODE; + } + if keycode == 0 || scancode == 0 { + return; + } + handle_keyboard_state(keycode, scancode, pressed); + } + + unsafe fn handle_legacy_escape_keyboard(message: Uint, lparam: Lparam) { + let pressed = matches!(message, WM_KEYDOWN | WM_SYSKEYDOWN); + let mut scancode = legacy_keyboard_scancode(lparam); + if scancode == 0 { + scancode = ESCAPE_SCANCODE; + } + handle_keyboard_state(VK_ESCAPE, scancode, pressed); + } + + fn is_escape_keyboard_message(message: Uint, wparam: Wparam) -> bool { + matches!(message, WM_KEYDOWN | WM_KEYUP | WM_SYSKEYDOWN | WM_SYSKEYUP) + && (wparam as u16) == VK_ESCAPE + } + + fn is_keyboard_message(message: Uint) -> bool { + matches!(message, WM_KEYDOWN | WM_KEYUP | WM_SYSKEYDOWN | WM_SYSKEYUP) + } + + fn legacy_keyboard_scancode(lparam: Lparam) -> u16 { + let scancode = ((lparam >> 16) & 0xff) as u16; + if scancode == 0 { + return 0; + } + if ((lparam >> 24) & 0x01) != 0 { + 0xe000 | scancode + } else { + scancode + } + } + + unsafe fn handle_legacy_shortcut_keyboard( + message: Uint, + wparam: Wparam, + lparam: Lparam, + ) -> bool { + let keycode = wparam as u16; + if keycode == VK_ESCAPE { + return false; + } + + let pressed = matches!(message, WM_KEYDOWN | WM_SYSKEYDOWN); + let scancode = legacy_keyboard_scancode(lparam); + let key_id = if scancode == 0 { keycode } else { scancode }; + let suppressed_keys = LEGACY_SUPPRESSED_KEYS.get_or_init(|| Mutex::new(HashSet::new())); + let Ok(mut suppressed_keys) = suppressed_keys.lock() else { + return false; + }; + + if !pressed { + return suppressed_keys.remove(&key_id); + } + + if suppressed_keys.contains(&key_id) { + return true; + } + + let modifiers = current_legacy_modifier_flags(); + if pressed && is_clipboard_paste_shortcut(keycode, modifiers) { + suppressed_keys.insert(key_id); + drop(suppressed_keys); + emit_clipboard_paste_request(); + return true; + } + + let Some(action) = shortcut_action_for_keypress(keycode, scancode, modifiers) else { + return false; + }; + + suppressed_keys.insert(key_id); + drop(suppressed_keys); + handle_shortcut_action(action); + true + } + + unsafe fn handle_keyboard_state(keycode: u16, scancode: u16, pressed: bool) { + sync_lock_keys_state(false); + + let keys = PRESSED_KEYS.get_or_init(|| Mutex::new(HashMap::new())); + let Ok(mut keys) = keys.lock() else { + return; + }; + if pressed && keycode == VK_TAB && is_alt_modifier_down(&keys) { + drop(keys); + release_current_input_capture(); + return; + } + if keycode == VK_ESCAPE { + drop(keys); + // In the internal renderer Electron must intercept the legacy Escape + // before Chromium exits fullscreen, then forward exactly one tap over + // IPC. RawInput still owns every other key in this mode. The external + // native window has no Electron interception and keeps this path. + if crate::gstreamer_config::use_internal_renderer() { + return; + } + handle_escape_keyboard_state(scancode, pressed); + return; + } + let previous = keys.get(&scancode).copied(); + if pressed { + if previous.is_some() { + return; + } + let modifiers = pressed_key_modifier_flags(&keys, keycode); + if is_clipboard_paste_shortcut(keycode, modifiers) { + keys.insert( + scancode, + PressedKey { + keycode, + scancode, + suppressed: true, + }, + ); + drop(keys); + emit_clipboard_paste_request(); + return; + } + if let Some(action) = shortcut_action_for_keypress(keycode, scancode, modifiers) { + keys.insert( + scancode, + PressedKey { + keycode, + scancode, + suppressed: true, + }, + ); + drop(keys); + handle_shortcut_action(action); + return; + } + keys.insert( + scancode, + PressedKey { + keycode, + scancode, + suppressed: false, + }, + ); + } else if let Some(previous) = keys.remove(&scancode) { + if previous.suppressed { + return; + } + } + let modifiers = pressed_key_modifier_flags(&keys, keycode); + drop(keys); + + emit_input_event(NativeWindowInputEvent::Key { + pressed, + keycode, + scancode, + modifiers, + timestamp_us: timestamp_us(), + }); + } + + unsafe fn handle_escape_keyboard_state(scancode: u16, pressed: bool) { + let slot = ESCAPE_KEY_PRESS.get_or_init(|| Mutex::new(None)); + let Ok(mut escape_press) = slot.lock() else { + return; + }; + + if pressed { + let should_start_hold_timer = if let Some(current) = escape_press.as_mut() { + let should_start = !crate::gstreamer_config::use_internal_renderer() + && !current.hold_timer_armed + && captured_hwnd().is_some(); + if should_start { + current.hold_timer_armed = true; + } + should_start + } else { + let hold_timer_armed = + !crate::gstreamer_config::use_internal_renderer() && captured_hwnd().is_some(); + *escape_press = Some(EscapeKeyPress { + scancode, + hold_timer_armed, + }); + hold_timer_armed + }; + drop(escape_press); + if should_start_hold_timer { + start_escape_hold_to_minimize_timer(); + } + return; + } + + let Some(escape_press) = escape_press.take() else { + cancel_escape_hold_to_minimize_timer(); + return; + }; + let scancode = escape_press.scancode; + + cancel_escape_hold_to_minimize_timer(); + send_escape_tap(scancode); + } + + fn clear_escape_key_press() { + let slot = ESCAPE_KEY_PRESS.get_or_init(|| Mutex::new(None)); + if let Ok(mut escape_press) = slot.lock() { + *escape_press = None; + } + } + + fn send_escape_tap(scancode: u16) { + let keydown_timestamp_us = timestamp_us(); + emit_input_event(NativeWindowInputEvent::Key { + pressed: true, + keycode: VK_ESCAPE, + scancode, + modifiers: 0, + timestamp_us: keydown_timestamp_us, + }); + emit_input_event(NativeWindowInputEvent::Key { + pressed: false, + keycode: VK_ESCAPE, + scancode, + modifiers: 0, + timestamp_us: timestamp_us(), + }); + } + + unsafe fn release_pressed_keys() { + let keys = PRESSED_KEYS.get_or_init(|| Mutex::new(HashMap::new())); + let Ok(mut keys) = keys.lock() else { + return; + }; + let pressed = keys.values().copied().collect::>(); + keys.clear(); + drop(keys); + + let timestamp_us = timestamp_us(); + for key in pressed { + if key.suppressed { + continue; + } + emit_input_event(NativeWindowInputEvent::Key { + pressed: false, + keycode: key.keycode, + scancode: key.scancode, + modifiers: 0, + timestamp_us, + }); + } + } + + fn normalize_virtual_key(vkey: u16, make_code: u16, flags: u16) -> u16 { + match vkey { + VK_SHIFT => match make_code { + 0x36 => VK_RSHIFT, + _ => VK_LSHIFT, + }, + VK_CONTROL => { + if (flags & RI_KEY_E0) != 0 { + VK_RCONTROL + } else { + VK_LCONTROL + } + } + VK_MENU => { + if (flags & RI_KEY_E0) != 0 { + VK_RMENU + } else { + VK_LMENU + } + } + _ => vkey, + } + } + + fn normalize_scancode(make_code: u16, flags: u16) -> u16 { + if make_code == 0 { + return 0; + } + if (flags & RI_KEY_E0) != 0 { + 0xe000 | make_code + } else if (flags & RI_KEY_E1) != 0 { + 0xe100 | make_code + } else { + make_code + } + } + + /// Lock-key bitmask for INPUT_LOCK_KEYS_SYNC (official GFN iS() on desktop Windows). + unsafe fn lock_keys_sync_state() -> u8 { + let mut state = 0x10; + if (GetKeyState(VK_CAPITAL) & 0x0001) != 0 { + state |= 0x01; + } + state |= 0x20; + state |= 0x40; + if (GetKeyState(VK_NUMLOCK) & 0x0001) != 0 { + state |= 0x02; + } + if (GetKeyState(VK_SCROLL) & 0x0001) != 0 { + state |= 0x04; + } + state + } + + unsafe fn sync_lock_keys_state(force: bool) { + let state = lock_keys_sync_state(); + let slot = LAST_LOCK_KEYS_STATE.get_or_init(|| Mutex::new(0)); + let Ok(mut last) = slot.lock() else { + return; + }; + if !force && *last == state { + return; + } + *last = state; + drop(last); + emit_input_event(NativeWindowInputEvent::LockKeysSync { state }); + } + + /// Per-key modifier byte from tracked pressed keys (official GFN yS()/Cb()). + /// Lock keys sync separately via INPUT_LOCK_KEYS_SYNC, not here. + unsafe fn pressed_key_modifier_flags( + keys: &HashMap, + active_keycode: u16, + ) -> u16 { + let mut modifiers = 0u16; + let mut shift_tracked = false; + let mut control_tracked = false; + let mut alt_tracked = false; + let mut win_tracked = false; + + for key in keys.values() { + if key.keycode == active_keycode { + continue; + } + match key.keycode { + VK_LSHIFT | VK_RSHIFT | VK_SHIFT => { + shift_tracked = true; + modifiers |= 0x01; + } + VK_LCONTROL | VK_RCONTROL | VK_CONTROL => { + control_tracked = true; + modifiers |= 0x02; + } + VK_LMENU | VK_RMENU | VK_MENU => { + alt_tracked = true; + modifiers |= 0x04; + } + VK_LWIN | VK_RWIN => { + win_tracked = true; + modifiers |= 0x08; + } + _ => {} + } + } + + if !matches!(active_keycode, VK_LSHIFT | VK_RSHIFT | VK_SHIFT) + && !shift_tracked + && (is_key_down(VK_SHIFT) || is_key_down(VK_LSHIFT) || is_key_down(VK_RSHIFT)) + { + modifiers |= 0x01; + } + if !matches!(active_keycode, VK_LCONTROL | VK_RCONTROL | VK_CONTROL) + && !control_tracked + && (is_key_down(VK_CONTROL) || is_key_down(VK_LCONTROL) || is_key_down(VK_RCONTROL)) + { + modifiers |= 0x02; + } + if !matches!(active_keycode, VK_LMENU | VK_RMENU | VK_MENU) + && !alt_tracked + && (is_key_down(VK_MENU) || is_key_down(VK_LMENU) || is_key_down(VK_RMENU)) + { + modifiers |= 0x04; + } + if !matches!(active_keycode, VK_LWIN | VK_RWIN) + && !win_tracked + && (is_key_down(VK_LWIN) || is_key_down(VK_RWIN)) + { + modifiers |= 0x08; + } + + modifiers + } + + /// Legacy fallback for shortcut matching before a key enters the pressed-key map. + unsafe fn keyboard_modifier_flags(active_keycode: u16) -> u16 { + let keys = PRESSED_KEYS.get_or_init(|| Mutex::new(HashMap::new())); + if let Ok(keys) = keys.lock() { + if !keys.is_empty() { + return pressed_key_modifier_flags(&keys, active_keycode); + } + } + + let mut modifiers = 0u16; + if !matches!(active_keycode, VK_LSHIFT | VK_RSHIFT | VK_SHIFT) + && (is_key_down(VK_SHIFT) || is_key_down(VK_LSHIFT) || is_key_down(VK_RSHIFT)) + { + modifiers |= 0x01; + } + if !matches!(active_keycode, VK_LCONTROL | VK_RCONTROL | VK_CONTROL) + && (is_key_down(VK_CONTROL) || is_key_down(VK_LCONTROL) || is_key_down(VK_RCONTROL)) + { + modifiers |= 0x02; + } + if !matches!(active_keycode, VK_LMENU | VK_RMENU | VK_MENU) + && (is_key_down(VK_MENU) || is_key_down(VK_LMENU) || is_key_down(VK_RMENU)) + { + modifiers |= 0x04; + } + if !matches!(active_keycode, VK_LWIN | VK_RWIN) + && (is_key_down(VK_LWIN) || is_key_down(VK_RWIN)) + { + modifiers |= 0x08; + } + modifiers + } + + unsafe fn current_legacy_modifier_flags() -> u16 { + let mut modifiers = 0u16; + if is_key_down(VK_SHIFT) || is_key_down(VK_LSHIFT) || is_key_down(VK_RSHIFT) { + modifiers |= 0x01; + } + if is_key_down(VK_CONTROL) || is_key_down(VK_LCONTROL) || is_key_down(VK_RCONTROL) { + modifiers |= 0x02; + } + if is_key_down(VK_MENU) || is_key_down(VK_LMENU) || is_key_down(VK_RMENU) { + modifiers |= 0x04; + } + if is_key_down(VK_LWIN) || is_key_down(VK_RWIN) { + modifiers |= 0x08; + } + if (GetKeyState(VK_CAPITAL) & 0x0001) != 0 { + modifiers |= 0x10; + } + if (GetKeyState(VK_NUMLOCK) & 0x0001) != 0 { + modifiers |= 0x20; + } + modifiers + } + + unsafe fn is_key_down(keycode: u16) -> bool { + ((GetKeyState(keycode as i32) as u16) & 0x8000) != 0 + } + + unsafe fn is_alt_modifier_down(keys: &HashMap) -> bool { + keys.values() + .any(|key| matches!(key.keycode, VK_LMENU | VK_RMENU | VK_MENU)) + || ((GetKeyState(VK_MENU as i32) as u16) & 0x8000) != 0 + } + + fn shortcut_action_for_keypress( + keycode: u16, + scancode: u16, + modifiers: u16, + ) -> Option { + SHORTCUT_MATCHER + .get() + .and_then(|matcher| matcher.lock().ok()) + .and_then(|matcher| matcher.match_keydown(keycode, scancode, modifiers)) + } + + unsafe fn handle_shortcut_action(action: NativeStreamerShortcutAction) { + match action { + NativeStreamerShortcutAction::TogglePointerLock => { + if let Some(hwnd) = captured_hwnd().or_else(protected_hwnd) { + let hwnd = hwnd as Hwnd; + if is_input_captured(hwnd) { + release_input_capture(hwnd); + } else { + begin_input_capture(hwnd); + } + } + // Internal: Electron's own keydown owns the UI shortcut; only + // toggle RawInput capture here and keep the key out of GFN. + if crate::gstreamer_config::use_internal_renderer() { + return; + } + } + _ => { + if shortcut_action_releases_input_capture(action) { + release_current_input_capture(); + } + // Internal: Electron already owns UI shortcuts via keydown. + // Suppress the key from GFN (caller marks suppressed) without + // emitting Shortcut, or Electron would double-fire. + if crate::gstreamer_config::use_internal_renderer() { + return; + } + emit_input_event(NativeWindowInputEvent::Shortcut { action }); + } + } + } + + fn shortcut_action_releases_input_capture(action: NativeStreamerShortcutAction) -> bool { + matches!( + action, + NativeStreamerShortcutAction::ToggleFullscreen + | NativeStreamerShortcutAction::StopStream + ) + } + + fn legacy_mouse_button(message: Uint, wparam: Wparam) -> Option<(u8, bool)> { + match message { + WM_LBUTTONDOWN => Some((1, true)), + WM_LBUTTONUP => Some((1, false)), + WM_MBUTTONDOWN => Some((2, true)), + WM_MBUTTONUP => Some((2, false)), + WM_RBUTTONDOWN => Some((3, true)), + WM_RBUTTONUP => Some((3, false)), + WM_XBUTTONDOWN | WM_XBUTTONUP => { + let xbutton = ((wparam >> 16) & 0xffff) as u16; + let button = match xbutton { + XBUTTON1 => 4, + XBUTTON2 => 5, + _ => return None, + }; + Some((button, message == WM_XBUTTONDOWN)) + } + _ => None, + } + } + + fn emit_input_event(event: NativeWindowInputEvent) { + let Some(sender) = INPUT_EVENT_SENDER + .get() + .and_then(|sender| sender.lock().ok().and_then(|sender| sender.clone())) + else { + return; + }; + let _ = sender.send(event); + } + + fn is_clipboard_paste_shortcut(keycode: u16, modifiers: u16) -> bool { + keycode == VK_V + && ((modifiers & 0x02) != 0 || unsafe { is_ctrl_modifier_down() }) + && (modifiers & 0x04) == 0 + } + + unsafe fn is_ctrl_modifier_down() -> bool { + is_key_down(VK_CONTROL) || is_key_down(VK_LCONTROL) || is_key_down(VK_RCONTROL) + } + + fn emit_clipboard_paste_request() { + let Some(sender) = INPUT_EVENT_SENDER + .get() + .and_then(|sender| sender.lock().ok().and_then(|sender| sender.clone())) + else { + return; + }; + let _ = sender.send(NativeWindowInputEvent::ClipboardPaste); + } + + fn emit_input_capture_changed(captured: bool) { + // Electron maps this to notifyPointerLockChange so main-process Escape + // interception stays in sync with RawInput capture (tap→GFN, hold→exit). + let Some(sender) = INPUT_EVENT_SENDER + .get() + .and_then(|sender| sender.lock().ok().and_then(|sender| sender.clone())) + else { + return; + }; + let _ = sender.send(NativeWindowInputEvent::InputCaptureChanged { captured }); + } + + fn clamp_i32_to_i16(value: i32) -> i16 { + value.clamp(i16::MIN as i32, i16::MAX as i32) as i16 + } + + fn timestamp_us() -> u64 { + STARTED_AT + .get_or_init(Instant::now) + .elapsed() + .as_micros() + .min(u128::from(u64::MAX)) as u64 + } + + unsafe fn hide_cursor() { + while ShowCursor(0) >= 0 {} + } + + unsafe fn show_cursor() { + while ShowCursor(1) < 0 {} + } +} + +// The old BrowserWindow-HWND GstVideoOverlay path was removed. Internal mode +// uses `crate::internal_renderer::InternalRenderer` (child surface owned by the +// streamer). External mode uses the floating GStreamer window + window guard. + +#[cfg(target_os = "windows")] +pub(crate) fn primary_display_refresh_hz() -> Option { + const VREFRESH: i32 = 116; + + #[link(name = "user32")] + extern "system" { + fn GetDC(hwnd: *mut c_void) -> *mut c_void; + fn ReleaseDC(hwnd: *mut c_void, hdc: *mut c_void) -> i32; + } + + #[link(name = "gdi32")] + extern "system" { + fn GetDeviceCaps(hdc: *mut c_void, index: i32) -> i32; + } + + let hdc = unsafe { GetDC(std::ptr::null_mut()) }; + if hdc.is_null() { + return None; + } + + let refresh = unsafe { GetDeviceCaps(hdc, VREFRESH) }; + unsafe { + ReleaseDC(std::ptr::null_mut(), hdc); + } + + (refresh > 1).then_some(refresh as u32) +} + +#[cfg(not(target_os = "windows"))] +pub(crate) fn primary_display_refresh_hz() -> Option { + None +} diff --git a/native/opennow-streamer/src/input.rs b/native/opennow-streamer/src/input.rs index b1990ac84..50a43cf22 100644 --- a/native/opennow-streamer/src/input.rs +++ b/native/opennow-streamer/src/input.rs @@ -286,10 +286,16 @@ pub fn is_partially_reliable_hid_transfer_eligible(input_type: u32) -> bool { input_type == INPUT_MOUSE_REL } -pub(crate) fn layout_mapped_keyboard_scancode(_physical_scancode: u16) -> u16 { +pub(crate) fn layout_mapped_keyboard_scancode(physical_scancode: u16) -> u16 { // GFN keyboard events use the selected remote layout plus VK; sending the local physical // scancode makes QWERTZ-only keys such as Y/Z resolve as their US physical positions. - 0 + // Escape is the exception: Chromium's synthetic/pointer-lock path sends scan code 0x01, + // and GFN ignores native Escape taps when that scan code is discarded. + if physical_scancode == 0x0001 { + physical_scancode + } else { + 0 + } } pub(crate) fn layout_mapped_keyboard_keycode(fallback_keycode: u16, physical_scancode: u16) -> u16 { @@ -746,6 +752,11 @@ mod tests { assert_eq!(layout_mapped_keyboard_scancode(0x002c), 0); } + #[test] + fn layout_mapped_keyboard_input_preserves_escape_scancode() { + assert_eq!(layout_mapped_keyboard_scancode(0x0001), 0x0001); + } + #[test] fn layout_mapped_keyboard_input_uses_official_position_keycodes() { assert_eq!(layout_mapped_keyboard_keycode(0x0059, 0x002c), 0x005a); diff --git a/native/opennow-streamer/src/internal_renderer.rs b/native/opennow-streamer/src/internal_renderer.rs new file mode 100644 index 000000000..4a15e2e0c --- /dev/null +++ b/native/opennow-streamer/src/internal_renderer.rs @@ -0,0 +1,1495 @@ +//! Dedicated child native surface for the internal (single-window) renderer. +//! +//! GStreamer paints into a streamer-owned child surface that is parented into +//! the Electron window. The Electron BrowserWindow handle is never used as a +//! GstVideoOverlay target. + +use crate::protocol::{NativeRenderRect, NativeRenderSurface}; +use gstreamer as gst; +use std::sync::atomic::{AtomicUsize, Ordering}; +use std::sync::Mutex; + +#[cfg(any(target_os = "windows", target_os = "linux"))] +fn parse_native_handle(value: &str) -> Result { + let trimmed = value.trim(); + let hex = trimmed + .strip_prefix("0x") + .or_else(|| trimmed.strip_prefix("0X")); + let parsed = if let Some(hex) = hex { + usize::from_str_radix(hex, 16) + } else { + trimmed.parse::() + } + .map_err(|error| format!("Invalid native parent window handle {value:?}: {error}"))?; + + if parsed == 0 { + return Err("Native parent window handle is zero.".to_owned()); + } + + Ok(parsed) +} + +fn normalized_rect(rect: Option<&NativeRenderRect>) -> NativeRenderRect { + let Some(rect) = rect else { + return NativeRenderRect { + x: 0, + y: 0, + width: 2, + height: 2, + }; + }; + + NativeRenderRect { + x: rect.x.max(0), + y: rect.y.max(0), + width: rect.width.max(2), + height: rect.height.max(2), + } +} + +/// Platform child surface that GStreamer presents into. +#[derive(Debug)] +pub(crate) struct InternalRenderer { + inner: Mutex, + /// Child window handle for prepare-window-handle (avoids locking during sink setup). + child_handle: AtomicUsize, + child_width: std::sync::atomic::AtomicI32, + child_height: std::sync::atomic::AtomicI32, +} + +// Child window handles are owned exclusively by this process and only mutated +// under the mutex; GStreamer callbacks require Send + Sync on render state. +unsafe impl Send for InternalRenderer {} +unsafe impl Sync for InternalRenderer {} + +#[derive(Debug)] +struct InternalRendererState { + surface: Option, + last_parent: Option, + last_bounds: Option, + last_visible: bool, + video_sink: Option, +} + +impl InternalRenderer { + pub(crate) fn new() -> Self { + Self { + inner: Mutex::new(InternalRendererState { + surface: None, + last_parent: None, + last_bounds: None, + last_visible: false, + video_sink: None, + }), + child_handle: AtomicUsize::new(0), + child_width: std::sync::atomic::AtomicI32::new(2), + child_height: std::sync::atomic::AtomicI32::new(2), + } + } + + fn publish_child_handle(&self, handle: usize, bounds: &NativeRenderRect) { + self.child_handle.store(handle, Ordering::SeqCst); + self.child_width.store(bounds.width, Ordering::SeqCst); + self.child_height.store(bounds.height, Ordering::SeqCst); + } + + fn clear_child_handle(&self) { + self.child_handle.store(0, Ordering::SeqCst); + self.child_width.store(2, Ordering::SeqCst); + self.child_height.store(2, Ordering::SeqCst); + } + + #[cfg(target_os = "windows")] + pub(crate) fn child_handle(&self) -> usize { + self.child_handle.load(Ordering::SeqCst) + } + + pub(crate) fn set_video_sink(&self, sink: gst::Element) -> Result<(), String> { + let mut state = self + .inner + .lock() + .map_err(|_| "Internal renderer lock poisoned.".to_owned())?; + state.video_sink = Some(sink); + Self::rebind_overlay_locked(self, &mut state) + } + + pub(crate) fn apply_surface(&self, surface: &NativeRenderSurface) -> Result<(), String> { + let mut state = self + .inner + .lock() + .map_err(|_| "Internal renderer lock poisoned.".to_owned())?; + + let parent = match surface.window_handle.as_deref() { + Some(handle) => Some(parse_parent_handle(handle)?), + None => None, + }; + + if !surface.visible || parent.is_none() || surface.rect.is_none() { + if let Some(child) = state.surface.as_mut() { + child.set_visible(false)?; + } + state.last_visible = false; + return Ok(()); + } + + let parent = parent.expect("checked above"); + let bounds = normalized_rect(surface.rect.as_ref()); + + let needs_recreate = match state.last_parent { + Some(existing) => existing != parent || state.surface.is_none(), + None => true, + }; + + if needs_recreate { + if let Some(mut previous) = state.surface.take() { + previous.destroy(); + } + let child = PlatformChildSurface::create(parent, &bounds)?; + self.publish_child_handle(child.handle(), &bounds); + state.surface = Some(child); + state.last_parent = Some(parent); + state.last_bounds = Some(bounds.clone()); + state.last_visible = true; + Self::rebind_overlay_locked(self, &mut state)?; + } else { + let bounds_changed = state.last_bounds.as_ref() != Some(&bounds); + let was_visible = state.last_visible; + if let Some(child) = state.surface.as_mut() { + if bounds_changed { + child.set_bounds(&bounds)?; + } + if !was_visible { + child.set_visible(true)?; + } + self.publish_child_handle(child.handle(), &bounds); + } + state.last_bounds = Some(bounds); + state.last_visible = true; + if bounds_changed || !was_visible { + Self::rebind_overlay_locked(self, &mut state)?; + } + } + + Ok(()) + } + + pub(crate) fn destroy(&self) { + if let Ok(mut state) = self.inner.lock() { + if let Some(mut surface) = state.surface.take() { + surface.destroy(); + } + state.last_parent = None; + state.last_bounds = None; + state.last_visible = false; + state.video_sink = None; + } + self.clear_child_handle(); + } + + fn rebind_overlay_locked( + renderer: &InternalRenderer, + state: &mut InternalRendererState, + ) -> Result<(), String> { + let (Some(child), Some(sink)) = (state.surface.as_ref(), state.video_sink.as_ref()) else { + return Ok(()); + }; + let bounds = state.last_bounds.clone().unwrap_or_else(|| NativeRenderRect { + x: 0, + y: 0, + width: renderer.child_width.load(Ordering::SeqCst).max(2), + height: renderer.child_height.load(Ordering::SeqCst).max(2), + }); + bind_overlay_to_child(sink, child.handle(), Some(&bounds)) + } +} + +impl Drop for InternalRenderer { + fn drop(&mut self) { + self.destroy(); + } +} + +fn parse_parent_handle(value: &str) -> Result { + #[cfg(any(target_os = "windows", target_os = "linux"))] + { + parse_native_handle(value) + } + #[cfg(target_os = "macos")] + { + parse_native_handle_macos(value) + } + #[cfg(not(any(target_os = "windows", target_os = "macos", target_os = "linux")))] + { + let _ = value; + Err("Internal renderer is not supported on this platform.".to_owned()) + } +} + +#[cfg(target_os = "macos")] +fn parse_native_handle_macos(value: &str) -> Result { + let trimmed = value.trim(); + let hex = trimmed + .strip_prefix("0x") + .or_else(|| trimmed.strip_prefix("0X")); + let parsed = if let Some(hex) = hex { + usize::from_str_radix(hex, 16) + } else { + trimmed.parse::() + } + .map_err(|error| format!("Invalid native parent view handle {value:?}: {error}"))?; + + if parsed == 0 { + return Err("Native parent view handle is zero.".to_owned()); + } + + Ok(parsed) +} + +fn bind_overlay_to_child( + sink: &gst::Element, + child_handle: usize, + bounds: Option<&NativeRenderRect>, +) -> Result<(), String> { + #[cfg(any(target_os = "windows", target_os = "macos", target_os = "linux"))] + { + use gst_video::prelude::*; + use gstreamer_video as gst_video; + + let overlay = sink + .clone() + .dynamic_cast::() + .map_err(|_| { + format!( + "Native render sink {} does not implement GstVideoOverlay.", + sink.name() + ) + })?; + + // SAFETY: child_handle is a platform window/view created by this process + // (or an X11 window id we own) and remains valid while the surface lives. + // Win32 vulkansink: our patched gstvkwindow presents on the GSTVULKAN child + // hwnd while parenting that child under this overlay handle (no floating window). + unsafe { + overlay.set_window_handle(child_handle); + } + overlay.handle_events(false); + // Child surface is already sized to the StreamView rect; present into the + // full client area so D3D sinks do not wait on an empty render rectangle. + let rect = normalized_rect(bounds); + let _ = overlay.set_render_rectangle(0, 0, rect.width, rect.height); + overlay.expose(); + Ok(()) + } + #[cfg(not(any(target_os = "windows", target_os = "macos", target_os = "linux")))] + { + let _ = (sink, child_handle, bounds); + Err("Internal renderer overlay binding is not supported on this platform.".to_owned()) + } +} + +#[derive(Debug)] +struct PlatformChildSurface { + #[cfg(target_os = "windows")] + win: windows_child::ChildWindow, + #[cfg(target_os = "macos")] + view: usize, + #[cfg(target_os = "linux")] + window: linux_child::XWindow, + #[cfg(not(any(target_os = "windows", target_os = "macos", target_os = "linux")))] + _unused: (), +} + +impl PlatformChildSurface { + fn create(parent: usize, bounds: &NativeRenderRect) -> Result { + #[cfg(target_os = "windows")] + { + Ok(Self { + win: windows_child::ChildWindow::create(parent, bounds)?, + }) + } + #[cfg(target_os = "macos")] + { + let view = macos_child::create_child(parent, bounds)?; + Ok(Self { + view: view as usize, + }) + } + #[cfg(target_os = "linux")] + { + let window = linux_child::create_child(parent, bounds)?; + Ok(Self { window }) + } + #[cfg(not(any(target_os = "windows", target_os = "macos", target_os = "linux")))] + { + let _ = (parent, bounds); + Err("Internal renderer child surfaces are not supported on this platform.".to_owned()) + } + } + + fn handle(&self) -> usize { + #[cfg(target_os = "windows")] + { + self.win.hwnd() + } + #[cfg(target_os = "macos")] + { + self.view + } + #[cfg(target_os = "linux")] + { + self.window.window as usize + } + #[cfg(not(any(target_os = "windows", target_os = "macos", target_os = "linux")))] + { + 0 + } + } + + fn set_bounds(&mut self, bounds: &NativeRenderRect) -> Result<(), String> { + #[cfg(target_os = "windows")] + { + self.win.set_bounds(bounds) + } + #[cfg(target_os = "macos")] + { + macos_child::set_bounds(self.view as macos_child::NsViewPtr, bounds) + } + #[cfg(target_os = "linux")] + { + linux_child::set_bounds(&self.window, bounds) + } + #[cfg(not(any(target_os = "windows", target_os = "macos", target_os = "linux")))] + { + let _ = bounds; + Ok(()) + } + } + + fn set_visible(&mut self, visible: bool) -> Result<(), String> { + #[cfg(target_os = "windows")] + { + self.win.set_visible(visible) + } + #[cfg(target_os = "macos")] + { + macos_child::set_visible(self.view as macos_child::NsViewPtr, visible) + } + #[cfg(target_os = "linux")] + { + linux_child::set_visible(&self.window, visible) + } + #[cfg(not(any(target_os = "windows", target_os = "macos", target_os = "linux")))] + { + let _ = visible; + Ok(()) + } + } + + fn destroy(&mut self) { + #[cfg(target_os = "windows")] + { + self.win.destroy(); + } + #[cfg(target_os = "macos")] + { + macos_child::destroy(self.view as macos_child::NsViewPtr); + self.view = 0; + } + #[cfg(target_os = "linux")] + { + linux_child::destroy(&mut self.window); + } + } +} + +#[cfg(target_os = "windows")] +mod windows_child { + use crate::protocol::NativeRenderRect; + use std::ffi::c_void; + use std::ptr::{null, null_mut}; + use std::sync::atomic::{AtomicBool, Ordering}; + + pub(super) type Hwnd = *mut c_void; + + type Atom = u16; + type Bool = i32; + type Hinstance = *mut c_void; + type Hmenu = *mut c_void; + type Lparam = isize; + type Lresult = isize; + type Wparam = usize; + + const CS_HREDRAW: u32 = 0x0002; + const CS_OWNDC: u32 = 0x0020; + const CS_VREDRAW: u32 = 0x0001; + // Sibling of Chromium Intermediate D3D: sit on top for present, force + // Intermediate D3D WS_CLIPSIBLINGS so it punches a paint hole. Input is + // owned by RawInput on this HWND (not Electron click-through). + const HWND_TOP: Hwnd = std::ptr::null_mut(); + const GWL_STYLE: i32 = -16; + const GWL_EXSTYLE: i32 = -20; + const SWP_NOSIZE: u32 = 0x0001; + const SWP_NOMOVE: u32 = 0x0002; + const SWP_NOACTIVATE: u32 = 0x0010; + const SWP_FRAMECHANGED: u32 = 0x0020; + const SWP_SHOWWINDOW: u32 = 0x0040; + const SWP_HIDEWINDOW: u32 = 0x0080; + const SWP_NOCOPYBITS: u32 = 0x0100; + const SWP_NOZORDER: u32 = 0x0004; + const SWP_NOSENDCHANGING: u32 = 0x0400; + const SW_HIDE: i32 = 0; + const SW_SHOWNOACTIVATE: i32 = 4; + const WM_DESTROY: u32 = 0x0002; + const WM_ERASEBKGND: u32 = 0x0014; + const WM_MOUSEACTIVATE: u32 = 0x0021; + const WM_PAINT: u32 = 0x000F; + const WS_CHILD: u32 = 0x4000_0000; + const WS_CLIPCHILDREN: u32 = 0x0200_0000; + const WS_CLIPSIBLINGS: u32 = 0x0400_0000; + const WS_VISIBLE: u32 = 0x1000_0000; + const WS_POPUP: u32 = 0x8000_0000; + const WS_CAPTION: u32 = 0x00C0_0000; + const WS_THICKFRAME: u32 = 0x0004_0000; + const WS_MINIMIZEBOX: u32 = 0x0002_0000; + const WS_MAXIMIZEBOX: u32 = 0x0001_0000; + const WS_SYSMENU: u32 = 0x0008_0000; + const WS_BORDER: u32 = 0x0080_0000; + const WS_EX_APPWINDOW: u32 = 0x0004_0000; + const WS_EX_TOOLWINDOW: u32 = 0x0000_0080; + const WS_EX_NOACTIVATE: u32 = 0x0800_0000; + const MA_ACTIVATE: isize = 1; + const BLACK_BRUSH: i32 = 4; + + #[repr(C)] + struct WndClassExW { + cb_size: u32, + style: u32, + lpfn_wnd_proc: Option Lresult>, + cb_cls_extra: i32, + cb_wnd_extra: i32, + h_instance: Hinstance, + h_icon: *mut c_void, + h_cursor: *mut c_void, + h_br_background: *mut c_void, + lpsz_menu_name: *const u16, + lpsz_class_name: *const u16, + h_icon_sm: *mut c_void, + } + + #[link(name = "user32")] + extern "system" { + fn CreateWindowExW( + dw_ex_style: u32, + lp_class_name: *const u16, + lp_window_name: *const u16, + dw_style: u32, + x: i32, + y: i32, + n_width: i32, + n_height: i32, + h_wnd_parent: Hwnd, + h_menu: Hmenu, + h_instance: Hinstance, + lp_param: *mut c_void, + ) -> Hwnd; + fn DefWindowProcW(h_wnd: Hwnd, msg: u32, w_param: Wparam, l_param: Lparam) -> Lresult; + fn DestroyWindow(h_wnd: Hwnd) -> Bool; + fn EnumChildWindows( + h_wnd_parent: Hwnd, + lp_enum_func: Option Bool>, + l_param: Lparam, + ) -> Bool; + fn EnumWindows( + lp_enum_func: Option Bool>, + l_param: Lparam, + ) -> Bool; + fn GetClassNameW(h_wnd: Hwnd, lp_class_name: *mut u16, n_max_count: i32) -> i32; + fn GetModuleHandleW(lp_module_name: *const u16) -> Hinstance; + fn GetParent(h_wnd: Hwnd) -> Hwnd; + fn GetWindowLongPtrW(h_wnd: Hwnd, n_index: i32) -> isize; + fn GetWindowThreadProcessId(h_wnd: Hwnd, process_id: *mut u32) -> u32; + fn RegisterClassExW(class: *const WndClassExW) -> Atom; + fn SetParent(h_wnd_child: Hwnd, h_wnd_new_parent: Hwnd) -> Hwnd; + fn SetWindowLongPtrW(h_wnd: Hwnd, n_index: i32, dw_new_long: isize) -> isize; + fn SetWindowPos( + h_wnd: Hwnd, + h_wnd_insert_after: Hwnd, + x: i32, + y: i32, + cx: i32, + cy: i32, + flags: u32, + ) -> Bool; + fn ShowWindow(h_wnd: Hwnd, n_cmd_show: i32) -> Bool; + fn ValidateRect(h_wnd: Hwnd, lp_rect: *const c_void) -> Bool; + fn GetMessageW(lp_msg: *mut Msg, h_wnd: Hwnd, w_msg_filter_min: u32, w_msg_filter_max: u32) -> Bool; + fn TranslateMessage(lp_msg: *const Msg) -> Bool; + fn DispatchMessageW(lp_msg: *const Msg) -> Lresult; + fn PostMessageW(h_wnd: Hwnd, msg: u32, w_param: Wparam, l_param: Lparam) -> Bool; + fn PostThreadMessageW(id_thread: u32, msg: u32, w_param: Wparam, l_param: Lparam) -> Bool; + fn GetCurrentThreadId() -> u32; + } + + #[link(name = "kernel32")] + extern "system" { + fn GetCurrentProcessId() -> u32; + } + + #[repr(C)] + struct Msg { + hwnd: Hwnd, + message: u32, + w_param: Wparam, + l_param: Lparam, + time: u32, + pt_x: i32, + pt_y: i32, + } + + const WM_QUIT: u32 = 0x0012; + const WM_USER_SET_BOUNDS: u32 = 0x0400; + const WM_USER_SET_VISIBLE: u32 = 0x0401; + + #[link(name = "gdi32")] + extern "system" { + fn GetStockObject(index: i32) -> *mut c_void; + } + + static CLASS_REGISTERED: AtomicBool = AtomicBool::new(false); + const CLASS_NAME: &[u16] = &[ + b'O' as u16, b'p' as u16, b'e' as u16, b'n' as u16, b'N' as u16, b'O' as u16, b'W' as u16, + b'I' as u16, b'n' as u16, b't' as u16, b'e' as u16, b'r' as u16, b'n' as u16, b'a' as u16, + b'l' as u16, b'V' as u16, b'i' as u16, b'd' as u16, b'e' as u16, b'o' as u16, 0, + ]; + + unsafe extern "system" fn wnd_proc( + hwnd: Hwnd, + msg: u32, + w_param: Wparam, + l_param: Lparam, + ) -> Lresult { + match msg { + WM_USER_SET_BOUNDS => { + let x = (l_param & 0xFFFF) as i16 as i32; + let y = ((l_param >> 16) & 0xFFFF) as i16 as i32; + let w = (w_param & 0xFFFF) as i32; + let h = ((w_param >> 16) & 0xFFFF) as i32; + let parent = GetParent(hwnd); + if !parent.is_null() { + enable_clip_styles(parent, find_chromium_content_hwnd(parent)); + } + SetWindowPos( + hwnd, + HWND_TOP, + x, + y, + w.max(2), + h.max(2), + SWP_NOACTIVATE | SWP_SHOWWINDOW | SWP_NOCOPYBITS | SWP_NOSENDCHANGING, + ); + 0 + } + WM_USER_SET_VISIBLE => { + let visible = w_param != 0; + ShowWindow(hwnd, if visible { SW_SHOWNOACTIVATE } else { SW_HIDE }); + SetWindowPos( + hwnd, + HWND_TOP, + 0, + 0, + 0, + 0, + SWP_NOACTIVATE + | SWP_NOZORDER + | SWP_NOSIZE + | SWP_NOMOVE + | if visible { + SWP_SHOWWINDOW + } else { + SWP_HIDEWINDOW + }, + ); + 0 + } + // Activation / RawInput capture is handled by the chained platform + // wndproc installed after create (see arm_internal_child_input). + WM_MOUSEACTIVATE => MA_ACTIVATE, + WM_ERASEBKGND => 1, + WM_PAINT => { + ValidateRect(hwnd, null()); + 0 + } + WM_DESTROY => 0, + _ => DefWindowProcW(hwnd, msg, w_param, l_param), + } + } + + unsafe extern "system" fn collect_chromium_child(hwnd: Hwnd, l_param: Lparam) -> Bool { + let out = &mut *(l_param as *mut Hwnd); + if !out.is_null() { + return 1; + } + let mut class_name = [0u16; 64]; + let len = GetClassNameW(hwnd, class_name.as_mut_ptr(), class_name.len() as i32); + if len <= 0 { + return 1; + } + // Chromium's Intermediate D3D / content HWND class names. + let name: String = String::from_utf16_lossy(&class_name[..len as usize]); + if name.contains("Intermediate D3D") + || name.contains("Chrome_RenderWidgetHostHWND") + || name.contains("Chrome_WidgetWin_") + { + *out = hwnd; + return 0; + } + 1 + } + + unsafe fn find_chromium_content_hwnd(parent: Hwnd) -> Option { + let mut found: Hwnd = null_mut(); + EnumChildWindows( + parent, + Some(collect_chromium_child), + &mut found as *mut Hwnd as Lparam, + ); + if found.is_null() { + None + } else { + Some(found) + } + } + + unsafe extern "system" fn collect_gst_vulkan_child(hwnd: Hwnd, l_param: Lparam) -> Bool { + let out = &mut *(l_param as *mut Hwnd); + if !out.is_null() { + return 1; + } + if class_name_is_gst_vulkan(hwnd) { + *out = hwnd; + return 0; + } + 1 + } + + unsafe extern "system" fn collect_gst_vulkan_top_level(hwnd: Hwnd, l_param: Lparam) -> Bool { + let state = &mut *(l_param as *mut (u32, Hwnd)); + if !state.1.is_null() { + return 1; + } + let mut process_id = 0u32; + GetWindowThreadProcessId(hwnd, &mut process_id); + if process_id != state.0 { + return 1; + } + if class_name_is_gst_vulkan(hwnd) { + state.1 = hwnd; + return 0; + } + 1 + } + + unsafe fn class_name_is_gst_vulkan(hwnd: Hwnd) -> bool { + let mut class_name = [0u16; 64]; + let len = GetClassNameW(hwnd, class_name.as_mut_ptr(), class_name.len() as i32); + if len <= 0 { + return false; + } + String::from_utf16_lossy(&class_name[..len as usize]) == "GSTVULKAN" + } + + unsafe fn find_gst_vulkan_under_parent(parent: Hwnd) -> Option { + let mut found: Hwnd = null_mut(); + EnumChildWindows( + parent, + Some(collect_gst_vulkan_child), + &mut found as *mut Hwnd as Lparam, + ); + if found.is_null() { + None + } else { + Some(found) + } + } + + unsafe fn find_process_gst_vulkan_window() -> Option { + let mut state = (GetCurrentProcessId(), null_mut::()); + EnumWindows( + Some(collect_gst_vulkan_top_level), + &mut state as *mut (u32, Hwnd) as Lparam, + ); + if state.1.is_null() { + None + } else { + Some(state.1) + } + } + + /// Hide any top-level GSTVULKAN windows so they never float over Electron. + pub(super) fn suppress_top_level_gst_vulkan_windows() { + unsafe { + let mut state = (GetCurrentProcessId(), 0u32); + EnumWindows( + Some(suppress_top_level_gst_vulkan), + &mut state as *mut (u32, u32) as Lparam, + ); + } + } + + unsafe extern "system" fn suppress_top_level_gst_vulkan(hwnd: Hwnd, l_param: Lparam) -> Bool { + let state = &mut *(l_param as *mut (u32, u32)); + let mut process_id = 0u32; + GetWindowThreadProcessId(hwnd, &mut process_id); + if process_id != state.0 || !class_name_is_gst_vulkan(hwnd) { + return 1; + } + // Already embedded under some parent — leave alone. + if !GetParent(hwnd).is_null() { + return 1; + } + ShowWindow(hwnd, SW_HIDE); + let ex = GetWindowLongPtrW(hwnd, GWL_EXSTYLE); + let desired_ex = (ex | WS_EX_TOOLWINDOW as isize | WS_EX_NOACTIVATE as isize) + & !(WS_EX_APPWINDOW as isize); + if desired_ex != ex { + SetWindowLongPtrW(hwnd, GWL_EXSTYLE, desired_ex); + } + SetWindowPos( + hwnd, + HWND_TOP, + -32_000, + -32_000, + 2, + 2, + SWP_NOACTIVATE | SWP_HIDEWINDOW | SWP_NOSENDCHANGING, + ); + state.1 = state.1.saturating_add(1); + 1 + } + + /// Reparent vulkansink's GSTVULKAN hwnd into our Internal child and size it. + /// Keeps the window hidden while top-level so nothing floats over Electron. + pub(super) fn embed_gst_vulkan_window(parent: Hwnd, width: i32, height: i32) -> bool { + if parent.is_null() { + return false; + } + let width = width.max(2); + let height = height.max(2); + unsafe { + suppress_top_level_gst_vulkan_windows(); + + let Some(vulkan) = find_gst_vulkan_under_parent(parent) + .or_else(|| find_process_gst_vulkan_window()) + else { + return false; + }; + + // Never show as a top-level window — hide first, then reparent, then show. + if GetParent(vulkan) != parent { + ShowWindow(vulkan, SW_HIDE); + SetParent(vulkan, parent); + } + + let style = GetWindowLongPtrW(vulkan, GWL_STYLE); + let cleared = (WS_POPUP + | WS_CAPTION + | WS_THICKFRAME + | WS_MINIMIZEBOX + | WS_MAXIMIZEBOX + | WS_SYSMENU + | WS_BORDER) as isize; + let desired = (style & !cleared) + | (WS_CHILD | WS_VISIBLE | WS_CLIPSIBLINGS | WS_CLIPCHILDREN) as isize; + if desired != style { + SetWindowLongPtrW(vulkan, GWL_STYLE, desired); + } + + let ex = GetWindowLongPtrW(vulkan, GWL_EXSTYLE); + let desired_ex = ex & !(WS_EX_APPWINDOW as isize | WS_EX_TOOLWINDOW as isize); + if desired_ex != ex { + SetWindowLongPtrW(vulkan, GWL_EXSTYLE, desired_ex); + } + + SetWindowPos( + vulkan, + HWND_TOP, + 0, + 0, + width, + height, + SWP_NOACTIVATE | SWP_SHOWWINDOW | SWP_FRAMECHANGED | SWP_NOCOPYBITS, + ); + ShowWindow(vulkan, SW_SHOWNOACTIVATE); + true + } + } + + unsafe fn enable_clip_styles(parent: Hwnd, chromium: Option) { + let parent_style = GetWindowLongPtrW(parent, GWL_STYLE); + let desired_parent = parent_style | (WS_CLIPCHILDREN as isize); + if desired_parent != parent_style { + SetWindowLongPtrW(parent, GWL_STYLE, desired_parent); + } + if let Some(chrome) = chromium { + let chrome_style = GetWindowLongPtrW(chrome, GWL_STYLE); + let desired_chrome = chrome_style | (WS_CLIPSIBLINGS as isize); + if desired_chrome != chrome_style { + SetWindowLongPtrW(chrome, GWL_STYLE, desired_chrome); + } + } + } + + fn ensure_class() -> Result<(), String> { + if CLASS_REGISTERED.load(Ordering::SeqCst) { + return Ok(()); + } + + unsafe { + let instance = GetModuleHandleW(null()); + if instance.is_null() { + return Err("GetModuleHandleW failed for internal renderer class.".to_owned()); + } + + let class = WndClassExW { + cb_size: std::mem::size_of::() as u32, + style: CS_HREDRAW | CS_VREDRAW | CS_OWNDC, + lpfn_wnd_proc: Some(wnd_proc), + cb_cls_extra: 0, + cb_wnd_extra: 0, + h_instance: instance, + h_icon: null_mut(), + h_cursor: null_mut(), + h_br_background: GetStockObject(BLACK_BRUSH), + lpsz_menu_name: null(), + lpsz_class_name: CLASS_NAME.as_ptr(), + h_icon_sm: null_mut(), + }; + + let atom = RegisterClassExW(&class); + if atom == 0 { + // Another thread may have won the race; treat as success if already registered. + if !CLASS_REGISTERED.load(Ordering::SeqCst) { + // ERROR_CLASS_ALREADY_EXISTS == 1410 + // Still mark registered so we do not loop. + } + } + CLASS_REGISTERED.store(true, Ordering::SeqCst); + } + + Ok(()) + } + + pub(super) struct ChildWindow { + hwnd: usize, + thread_id: u32, + join: Option>, + } + + impl std::fmt::Debug for ChildWindow { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("ChildWindow") + .field("hwnd", &self.hwnd) + .field("thread_id", &self.thread_id) + .finish() + } + } + + impl ChildWindow { + pub(super) fn create(parent: usize, bounds: &NativeRenderRect) -> Result { + ensure_class()?; + let parent_hwnd = parent as Hwnd; + if parent_hwnd.is_null() { + return Err("Internal renderer parent HWND is null.".to_owned()); + } + + let (tx, rx) = std::sync::mpsc::channel::>(); + let bounds = bounds.clone(); + let parent_handle = parent; + let join = std::thread::Builder::new() + .name("opennow-internal-video".to_owned()) + .spawn(move || { + let created = + unsafe { create_child_on_thread(parent_handle as Hwnd, &bounds) }; + match created { + Ok(hwnd) => { + let thread_id = unsafe { GetCurrentThreadId() }; + let _ = tx.send(Ok((hwnd as usize, thread_id))); + run_message_loop(hwnd); + } + Err(error) => { + let _ = tx.send(Err(error)); + } + } + }) + .map_err(|error| format!("Failed to spawn internal renderer UI thread: {error}"))?; + + let (hwnd, thread_id) = rx + .recv() + .map_err(|_| "Internal renderer UI thread exited before creating HWND.".to_owned())? + ?; + + Ok(Self { + hwnd, + thread_id, + join: Some(join), + }) + } + + pub(super) fn hwnd(&self) -> usize { + self.hwnd + } + + pub(super) fn set_bounds(&self, bounds: &NativeRenderRect) -> Result<(), String> { + if self.hwnd == 0 { + return Ok(()); + } + // Pack x/y into lParam (signed 16-bit each) and w/h into wParam. + let x = bounds.x.clamp(i16::MIN as i32, i16::MAX as i32) as u16 as u32; + let y = bounds.y.clamp(i16::MIN as i32, i16::MAX as i32) as u16 as u32; + let l_param = ((y as Lparam) << 16) | (x as Lparam); + let w = bounds.width.max(2).min(u16::MAX as i32) as u16 as usize; + let h = bounds.height.max(2).min(u16::MAX as i32) as u16 as usize; + let w_param = (h << 16) | w; + unsafe { + if PostMessageW( + self.hwnd as Hwnd, + WM_USER_SET_BOUNDS, + w_param, + l_param, + ) == 0 + { + return Err("PostMessageW(SET_BOUNDS) failed for internal renderer.".to_owned()); + } + } + Ok(()) + } + + pub(super) fn set_visible(&self, visible: bool) -> Result<(), String> { + if self.hwnd == 0 { + return Ok(()); + } + unsafe { + if PostMessageW( + self.hwnd as Hwnd, + WM_USER_SET_VISIBLE, + if visible { 1 } else { 0 }, + 0, + ) == 0 + { + return Err("PostMessageW(SET_VISIBLE) failed for internal renderer.".to_owned()); + } + } + Ok(()) + } + + pub(super) fn destroy(&mut self) { + if self.hwnd != 0 { + unsafe { + // DestroyWindow posts WM_DESTROY; then quit the UI thread loop. + DestroyWindow(self.hwnd as Hwnd); + PostThreadMessageW(self.thread_id, WM_QUIT, 0, 0); + } + self.hwnd = 0; + } + if let Some(join) = self.join.take() { + let _ = join.join(); + } + } + } + + impl Drop for ChildWindow { + fn drop(&mut self) { + self.destroy(); + } + } + + unsafe fn create_child_on_thread(parent_hwnd: Hwnd, bounds: &NativeRenderRect) -> Result { + let instance = GetModuleHandleW(null()); + let chromium = find_chromium_content_hwnd(parent_hwnd); + enable_clip_styles(parent_hwnd, chromium); + + // Sibling above Intermediate D3D for present. Clip styles punch a hole + // so Chromium does not paint over us. Input uses RawInput on this HWND + // (Electron click-through is unreliable across process boundaries). + // Do not use WS_EX_NOACTIVATE: the first click must activate so RawInput + // capture can arm (same as the floating external renderer window). + let hwnd = CreateWindowExW( + 0, + CLASS_NAME.as_ptr(), + null(), + WS_CHILD | WS_CLIPSIBLINGS | WS_VISIBLE, + bounds.x, + bounds.y, + bounds.width, + bounds.height, + parent_hwnd, + null_mut(), + instance, + null_mut(), + ); + + if hwnd.is_null() { + return Err("CreateWindowExW failed for internal renderer child HWND.".to_owned()); + } + + SetWindowPos( + hwnd, + HWND_TOP, + bounds.x, + bounds.y, + bounds.width, + bounds.height, + SWP_NOACTIVATE | SWP_SHOWWINDOW | SWP_NOCOPYBITS | SWP_NOSENDCHANGING, + ); + + Ok(hwnd) + } + + fn run_message_loop(_hwnd: Hwnd) { + unsafe { + let mut msg = Msg { + hwnd: null_mut(), + message: 0, + w_param: 0, + l_param: 0, + time: 0, + pt_x: 0, + pt_y: 0, + }; + // D3D11 videosink present requires a live message pump on the + // thread that owns the child HWND. Without this, sink stays at 0 fps. + while GetMessageW(&mut msg, null_mut(), 0, 0) > 0 { + TranslateMessage(&msg); + DispatchMessageW(&msg); + } + } + } + + // ChildWindow owns create/bounds/visible/destroy + UI thread lifecycle. +} + +#[cfg(target_os = "macos")] +mod macos_child { + use crate::protocol::NativeRenderRect; + use std::ffi::c_void; + use std::sync::OnceLock; + + pub(super) type NsViewPtr = *mut c_void; + + #[link(name = "objc")] + extern "C" { + fn sel_registerName(name: *const i8) -> *const c_void; + fn objc_getClass(name: *const i8) -> *mut c_void; + fn objc_msgSend(); + } + + // objc_msgSend is variadic; call via transmute per selector signature. + type MsgSend0 = unsafe extern "C" fn(*mut c_void, *const c_void) -> *mut c_void; + type MsgSend1Ptr = unsafe extern "C" fn(*mut c_void, *const c_void, *mut c_void) -> *mut c_void; + type MsgSendRect = unsafe extern "C" fn(*mut c_void, *const c_void, NsRect) -> *mut c_void; + type MsgSendBool = unsafe extern "C" fn(*mut c_void, *const c_void, bool); + type MsgSendVoidPtr = unsafe extern "C" fn(*mut c_void, *const c_void, *mut c_void); + + #[repr(C)] + #[derive(Clone, Copy)] + struct NsPoint { + x: f64, + y: f64, + } + + #[repr(C)] + #[derive(Clone, Copy)] + struct NsSize { + width: f64, + height: f64, + } + + #[repr(C)] + #[derive(Clone, Copy)] + struct NsRect { + origin: NsPoint, + size: NsSize, + } + + struct Selectors { + alloc: *const c_void, + init_with_frame: *const c_void, + add_subview: *const c_void, + set_frame: *const c_void, + set_hidden: *const c_void, + set_wants_layer: *const c_void, + set_autoresizes_subviews: *const c_void, + release: *const c_void, + bounds: *const c_void, + } + + // Objective-C selectors are process-global, immutable runtime tokens returned by + // sel_registerName. Sharing these non-null tokens does not share pointed-to data. + unsafe impl Send for Selectors {} + unsafe impl Sync for Selectors {} + + fn selectors() -> &'static Selectors { + static SELECTORS: OnceLock = OnceLock::new(); + SELECTORS.get_or_init(|| unsafe { + Selectors { + alloc: sel_registerName(b"alloc\0".as_ptr().cast()), + init_with_frame: sel_registerName(b"initWithFrame:\0".as_ptr().cast()), + add_subview: sel_registerName(b"addSubview:\0".as_ptr().cast()), + set_frame: sel_registerName(b"setFrame:\0".as_ptr().cast()), + set_hidden: sel_registerName(b"setHidden:\0".as_ptr().cast()), + set_wants_layer: sel_registerName(b"setWantsLayer:\0".as_ptr().cast()), + set_autoresizes_subviews: sel_registerName( + b"setAutoresizesSubviews:\0".as_ptr().cast(), + ), + release: sel_registerName(b"release\0".as_ptr().cast()), + bounds: sel_registerName(b"bounds\0".as_ptr().cast()), + } + }) + } + + fn msg_send_0(obj: *mut c_void, sel: *const c_void) -> *mut c_void { + unsafe { + let f: MsgSend0 = std::mem::transmute(objc_msgSend as *const ()); + f(obj, sel) + } + } + + fn parent_bounds(parent: NsViewPtr) -> NsRect { + // bounds returns NSRect by value; on x86_64/arm64 macOS this is returned in registers / + // as a struct. Use a dedicated trampoline via objc_msgSend_stret is obsolete on arm64. + type MsgSendBounds = unsafe extern "C" fn(*mut c_void, *const c_void) -> NsRect; + unsafe { + let f: MsgSendBounds = std::mem::transmute(objc_msgSend as *const ()); + f(parent, selectors().bounds) + } + } + + fn to_ns_rect(bounds: &NativeRenderRect, parent: NsViewPtr) -> NsRect { + // Electron reports top-left client coords in device pixels. NSView is bottom-left + // in points; approximate by treating incoming coords as points relative to parent + // bounds (Electron already DPI-scales the rect we receive). + let parent_bounds = parent_bounds(parent); + let height = bounds.height as f64; + let y = parent_bounds.size.height - (bounds.y as f64) - height; + NsRect { + origin: NsPoint { + x: bounds.x as f64, + y: y.max(0.0), + }, + size: NsSize { + width: bounds.width as f64, + height, + }, + } + } + + pub(super) fn create_child(parent: usize, bounds: &NativeRenderRect) -> Result { + let parent_view = parent as NsViewPtr; + if parent_view.is_null() { + return Err("Internal renderer parent NSView is null.".to_owned()); + } + + unsafe { + let class = objc_getClass(b"NSView\0".as_ptr().cast()); + if class.is_null() { + return Err("objc_getClass(NSView) failed.".to_owned()); + } + + let sels = selectors(); + let alloc = msg_send_0(class, sels.alloc); + if alloc.is_null() { + return Err("NSView alloc failed.".to_owned()); + } + + let frame = to_ns_rect(bounds, parent_view); + let init: MsgSendRect = std::mem::transmute(objc_msgSend as *const ()); + let view = init(alloc, sels.init_with_frame, frame); + if view.is_null() { + return Err("NSView initWithFrame failed.".to_owned()); + } + + let set_bool: MsgSendBool = std::mem::transmute(objc_msgSend as *const ()); + set_bool(view, sels.set_wants_layer, true); + set_bool(view, sels.set_autoresizes_subviews, false); + set_bool(view, sels.set_hidden, false); + + let add: MsgSend1Ptr = std::mem::transmute(objc_msgSend as *const ()); + add(parent_view, sels.add_subview, view); + + Ok(view) + } + } + + pub(super) fn set_bounds(view: NsViewPtr, bounds: &NativeRenderRect) -> Result<(), String> { + if view.is_null() { + return Ok(()); + } + // Parent is needed for Y-flip; store is not available here — use frame in parent + // coordinates assuming the same parent. Read superview. + unsafe { + let sels = selectors(); + let superview_sel = sel_registerName(b"superview\0".as_ptr().cast()); + let parent = msg_send_0(view, superview_sel); + let frame = if parent.is_null() { + NsRect { + origin: NsPoint { + x: bounds.x as f64, + y: bounds.y as f64, + }, + size: NsSize { + width: bounds.width as f64, + height: bounds.height as f64, + }, + } + } else { + to_ns_rect(bounds, parent) + }; + let set_frame: MsgSendRect = std::mem::transmute(objc_msgSend as *const ()); + set_frame(view, sels.set_frame, frame); + } + Ok(()) + } + + pub(super) fn set_visible(view: NsViewPtr, visible: bool) -> Result<(), String> { + if view.is_null() { + return Ok(()); + } + unsafe { + let set_bool: MsgSendBool = std::mem::transmute(objc_msgSend as *const ()); + set_bool(view, selectors().set_hidden, !visible); + } + Ok(()) + } + + pub(super) fn destroy(view: NsViewPtr) { + if view.is_null() { + return; + } + unsafe { + let sels = selectors(); + let remove_sel = sel_registerName(b"removeFromSuperview\0".as_ptr().cast()); + let _ = msg_send_0(view, remove_sel); + let _ = msg_send_0(view, sels.release); + } + } +} + +#[cfg(target_os = "linux")] +mod linux_child { + use crate::protocol::NativeRenderRect; + use std::ffi::c_void; + use std::ptr::null_mut; + + #[derive(Debug)] + pub(super) struct XWindow { + pub(super) display: usize, + pub(super) window: u64, + parent: u64, + } + + // Minimal X11 FFI — enough to create a child window for GstVideoOverlay. + #[repr(C)] + struct XSetWindowAttributes { + background_pixmap: u64, + background_pixel: u64, + border_pixmap: u64, + border_pixel: u64, + bit_gravity: i32, + win_gravity: i32, + backing_store: i32, + backing_planes: u64, + backing_pixel: u64, + save_under: i32, + event_mask: i64, + do_not_propagate_mask: i64, + override_redirect: i32, + colormap: u64, + cursor: u64, + } + + #[link(name = "X11")] + extern "C" { + fn XOpenDisplay(display_name: *const i8) -> *mut c_void; + fn XDefaultScreen(display: *mut c_void) -> i32; + fn XDefaultVisual(display: *mut c_void, screen: i32) -> *mut c_void; + fn XDefaultDepth(display: *mut c_void, screen: i32) -> i32; + fn XDefaultColormap(display: *mut c_void, screen: i32) -> u64; + fn XBlackPixel(display: *mut c_void, screen: i32) -> u64; + fn XCreateWindow( + display: *mut c_void, + parent: u64, + x: i32, + y: i32, + width: u32, + height: u32, + border_width: u32, + depth: i32, + class: u32, + visual: *mut c_void, + valuemask: u64, + attributes: *mut XSetWindowAttributes, + ) -> u64; + fn XMapWindow(display: *mut c_void, window: u64) -> i32; + fn XUnmapWindow(display: *mut c_void, window: u64) -> i32; + fn XMoveResizeWindow( + display: *mut c_void, + window: u64, + x: i32, + y: i32, + width: u32, + height: u32, + ) -> i32; + fn XRaiseWindow(display: *mut c_void, window: u64) -> i32; + fn XClearWindow(display: *mut c_void, window: u64) -> i32; + fn XDestroyWindow(display: *mut c_void, window: u64) -> i32; + fn XFlush(display: *mut c_void) -> i32; + fn XSync(display: *mut c_void, discard: i32) -> i32; + fn XSelectInput(display: *mut c_void, window: u64, event_mask: i64) -> i32; + } + + const INPUT_OUTPUT: u32 = 1; + const CW_BACK_PIXEL: u64 = 0x0002; + const CW_EVENT_MASK: u64 = 0x0800; + const CW_COLORMAP: u64 = 0x2000; + // Exposure + StructureNotify so GstVideoOverlay can redraw after map/resize. + // No pointer/keyboard masks — Electron owns input in internal mode. + const EXPOSURE_MASK: i64 = 0x0000_8000; + const STRUCTURE_NOTIFY_MASK: i64 = 0x0002_0000; + const EVENT_MASK: i64 = EXPOSURE_MASK | STRUCTURE_NOTIFY_MASK; + + fn wayland_session_active() -> bool { + std::env::var_os("WAYLAND_DISPLAY") + .filter(|value| !value.is_empty()) + .is_some() + } + + fn x11_unavailable_message() -> String { + if wayland_session_active() { + "Native internal renderer requires X11 or XWayland. Pure Wayland embedding is not supported yet — launch under X11, or set GDK_BACKEND=x11 / use an XWayland session." + .to_owned() + } else { + "XOpenDisplay failed; native internal renderer requires a working X11 display (DISPLAY unset or unreachable)." + .to_owned() + } + } + + pub(super) fn create_child(parent: usize, bounds: &NativeRenderRect) -> Result { + unsafe { + let display = XOpenDisplay(null_mut()); + if display.is_null() { + return Err(x11_unavailable_message()); + } + + let screen = XDefaultScreen(display); + let visual = XDefaultVisual(display, screen); + let depth = XDefaultDepth(display, screen); + let mut attrs = XSetWindowAttributes { + background_pixmap: 0, + background_pixel: XBlackPixel(display, screen), + border_pixmap: 0, + border_pixel: 0, + bit_gravity: 0, + win_gravity: 0, + backing_store: 0, + backing_planes: 0, + backing_pixel: 0, + save_under: 0, + event_mask: EVENT_MASK, + do_not_propagate_mask: 0, + override_redirect: 0, + colormap: XDefaultColormap(display, screen), + cursor: 0, + }; + + let window = XCreateWindow( + display, + parent as u64, + bounds.x, + bounds.y, + bounds.width.max(2) as u32, + bounds.height.max(2) as u32, + 0, + depth, + INPUT_OUTPUT, + visual, + CW_BACK_PIXEL | CW_EVENT_MASK | CW_COLORMAP, + &mut attrs, + ); + + if window == 0 { + return Err("XCreateWindow failed for internal renderer child.".to_owned()); + } + + XSelectInput(display, window, EVENT_MASK); + XMapWindow(display, window); + XRaiseWindow(display, window); + // Ensure the child is mapped and sized before GstVideoOverlay binds. + XSync(display, 0); + XClearWindow(display, window); + XFlush(display); + + Ok(XWindow { + display: display as usize, + window, + parent: parent as u64, + }) + } + } + + pub(super) fn set_bounds(window: &XWindow, bounds: &NativeRenderRect) -> Result<(), String> { + if window.display == 0 || window.window == 0 { + return Ok(()); + } + let display = window.display as *mut c_void; + unsafe { + XMoveResizeWindow( + display, + window.window, + bounds.x, + bounds.y, + bounds.width.max(2) as u32, + bounds.height.max(2) as u32, + ); + XRaiseWindow(display, window.window); + XSync(display, 0); + XFlush(display); + } + let _ = window.parent; + Ok(()) + } + + pub(super) fn set_visible(window: &XWindow, visible: bool) -> Result<(), String> { + if window.display == 0 || window.window == 0 { + return Ok(()); + } + let display = window.display as *mut c_void; + unsafe { + if visible { + XMapWindow(display, window.window); + XRaiseWindow(display, window.window); + XClearWindow(display, window.window); + } else { + XUnmapWindow(display, window.window); + } + XSync(display, 0); + XFlush(display); + } + Ok(()) + } + + pub(super) fn destroy(window: &mut XWindow) { + if window.display == 0 { + return; + } + let display = window.display as *mut c_void; + unsafe { + if window.window != 0 { + XDestroyWindow(display, window.window); + window.window = 0; + } + // Intentionally keep the display open for process lifetime; closing here + // can race with other X users in the same process. + XFlush(display); + } + } +} diff --git a/native/opennow-streamer/src/main.rs b/native/opennow-streamer/src/main.rs index 22dfa27a5..abf8216a9 100644 --- a/native/opennow-streamer/src/main.rs +++ b/native/opennow-streamer/src/main.rs @@ -3,7 +3,8 @@ mod backend; #[cfg(feature = "gstreamer")] mod gstreamer_backend; -#[cfg(feature = "gstreamer")] +// Present/input policy helpers are pure Rust and stay available for unit tests +// even when the optional GStreamer feature is off. mod gstreamer_config; #[cfg(feature = "gstreamer")] mod gstreamer_input; @@ -15,10 +16,15 @@ mod gstreamer_pipeline; mod gstreamer_platform; #[cfg(feature = "gstreamer")] mod gstreamer_transitions; +#[cfg(feature = "gstreamer")] +mod internal_renderer; mod input; +mod nvst_video; mod protocol; mod shortcuts; mod sdp; +#[cfg(target_os = "windows")] +mod windows_dpi; use serde::Serialize; use serde_json::Value; @@ -123,6 +129,9 @@ fn handle_command( } fn main() -> io::Result<()> { + #[cfg(target_os = "windows")] + windows_dpi::enable_per_monitor_awareness(); + let stdin = io::stdin(); let (event_sender, event_receiver) = mpsc::channel::(); let event_writer = thread::spawn(move || { diff --git a/native/opennow-streamer/src/nvst_video.rs b/native/opennow-streamer/src/nvst_video.rs new file mode 100644 index 000000000..3415d24ab --- /dev/null +++ b/native/opennow-streamer/src/nvst_video.rs @@ -0,0 +1,537 @@ +//! Classic NVST / Mjolnir UDP video receive scaffold (GO-with-Moonlight-hypothesis). +//! +//! Pipeline (intended): +//! UDP bind → hole-punch PING → (SRTP decrypt) → RTP(+ext) → NV_VIDEO_PACKET → +//! assemble AUs on FLAG_EOF → appsrc (Annex-B) → h265parse/h264parse → decoder → sink +//! +//! SRTP: master = AES-256 key[32] || salt[12] where salt = key_id as BE u32 zero-padded +//! to 12 bytes (`%024x`). Prefer AEAD_AES_256_GCM. This scaffold does **not** link +//! libsrtp; when decrypt is unavailable it logs once and parses cleartext RTP so the +//! receive/assemble/appsrc path can be exercised. A GStreamer `srtpdec` branch can be +//! added later once the wire profile is confirmed on a live pcap. + +#![cfg_attr(not(feature = "gstreamer"), allow(dead_code))] + +#[cfg(feature = "gstreamer")] +use crate::gstreamer_backend::send_log; +#[cfg(feature = "gstreamer")] +use crate::protocol::{Event, NvstVideoSession}; +#[cfg(feature = "gstreamer")] +use gstreamer as gst; +#[cfg(feature = "gstreamer")] +use gstreamer::prelude::*; +#[cfg(feature = "gstreamer")] +use std::io; +#[cfg(feature = "gstreamer")] +use std::net::{SocketAddr, UdpSocket}; +#[cfg(feature = "gstreamer")] +use std::sync::atomic::{AtomicBool, Ordering}; +#[cfg(feature = "gstreamer")] +use std::sync::mpsc::Sender; +#[cfg(feature = "gstreamer")] +use std::sync::Arc; +#[cfg(feature = "gstreamer")] +use std::thread::{self, JoinHandle}; +#[cfg(feature = "gstreamer")] +use std::time::Duration; + +/// Moonlight / GameStream `NV_VIDEO_PACKET` size (little-endian fields). +pub const NV_VIDEO_PACKET_LEN: usize = 16; +/// `FLAG_EOF` — last packet of a frame (assemble AU). +pub const FLAG_EOF: u8 = 0x02; +/// `FLAG_SOF` — first packet of a frame. +#[allow(dead_code)] +pub const FLAG_SOF: u8 = 0x04; +/// `FLAG_CONTAINS_PIC_DATA`. +#[allow(dead_code)] +pub const FLAG_CONTAINS_PIC_DATA: u8 = 0x01; + +/// Pack AES-256 key + key ID into the 44-byte libsrtp master key||salt. +/// +/// Salt is `key_id` as a big-endian u32, zero-padded to 12 bytes (`printf`-style `%024x`). +pub fn pack_srtp_master_key_salt(aes_key: &[u8; 32], key_id: u32) -> [u8; 44] { + let mut out = [0u8; 44]; + out[..32].copy_from_slice(aes_key); + out[40..44].copy_from_slice(&key_id.to_be_bytes()); + out +} + +/// Decode a 64-hex AES-256 key string into 32 bytes. +pub fn parse_aes_key_hex(hex: &str) -> Result<[u8; 32], String> { + let trimmed = hex.trim(); + if trimmed.len() != 64 { + return Err(format!( + "srtpAesKeyHex must be 64 hex chars, got {}", + trimmed.len() + )); + } + let mut out = [0u8; 32]; + for (i, chunk) in trimmed.as_bytes().chunks(2).enumerate() { + let s = std::str::from_utf8(chunk).map_err(|e| e.to_string())?; + out[i] = u8::from_str_radix(s, 16) + .map_err(|e| format!("Invalid hex in srtpAesKeyHex at byte {i}: {e}"))?; + } + Ok(out) +} + +/// Parsed Moonlight-hypothesis `NV_VIDEO_PACKET` (LE). +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct NvVideoPacket { + pub stream_packet_index: u32, + pub frame_index: u32, + pub flags: u8, + pub extra_flags: u8, + pub multi_fec_flags: u8, + pub multi_fec_blocks: u8, + pub fec_info: u32, +} + +impl NvVideoPacket { + pub fn parse(bytes: &[u8]) -> Option { + if bytes.len() < NV_VIDEO_PACKET_LEN { + return None; + } + Some(Self { + stream_packet_index: u32::from_le_bytes(bytes[0..4].try_into().ok()?), + frame_index: u32::from_le_bytes(bytes[4..8].try_into().ok()?), + flags: bytes[8], + extra_flags: bytes[9], + multi_fec_flags: bytes[10], + multi_fec_blocks: bytes[11], + fec_info: u32::from_le_bytes(bytes[12..16].try_into().ok()?), + }) + } + + /// FEC / non-picture packets use `flags == 0` in the Moonlight hypothesis. + pub fn is_fec_or_empty(&self) -> bool { + self.flags == 0 + } + + pub fn is_eof(&self) -> bool { + self.flags & FLAG_EOF != 0 + } +} + +/// Strip standard RTP header (+ optional 4-byte extension pad when X is set) and +/// return the payload starting at `NV_VIDEO_PACKET`. +/// +/// Layout hypothesis (post-SRTP): `12B RTP + 4B ext pad if X + 16B NV_VIDEO_PACKET + payload`. +pub fn strip_rtp_header(packet: &[u8]) -> Option<&[u8]> { + if packet.len() < 12 { + return None; + } + let v_pxcc = packet[0]; + let version = v_pxcc >> 6; + if version != 2 { + return None; + } + let has_padding = (v_pxcc & 0x20) != 0; + let has_extension = (v_pxcc & 0x10) != 0; + let csrc_count = (v_pxcc & 0x0f) as usize; + let mut offset = 12 + csrc_count * 4; + if packet.len() < offset { + return None; + } + if has_extension { + // Moonlight-hypothesis: fixed 4-byte extension pad (not full RFC 5285 walk). + offset = offset.checked_add(4)?; + if packet.len() < offset { + return None; + } + } + + let mut payload = &packet[offset..]; + if has_padding { + let pad_len = *payload.last()? as usize; + if pad_len == 0 || pad_len > payload.len() { + return None; + } + payload = &payload[..payload.len() - pad_len]; + } + Some(payload) +} + +/// Parse RTP → NV_VIDEO_PACKET → media payload. Returns `None` for FEC (`flags==0`) +/// or malformed packets. +pub fn parse_nvst_rtp_payload(packet: &[u8]) -> Option<(NvVideoPacket, &[u8])> { + let after_rtp = strip_rtp_header(packet)?; + let header = NvVideoPacket::parse(after_rtp)?; + if header.is_fec_or_empty() { + return None; + } + let media = after_rtp.get(NV_VIDEO_PACKET_LEN..)?; + Some((header, media)) +} + +/// Assembles Annex-B access units from NVST RTP payloads (Moonlight hypothesis). +#[derive(Debug, Default)] +pub struct NvstFrameAssembler { + current_frame: Option, + buffer: Vec, +} + +impl NvstFrameAssembler { + pub fn new() -> Self { + Self::default() + } + + /// Push one media packet. Returns a completed AU when `FLAG_EOF` is set. + pub fn push(&mut self, header: &NvVideoPacket, payload: &[u8]) -> Option> { + if header.is_fec_or_empty() { + return None; + } + + match self.current_frame { + Some(frame) if frame != header.frame_index => { + self.buffer.clear(); + self.current_frame = Some(header.frame_index); + } + None => { + self.current_frame = Some(header.frame_index); + } + _ => {} + } + + self.buffer.extend_from_slice(payload); + + if header.is_eof() { + let au = std::mem::take(&mut self.buffer); + self.current_frame = None; + if au.is_empty() { + None + } else { + Some(au) + } + } else { + None + } + } +} + +/// Caps string for Annex-B appsrc feeding `h265parse` / `h264parse`. +pub fn annexb_appsrc_caps(codec: &str) -> &'static str { + match codec.to_ascii_uppercase().as_str() { + "H264" => "video/x-h264,stream-format=byte-stream,alignment=au", + _ => "video/x-h265,stream-format=byte-stream,alignment=au", + } +} + +/// Build description for a GStreamer branch that would decrypt SRTP then feed RTP +/// into an appsrc-style path. Documented for future `srtpdec` wiring; the live +/// scaffold currently decrypts (or skips) in the UDP thread and pushes Annex-B AUs. +pub fn srtpdec_pipeline_branch_hint(codec: &str) -> String { + let caps = annexb_appsrc_caps(codec); + let parser = match codec.to_ascii_uppercase().as_str() { + "H264" => "h264parse", + _ => "h265parse", + }; + format!( + "appsrc name=nvst-annexb caps=\"{caps}\" is-live=true format=time ! \ + {parser} ! …decoder… ! …sink… \ + (SRTP: prefer libsrtp AEAD_AES_256_GCM with pack_srtp_master_key_salt; \ + GStreamer srtpdec may be wired later — do not use appsrc caps=application/x-rtp \ + named nvst-rtp for assembled AUs)" + ) +} + +#[cfg(feature = "gstreamer")] +pub(crate) struct NvstVideoReceiveHandle { + stop: Arc, + join: Option>, +} + +#[cfg(feature = "gstreamer")] +impl std::fmt::Debug for NvstVideoReceiveHandle { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("NvstVideoReceiveHandle") + .field("stop", &self.stop.load(Ordering::SeqCst)) + .field("join", &self.join.as_ref().map(|_| "JoinHandle")) + .finish() + } +} + +#[cfg(feature = "gstreamer")] +impl NvstVideoReceiveHandle { + pub(crate) fn stop(mut self) { + self.stop.store(true, Ordering::SeqCst); + if let Some(join) = self.join.take() { + let _ = join.join(); + } + } +} + +#[cfg(feature = "gstreamer")] +impl Drop for NvstVideoReceiveHandle { + fn drop(&mut self) { + self.stop.store(true, Ordering::SeqCst); + if let Some(join) = self.join.take() { + let _ = join.join(); + } + } +} + +/// Spawn UDP bind → PING hole-punch → recv → parse → push Annex-B AUs into `appsrc`. +#[cfg(feature = "gstreamer")] +pub(crate) fn spawn_nvst_udp_receive( + session: NvstVideoSession, + appsrc: gst::Element, + event_sender: Option>, +) -> Result { + let aes_key = parse_aes_key_hex(&session.srtp_aes_key_hex)?; + let master = pack_srtp_master_key_salt(&aes_key, session.srtp_key_id); + let _ = master; // reserved for future libsrtp / srtpdec install + + let peer: SocketAddr = format!("{}:{}", session.video_peer_ip, session.video_peer_port) + .parse() + .map_err(|e| format!("Invalid nvstVideo peer address: {e}"))?; + + let bind_addr = SocketAddr::from(([0, 0, 0, 0], session.client_udp_port)); + let socket = UdpSocket::bind(bind_addr) + .map_err(|e| format!("Failed to bind NVST UDP {bind_addr}: {e}"))?; + socket + .set_read_timeout(Some(Duration::from_millis(250))) + .map_err(|e| format!("Failed to set NVST UDP read timeout: {e}"))?; + + let ping = session + .ping_payload + .as_deref() + .filter(|s| !s.is_empty()) + .unwrap_or("PING"); + match socket.send_to(ping.as_bytes(), peer) { + Ok(_) => send_log( + &event_sender, + "info", + format!( + "NVST video hole-punch sent ({ping_len} B) to {peer} from {bind_addr}.", + ping_len = ping.len() + ), + ), + Err(e) => send_log( + &event_sender, + "warn", + format!("NVST video hole-punch to {peer} failed: {e}"), + ), + } + + send_log( + &event_sender, + "info", + format!( + "NVST SRTP scaffold: master key/salt packed (44 B) for keyId {}; \ + libsrtp/srtpdec not linked — parsing cleartext RTP if packets arrive undecrypted. {}", + session.srtp_key_id, + srtpdec_pipeline_branch_hint(session.codec.as_deref().unwrap_or("H265")) + ), + ); + + let stop = Arc::new(AtomicBool::new(false)); + let stop_flag = stop.clone(); + let join = thread::Builder::new() + .name("nvst-udp-video".to_owned()) + .spawn(move || { + nvst_udp_recv_loop(socket, appsrc, event_sender, stop_flag); + }) + .map_err(|e| format!("Failed to spawn NVST UDP thread: {e}"))?; + + Ok(NvstVideoReceiveHandle { + stop, + join: Some(join), + }) +} + +#[cfg(feature = "gstreamer")] +fn nvst_udp_recv_loop( + socket: UdpSocket, + appsrc: gst::Element, + event_sender: Option>, + stop: Arc, +) { + let mut buf = [0u8; 2048]; + let mut assembler = NvstFrameAssembler::new(); + let mut first_packet_logged = false; + let mut first_au_logged = false; + let mut decrypt_fail_logged = false; + let mut cleartext_notice_logged = false; + + while !stop.load(Ordering::SeqCst) { + match socket.recv_from(&mut buf) { + Ok((len, _from)) => { + if !first_packet_logged { + first_packet_logged = true; + send_log( + &event_sender, + "info", + format!("NVST UDP received first packet ({len} B)."), + ); + } + if !cleartext_notice_logged { + cleartext_notice_logged = true; + send_log( + &event_sender, + "info", + "NVST SRTP decrypt unavailable in scaffold; attempting cleartext RTP parse." + .to_owned(), + ); + } + + let packet = &buf[..len]; + if packet.first().map(|b| b >> 6) != Some(2) { + if !decrypt_fail_logged { + decrypt_fail_logged = true; + send_log( + &event_sender, + "warn", + "NVST UDP packet does not look like cleartext RTP (version != 2); \ + SRTP decrypt required — continuing to listen." + .to_owned(), + ); + } + continue; + } + + let Some((header, payload)) = parse_nvst_rtp_payload(packet) else { + continue; + }; + let Some(au) = assembler.push(&header, payload) else { + continue; + }; + if !first_au_logged { + first_au_logged = true; + send_log( + &event_sender, + "info", + format!( + "NVST assembled first Annex-B AU ({} B, frameIndex={}).", + au.len(), + header.frame_index + ), + ); + } + if let Err(err) = push_au_to_appsrc(&appsrc, &au) { + send_log( + &event_sender, + "warn", + format!("NVST appsrc push failed: {err}"), + ); + } + } + Err(e) if e.kind() == io::ErrorKind::WouldBlock || e.kind() == io::ErrorKind::TimedOut => { + continue; + } + Err(e) => { + if !stop.load(Ordering::SeqCst) { + send_log( + &event_sender, + "warn", + format!("NVST UDP recv error: {e}"), + ); + } + break; + } + } + } +} + +#[cfg(feature = "gstreamer")] +fn push_au_to_appsrc(appsrc: &gst::Element, au: &[u8]) -> Result<(), String> { + let mut buffer = gst::Buffer::from_mut_slice(au.to_vec()); + { + let buffer = buffer + .get_mut() + .ok_or_else(|| "NVST appsrc buffer not writable".to_owned())?; + buffer.set_pts(gst::ClockTime::NONE); + buffer.set_dts(gst::ClockTime::NONE); + } + let flow = appsrc.emit_by_name::("push-buffer", &[&buffer]); + if flow != gst::FlowReturn::Ok { + return Err(format!("appsrc push-buffer returned {flow:?}")); + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn pack_srtp_master_key_salt_matches_docs_key_id() { + // Docs: key_id 2664076126 → salt ends 9ECA935E (`%024x` → 00000000000000009ECA935E). + let key = [0xABu8; 32]; + let packed = pack_srtp_master_key_salt(&key, 2664076126); + assert_eq!(&packed[..32], &key); + assert_eq!(&packed[32..40], &[0u8; 8]); + assert_eq!(&packed[40..44], &[0x9E, 0xCA, 0x93, 0x5E]); + } + + #[test] + fn pack_srtp_additional_doc_key_ids() { + let key = [0u8; 32]; + let a = pack_srtp_master_key_salt(&key, 1664590642); + assert_eq!(&a[40..44], &[0x63, 0x37, 0xA3, 0x32]); + let b = pack_srtp_master_key_salt(&key, 2478780175); + assert_eq!(&b[40..44], &[0x93, 0xBF, 0x2F, 0x0F]); + } + + #[test] + fn parse_nv_video_packet_synthetic() { + let mut bytes = [0u8; 16]; + bytes[0..4].copy_from_slice(&0x11223344u32.to_le_bytes()); + bytes[4..8].copy_from_slice(&7u32.to_le_bytes()); + bytes[8] = FLAG_SOF | FLAG_EOF | FLAG_CONTAINS_PIC_DATA; + bytes[9] = 0x10; + bytes[10] = 0x20; + bytes[11] = 0x30; + bytes[12..16].copy_from_slice(&0xAABBCCDDu32.to_le_bytes()); + + let pkt = NvVideoPacket::parse(&bytes).expect("parse"); + assert_eq!(pkt.stream_packet_index, 0x11223344); + assert_eq!(pkt.frame_index, 7); + assert!(pkt.is_eof()); + assert!(!pkt.is_fec_or_empty()); + assert_eq!(pkt.extra_flags, 0x10); + assert_eq!(pkt.fec_info, 0xAABBCCDD); + } + + #[test] + fn fec_flags_zero_ignored_by_parser() { + let mut packet = vec![0x80u8, 0x60, 0x00, 0x01, 0, 0, 0, 0, 0, 0, 0, 0]; + packet.extend_from_slice(&[0u8; 16]); + packet.extend_from_slice(&[0xDE, 0xAD]); + assert!(parse_nvst_rtp_payload(&packet).is_none()); + } + + #[test] + fn assemble_frame_on_eof() { + let mut asm = NvstFrameAssembler::new(); + let sof = NvVideoPacket { + stream_packet_index: 1, + frame_index: 3, + flags: FLAG_SOF | FLAG_CONTAINS_PIC_DATA, + extra_flags: 0, + multi_fec_flags: 0, + multi_fec_blocks: 0, + fec_info: 0, + }; + let eof = NvVideoPacket { + flags: FLAG_EOF | FLAG_CONTAINS_PIC_DATA, + stream_packet_index: 2, + ..sof + }; + assert!(asm.push(&sof, b"AA").is_none()); + let au = asm.push(&eof, b"BB").expect("AU"); + assert_eq!(au, b"AABB"); + } + + #[test] + fn strip_rtp_with_extension_pad() { + let mut packet = vec![0x90u8, 0x60, 0x00, 0x02, 0, 0, 0, 0, 0, 0, 0, 0]; + packet.extend_from_slice(&[0xBE, 0xDE, 0x00, 0x00]); + let mut nv = [0u8; 16]; + nv[8] = FLAG_EOF | FLAG_CONTAINS_PIC_DATA; + packet.extend_from_slice(&nv); + packet.extend_from_slice(b"NAL"); + let (hdr, payload) = parse_nvst_rtp_payload(&packet).expect("parse"); + assert!(hdr.is_eof()); + assert_eq!(payload, b"NAL"); + } +} diff --git a/native/opennow-streamer/src/protocol.rs b/native/opennow-streamer/src/protocol.rs index c3b33d6bd..a3f79b572 100644 --- a/native/opennow-streamer/src/protocol.rs +++ b/native/opennow-streamer/src/protocol.rs @@ -34,11 +34,36 @@ pub struct CommandEnvelope { } #[derive(Debug, Clone, Deserialize)] +#[serde(rename_all = "camelCase")] pub struct NativeStreamerSessionContext { pub session: SessionInfo, pub settings: StreamSettings, #[serde(default)] pub shortcuts: NativeStreamerShortcutBindings, + /// Classic Mjolnir UDP video (post-SETUP peer + SRTP material). When set, + /// the GStreamer backend receives video over UDP while keeping webrtcbin + /// for SCTP datachannels / input. + #[serde(default, rename = "nvstVideo")] + pub nvst_video: Option, +} + +/// Classic NVST UDP video session parameters (Moonlight-hypothesis scaffold). +#[derive(Debug, Clone, Deserialize)] +#[serde(rename_all = "camelCase")] +#[cfg_attr(not(feature = "gstreamer"), allow(dead_code))] +pub struct NvstVideoSession { + pub client_udp_port: u16, + pub video_peer_ip: String, + pub video_peer_port: u16, + /// 64 hex chars = 32-byte AES-256 key. + pub srtp_aes_key_hex: String, + pub srtp_key_id: u32, + /// Optional SETUP `X-Nv-Ping-Payload` token; when absent, hole-punch sends `PING`. + #[serde(default)] + pub ping_payload: Option, + /// `H265` / `H264` (defaults to session settings codec when omitted). + #[serde(default)] + pub codec: Option, } #[derive(Debug, Clone, Deserialize)] @@ -47,6 +72,8 @@ pub struct SessionInfo { pub session_id: String, pub server_ip: String, #[serde(default)] + pub ice_servers: Vec, + #[serde(default)] pub media_connection_info: Option, #[allow(dead_code)] #[serde(default)] @@ -59,6 +86,18 @@ pub struct SessionInfo { pub finalized_streaming_features: Option, } +#[derive(Debug, Clone, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct IceServer { + pub urls: Vec, + #[allow(dead_code)] + #[serde(default)] + pub username: Option, + #[allow(dead_code)] + #[serde(default)] + pub credential: Option, +} + #[derive(Debug, Clone, Deserialize)] #[serde(rename_all = "camelCase")] pub struct MediaConnectionInfo { @@ -265,7 +304,7 @@ pub struct NativeRenderSurface { pub show_stats: bool, } -#[derive(Debug, Clone, Deserialize, Serialize)] +#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct NativeRenderRect { pub x: i32, @@ -662,4 +701,39 @@ mod tests { assert_eq!(value["transition"]["transitionType"], "sink-caps-change"); assert_eq!(value["transition"]["highFpsRisk"], true); } + + #[test] + fn nvst_video_session_deserializes_camel_case() { + let value = serde_json::json!({ + "session": { + "sessionId": "s1", + "serverIp": "1.2.3.4" + }, + "settings": { + "resolution": "1920x1080", + "fps": 60, + "maxBitrateMbps": 50, + "codec": "H265", + "colorQuality": "8bit_420", + "enableCloudGsync": false + }, + "shortcuts": {}, + "nvstVideo": { + "clientUdpPort": 49005, + "videoPeerIp": "10.0.0.1", + "videoPeerPort": 5004, + "srtpAesKeyHex": "00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff", + "srtpKeyId": 2664076126u32, + "pingPayload": "token", + "codec": "H265" + } + }); + let ctx: NativeStreamerSessionContext = + serde_json::from_value(value).expect("deserialize"); + let nvst = ctx.nvst_video.expect("nvstVideo present"); + assert_eq!(nvst.client_udp_port, 49005); + assert_eq!(nvst.video_peer_port, 5004); + assert_eq!(nvst.srtp_key_id, 2664076126); + assert_eq!(nvst.ping_payload.as_deref(), Some("token")); + } } diff --git a/native/opennow-streamer/src/sdp.rs b/native/opennow-streamer/src/sdp.rs index 1f3736c89..5517d27bf 100644 --- a/native/opennow-streamer/src/sdp.rs +++ b/native/opennow-streamer/src/sdp.rs @@ -9,7 +9,8 @@ use crate::protocol::{ColorQuality, VideoCodec}; // Match the official web client's 240 FPS profile. Disabling split encode at // this frame rate can leave H265 streams smeared because the server/client // repair and frame-state assumptions no longer line up. -const ENABLE_OUT_OF_FOCUS_FPS_ADJUSTMENT: bool = false; +// Official Nvsc dumps set adjustStreamingFpsDuringOutOfFocus:1 (matches TS builder). +const ENABLE_OUT_OF_FOCUS_FPS_ADJUSTMENT: bool = true; const ENABLE_240_FPS_SPLIT_ENCODE: bool = true; const ENABLE_DYNAMIC_SPLIT_ENCODE_UPDATES: bool = true; @@ -656,11 +657,17 @@ pub fn munge_answer_sdp(sdp: &str, max_bitrate_kbps: u32) -> String { } pub fn build_nvst_sdp(params: &NvstParams) -> String { - let min_bitrate = 5000.max(params.max_bitrate_kbps * 35 / 100); - let initial_bitrate = min_bitrate.max(params.max_bitrate_kbps * 70 / 100); - let is_high_fps = params.fps >= 90; + // Align bitrate floor/startup with the TS WebRTC companion builder + // (official web client uses a 4 Mbps floor and ~max/4 startup). + const OFFICIAL_MIN_BITRATE_KBPS: u32 = 4000; + let max_bitrate = params.max_bitrate_kbps.max(OFFICIAL_MIN_BITRATE_KBPS); + let min_bitrate = OFFICIAL_MIN_BITRATE_KBPS; + let initial_bitrate = OFFICIAL_MIN_BITRATE_KBPS.max(max_bitrate / 4); + let is_high_fps = params.fps > 60; + let is_at_least_120_fps = params.fps >= 120; + let is_90_fps = params.fps == 90; let is_120_fps = params.fps == 120; - let is_240_fps = params.fps >= 240; + let is_240_fps = params.fps == 240; let is_av1 = params.codec == VideoCodec::AV1; let bit_depth = params.color_quality.bit_depth(); let hid_device_mask = params @@ -672,6 +679,9 @@ pub fn build_nvst_sdp(params: &NvstParams) -> String { let enable_partially_reliable_transfer_hid = params .enable_partially_reliable_transfer_hid .unwrap_or(hid_device_mask); + let min_target_frame_time_us = (1_000_000u32.saturating_mul(95) + / params.fps.max(1).saturating_mul(100)) + .max(1000); let mut lines = vec![ "v=0".to_owned(), @@ -691,18 +701,48 @@ pub fn build_nvst_sdp(params: &NvstParams) -> String { "a=vqos.fec.repairMinPercent:5".to_owned(), "a=vqos.fec.repairPercent:5".to_owned(), "a=vqos.fec.repairMaxPercent:35".to_owned(), + "a=vqos.bllFec.enable:0".to_owned(), "a=vqos.dynamicStreamingMode:0".to_owned(), "a=vqos.drc.enable:0".to_owned(), + "a=vqos.calculateAvgVideoStreamingBitrate:1".to_owned(), "a=video.dx9EnableNv12:1".to_owned(), "a=video.dx9EnableHdr:1".to_owned(), "a=vqos.qpg.enable:1".to_owned(), "a=vqos.resControl.qp.qpg.featureSetting:7".to_owned(), + "a=video.adaptiveQuantization.spatialAQSetting:7".to_owned(), + "a=video.adaptiveQuantization.temporalAQSetting:0".to_owned(), + "a=video.adaptiveQuantization.spatialAQStrength:12".to_owned(), + "a=video.adaptiveQuantization.qpThresholdAdjPercent:2".to_owned(), + "a=video.adaptiveQuantization.saqAdaptMinQpThresholdPercent:40".to_owned(), + "a=video.adaptiveQuantization.saqAdaptMaxQpThresholdPercent:100".to_owned(), + "a=video.adaptiveQuantization.saqAdaptDecayStrengthX100:250".to_owned(), + "a=video.adaptiveQuantization.perfAdjEnablement:1".to_owned(), + "a=video.framePacing.mode:2".to_owned(), + format!("a=video.framePacing.pid.minTargetFrameTimeUs:{min_target_frame_time_us}"), "a=bwe.useOwdCongestionControl:1".to_owned(), "a=video.enableRtpNack:1".to_owned(), "a=vqos.bw.txRxLag.minFeedbackTxDeltaMs:200".to_owned(), "a=vqos.drc.bitrateIirFilterFactor:18".to_owned(), "a=video.packetSize:1140".to_owned(), + "a=packetPacing.version:3".to_owned(), + "a=packetPacing.mode:1".to_owned(), "a=packetPacing.minNumPacketsPerGroup:15".to_owned(), + "a=packetPacing.enableAccurateSleep:1".to_owned(), + "a=packetPacing.enableSmoothTransition:1".to_owned(), + "a=packetPacing.allowFpsBasedToggle:1".to_owned(), + "a=vqos.relaxMaxBitrate.overrideAvgBitrateThresholdPercent:4".to_owned(), + "a=vqos.relaxMaxBitrate.customAvgBitrateThresholdPercent:65".to_owned(), + "a=vqos.relaxMaxBitrate.overrideAvgQpThresholdPercent:7".to_owned(), + "a=vqos.relaxMaxBitrate.customAvgQpThresholdPercent:51".to_owned(), + "a=vqos.relaxMaxBitrate.iirFilterFactor:120".to_owned(), + "a=vqos.qpDelta.qpDeltaMaxPercent:10".to_owned(), + "a=vqos.qpDelta.qpDeltaSurfaceAdjustmentStrengthPercent:70".to_owned(), + "a=vqos.qpDelta.qpDeltaVbvUsageFactorPercentH264:100".to_owned(), + "a=vqos.qpDelta.qpDeltaVbvUsageFactorPercentH265:100".to_owned(), + "a=vqos.qpDelta.qpDeltaVbvUsageFactorPercentAv1:100".to_owned(), + "a=vqos.qpDelta.qpDeltaMinPercent:60".to_owned(), + "a=vqos.qpDelta.qpDeltaIirFactor:60".to_owned(), + "a=vqos.qpDelta.qpDeltaThrottlePercent:100".to_owned(), ]; if is_high_fps { @@ -712,11 +752,11 @@ pub fn build_nvst_sdp(params: &NvstParams) -> String { "a=vqos.dfc.targetDownCooldownMs:250".to_owned(), format!( "a=vqos.dfc.dfcAlgoVersion:{}", - if is_120_fps || is_240_fps { 2 } else { 1 } + if is_at_least_120_fps { 2 } else { 1 } ), format!( "a=vqos.dfc.minTargetFps:{}", - if is_120_fps || is_240_fps { 100 } else { 60 } + if is_at_least_120_fps { 100 } else { 60 } ), "a=vqos.resControl.dfc.useClientFpsPerf:0".to_owned(), "a=vqos.dfc.adjustResAndFps:0".to_owned(), @@ -725,17 +765,24 @@ pub fn build_nvst_sdp(params: &NvstParams) -> String { "a=bwe.iirFilterFactor:8".to_owned(), "a=video.encoderFeatureSetting:47".to_owned(), "a=video.encoderPreset:6".to_owned(), - "a=vqos.resControl.cpmRtc.badNwSkipFramesCount:600".to_owned(), - "a=vqos.resControl.cpmRtc.decodeTimeThresholdMs:9".to_owned(), - format!( - "a=video.fbcDynamicFpsGrabTimeoutMs:{}", - if is_120_fps { 6 } else { 18 } - ), - format!( - "a=vqos.resControl.cpmRtc.serverResolutionUpdateCoolDownCount:{}", - if is_120_fps { 6000 } else { 12000 } - ), ]); + let fps_specific_capture_tuning = if is_90_fps { + Some((9, 11)) + } else if is_120_fps { + Some((6, 9)) + } else if is_240_fps { + Some((18, 9)) + } else { + None + }; + if let Some((grab_timeout_ms, decode_threshold_ms)) = fps_specific_capture_tuning { + lines.push(format!( + "a=video.fbcDynamicFpsGrabTimeoutMs:{grab_timeout_ms}" + )); + lines.push(format!( + "a=vqos.resControl.cpmRtc.decodeTimeThresholdMs:{decode_threshold_ms}" + )); + } } else { lines.extend([ "a=vqos.dfc.enable:0".to_owned(), @@ -749,7 +796,15 @@ pub fn build_nvst_sdp(params: &NvstParams) -> String { "a=vqos.maxStreamFpsEstimate:240".to_owned(), ]); if ENABLE_240_FPS_SPLIT_ENCODE { - lines.push("a=video.videoSplitEncodeStripsPerFrame:3".to_owned()); + let strips_per_frame = + if is_av1 && params.width.saturating_mul(params.height) >= 2_764_800 { + 63 + } else { + 3 + }; + lines.push(format!( + "a=video.videoSplitEncodeStripsPerFrame:{strips_per_frame}" + )); lines.push(format!( "a=video.updateSplitEncodeStateDynamically:{}", if ENABLE_DYNAMIC_SPLIT_ENCODE_UPDATES { @@ -823,20 +878,14 @@ pub fn build_nvst_sdp(params: &NvstParams) -> String { format!("a=video.clientViewportHt:{}", params.height), format!("a=video.maxFPS:{}", params.fps), format!("a=video.initialBitrateKbps:{initial_bitrate}"), - format!( - "a=video.initialPeakBitrateKbps:{}", - params.max_bitrate_kbps - ), - format!("a=vqos.bw.maximumBitrateKbps:{}", params.max_bitrate_kbps), + format!("a=video.initialPeakBitrateKbps:{initial_bitrate}"), + format!("a=vqos.bw.maximumBitrateKbps:{max_bitrate}"), format!("a=vqos.bw.minimumBitrateKbps:{min_bitrate}"), - format!("a=vqos.bw.peakBitrateKbps:{}", params.max_bitrate_kbps), - format!( - "a=vqos.bw.serverPeakBitrateKbps:{}", - params.max_bitrate_kbps - ), + format!("a=vqos.bw.peakBitrateKbps:{max_bitrate}"), + format!("a=vqos.bw.serverPeakBitrateKbps:{max_bitrate}"), "a=vqos.bw.enableBandwidthEstimation:1".to_owned(), "a=vqos.bw.disableBitrateLimit:0".to_owned(), - format!("a=vqos.grc.maximumBitrateKbps:{}", params.max_bitrate_kbps), + format!("a=vqos.grc.maximumBitrateKbps:{max_bitrate}"), "a=vqos.grc.enable:0".to_owned(), "a=video.maxNumReferenceFrames:4".to_owned(), "a=video.mapRtpTimestampsToFrames:1".to_owned(), @@ -847,6 +896,12 @@ pub fn build_nvst_sdp(params: &NvstParams) -> String { "a=video.prefilterParams.prefilterModel:0".to_owned(), "m=audio 0 RTP/AVP".to_owned(), "a=msid:audio".to_owned(), + "a=aqos.enableRedundancy:1".to_owned(), + "a=aqos.redundancyLevel:2".to_owned(), + "a=aqos.enableRedundancyForMic:1".to_owned(), + "a=aqos.redundancyLevelForMic:3".to_owned(), + "a=audio.enableDynamicAudioConfig:1".to_owned(), + "a=audio.enableTimestampAudioBuffer:1".to_owned(), "m=mic 0 RTP/AVP".to_owned(), "a=msid:mic".to_owned(), "a=rtpmap:0 PCMU/8000".to_owned(), @@ -863,6 +918,8 @@ pub fn build_nvst_sdp(params: &NvstParams) -> String { format!( "a=ri.enablePartiallyReliableTransferHid:{enable_partially_reliable_transfer_hid}" ), + "a=ri.timestampsEnabled:1".to_owned(), + "a=ri.useMultipleGamepads:1".to_owned(), String::new(), ]); @@ -1119,19 +1176,66 @@ mod tests { #[test] fn builds_nvst_sdp_disables_dynamic_streaming_for_high_fps() { - for fps in [120, 240] { + for fps in [90, 120, 144, 165, 240, 360] { let nvst = build_nvst_sdp(&nvst_params_for_fps(fps)); assert!(nvst.contains("a=vqos.dynamicStreamingMode:0")); assert!(nvst.contains("a=vqos.dfc.adjustResAndFps:0")); assert!(nvst.contains("a=vqos.dfc.enable:1")); assert!(nvst.contains("a=vqos.resControl.dfc.useClientFpsPerf:0")); - assert!(nvst.contains("a=vqos.dfc.dfcAlgoVersion:2")); - assert!(nvst.contains("a=vqos.dfc.minTargetFps:100")); + if fps >= 120 { + assert!(nvst.contains("a=vqos.dfc.dfcAlgoVersion:2")); + assert!(nvst.contains("a=vqos.dfc.minTargetFps:100")); + } else { + assert!(nvst.contains("a=vqos.dfc.dfcAlgoVersion:1")); + assert!(nvst.contains("a=vqos.dfc.minTargetFps:60")); + } assert!(!nvst.contains("a=vqos.dfc.enable:0")); } } + #[test] + fn applies_only_official_fps_specific_capture_tuning() { + let cases = [ + (90, Some((9, 11))), + (120, Some((6, 9))), + (144, None), + (165, None), + (240, Some((18, 9))), + (360, None), + ]; + + for (fps, expected) in cases { + let nvst = build_nvst_sdp(&nvst_params_for_fps(fps)); + match expected { + Some((grab_timeout_ms, decode_threshold_ms)) => { + assert!(nvst.contains(&format!( + "a=video.fbcDynamicFpsGrabTimeoutMs:{grab_timeout_ms}" + ))); + assert!(nvst.contains(&format!( + "a=vqos.resControl.cpmRtc.decodeTimeThresholdMs:{decode_threshold_ms}" + ))); + } + None => { + assert!(!nvst.contains("a=video.fbcDynamicFpsGrabTimeoutMs:")); + assert!(!nvst.contains("a=vqos.resControl.cpmRtc.decodeTimeThresholdMs:")); + } + } + } + } + + #[test] + fn reserves_240_fps_capture_profile_for_exact_240_fps() { + let nvst_240 = build_nvst_sdp(&nvst_params_for_fps(240)); + let nvst_360 = build_nvst_sdp(&nvst_params_for_fps(360)); + + assert!(nvst_240.contains("a=vqos.maxStreamFpsEstimate:240")); + assert!(nvst_240.contains("a=video.enableNextCaptureMode:1")); + assert!(!nvst_360.contains("a=vqos.maxStreamFpsEstimate:240")); + assert!(!nvst_360.contains("a=video.enableNextCaptureMode:1")); + assert!(!nvst_360.contains("a=video.videoSplitEncodeStripsPerFrame:")); + } + #[test] fn extracts_negotiated_video_codec_from_answer_payload_order() { let answer_sdp = [ @@ -1254,7 +1358,17 @@ mod tests { assert!(nvst.contains("a=video.maxFPS:120")); assert!(nvst.contains("a=video.bitDepth:10")); assert!(nvst.contains("a=packetPacing.numGroups:3")); - assert!(nvst.contains("a=vqos.adjustStreamingFpsDuringOutOfFocus:0")); + assert!(nvst.contains("a=packetPacing.enableAccurateSleep:1")); + assert!(nvst.contains("a=packetPacing.minNumPacketsPerGroup:15")); + assert!(nvst.contains("a=video.framePacing.mode:2")); + assert!(nvst.contains("a=vqos.fec.repairPercent:5")); + assert!(nvst.contains("a=vqos.fec.repairMaxPercent:35")); + assert!(nvst.contains("a=vqos.bllFec.enable:0")); + assert!(nvst.contains("a=video.rtpNackQueueLength:1024")); + assert!(nvst.contains("a=video.rtpNackQueueMaxPackets:512")); + assert!(nvst.contains("a=video.rtpNackMaxPacketCount:25")); + assert!(nvst.contains("a=aqos.enableRedundancy:1")); + assert!(nvst.contains("a=vqos.adjustStreamingFpsDuringOutOfFocus:1")); assert!(!nvst.contains("a=video.updateSplitEncodeStateDynamically:1")); assert!(nvst.contains("a=ri.partialReliableThresholdMs:16")); assert!(nvst.ends_with('\n')); @@ -1283,7 +1397,20 @@ mod tests { assert!(nvst.contains("a=vqos.maxStreamFpsEstimate:240")); assert!(nvst.contains("a=video.videoSplitEncodeStripsPerFrame:3")); assert!(nvst.contains("a=video.updateSplitEncodeStateDynamically:1")); + assert!(nvst.contains("a=video.framePacing.pid.minTargetFrameTimeUs:3958")); assert!(nvst.contains("a=vqos.rtcPreemptiveIdrSettings.minBurstNackSize:65535")); assert!(nvst.contains("a=vqos.rtcPreemptiveIdrSettings.minNackPacketCaptureAgeMs:65535")); } + + #[test] + fn uses_wide_split_encode_only_for_high_resolution_av1() { + let mut params = nvst_params_for_fps(240); + params.codec = VideoCodec::AV1; + params.width = 2560; + params.height = 1440; + + let nvst = build_nvst_sdp(¶ms); + + assert!(nvst.contains("a=video.videoSplitEncodeStripsPerFrame:63")); + } } diff --git a/native/opennow-streamer/src/windows_dpi.rs b/native/opennow-streamer/src/windows_dpi.rs new file mode 100644 index 000000000..fdf2fcf16 --- /dev/null +++ b/native/opennow-streamer/src/windows_dpi.rs @@ -0,0 +1,29 @@ +//! Windows DPI policy for native renderer windows. +//! +//! Electron publishes native render-surface bounds in physical pixels. The +//! streamer must use the same coordinate space or Windows DPI virtualization +//! can offset or shrink the video window on scaled and mixed-DPI displays. + +use std::ffi::c_void; + +type DpiAwarenessContext = *mut c_void; + +// DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2 from winuser.h. +const PER_MONITOR_AWARE_V2: DpiAwarenessContext = -4_isize as DpiAwarenessContext; + +#[link(name = "user32")] +extern "system" { + fn SetProcessDpiAwarenessContext(value: DpiAwarenessContext) -> i32; + fn SetProcessDPIAware() -> i32; +} + +/// Opt the streamer into physical-pixel coordinates before GStreamer or any +/// renderer thread can create a window. The legacy fallback keeps coordinates +/// unvirtualized on Windows versions that reject the per-monitor-v2 context. +pub(crate) fn enable_per_monitor_awareness() { + unsafe { + if SetProcessDpiAwarenessContext(PER_MONITOR_AWARE_V2) == 0 { + let _ = SetProcessDPIAware(); + } + } +} diff --git a/native/opennow-streamer/vendor/gstreamer-vulkan-windows/1.28.3/OPENNOW-PATCH.txt b/native/opennow-streamer/vendor/gstreamer-vulkan-windows/1.28.3/OPENNOW-PATCH.txt new file mode 100644 index 000000000..48d2a680d --- /dev/null +++ b/native/opennow-streamer/vendor/gstreamer-vulkan-windows/1.28.3/OPENNOW-PATCH.txt @@ -0,0 +1 @@ +Patched gstvkwindow_win32: VkSurface on internal GSTVULKAN hwnd (not Electron parent). diff --git a/native/opennow-streamer/vendor/gstreamer-vulkan-windows/1.28.3/bin/gstvulkan-1.0-0.dll b/native/opennow-streamer/vendor/gstreamer-vulkan-windows/1.28.3/bin/gstvulkan-1.0-0.dll new file mode 100644 index 000000000..542872401 Binary files /dev/null and b/native/opennow-streamer/vendor/gstreamer-vulkan-windows/1.28.3/bin/gstvulkan-1.0-0.dll differ diff --git a/native/opennow-streamer/vendor/gstreamer-vulkan-windows/1.28.3/lib/gstreamer-1.0/gstvulkan.dll b/native/opennow-streamer/vendor/gstreamer-vulkan-windows/1.28.3/lib/gstreamer-1.0/gstvulkan.dll new file mode 100644 index 000000000..097b856a1 Binary files /dev/null and b/native/opennow-streamer/vendor/gstreamer-vulkan-windows/1.28.3/lib/gstreamer-1.0/gstvulkan.dll differ diff --git a/native/opennow-streamer/vendor/gstreamer-vulkan-windows/README.md b/native/opennow-streamer/vendor/gstreamer-vulkan-windows/README.md new file mode 100644 index 000000000..a7dc26e69 --- /dev/null +++ b/native/opennow-streamer/vendor/gstreamer-vulkan-windows/README.md @@ -0,0 +1,18 @@ +# Windows GStreamer Vulkan plugins + +Official GStreamer Windows packages disable the Vulkan plugin in Cerbero +(`disable_plugin('vulkan', ...)` for Linux/Windows binary builds). + +OpenNOW vendors a matching `gstvulkan` build so the experimental Windows +Vulkan video backend can load `vulkansink` / `vulkanh264dec` / `vulkanh265dec`. + +Artifacts under `1.28.3/` target GStreamer 1.28.3 MSVC x86_64. + +## OpenNOW Win32 embed patch + +`gstvkwindow_win32.c` is patched so `vkCreateWin32SurfaceKHR` targets the +`GSTVULKAN` child hwnd (not the Electron/Chromium parent overlay hwnd). +Stock GStreamer presents onto the parent when `set_window_handle` is used, +which stays black under DirectComposition hole-punch. With this patch, +VideoOverlay parenting works: GSTVULKAN is a visible child of the Internal +surface and the swapchain presents there (no floating top-level window). diff --git a/opennow-stable/.claude/skills/verify/SKILL.md b/opennow-stable/.claude/skills/verify/SKILL.md new file mode 100644 index 000000000..2e58bd530 --- /dev/null +++ b/opennow-stable/.claude/skills/verify/SKILL.md @@ -0,0 +1,50 @@ +--- +description: Build, launch, drive, and screenshot the OpenNOW Electron settings UI on Windows. +--- + +# Verify OpenNOW desktop UI + +## Build and launch + +1. Build the current tree from the repository root: + ```powershell + npm --prefix opennow-stable run build + ``` +2. Launch Electron with Chrome DevTools Protocol enabled. Run this as a background command so Electron remains available: + ```powershell + $env:OPENNOW_REMOTE_DEBUG = '1'; & "opennow-stable\node_modules\electron\dist\electron.exe" "opennow-stable" + ``` +3. Wait for `http://127.0.0.1:9222/json/list` to expose a page titled `OpenNOW`. The renderer URL should end in `opennow-stable/dist/index.html`. + +## Drive the renderer + +Use CDP against the target's `webSocketDebuggerUrl`. Node in this environment provides the global `WebSocket` API. The working driver supports: + +- `Runtime.evaluate` with `awaitPromise: true`, `returnByValue: true`, and `userGesture: true` for DOM inspection and clicks. +- `Input.dispatchKeyEvent` (`keyDown`, then `keyUp`) for real keyboard behavior. This is required for native range-input arrow-key changes; synthetic DOM keyboard events do not invoke browser default actions. +- `Input.insertText` for typing into the focused control. +- `Page.captureScreenshot` with `{ format: "png", fromSurface: true, captureBeyondViewport: false }`. + +The target can be found with: +```js +const targets = await fetch("http://127.0.0.1:9222/json/list").then((response) => response.json()); +const target = targets.find((item) => item.type === "page" && item.title === "OpenNOW") ?? targets[0]; +``` + +Send CDP requests as incrementing `{ id, method, params }` JSON messages and resolve responses by matching `id`. + +## Settings flows worth exercising + +- Click the navbar `Settings` button; the modal root is `.settings-modal`. +- Section navigation buttons are `.settings-nav-item`. +- Shared dropdowns use `[role="combobox"]`, `aria-controls`, and `aria-activedescendant`. +- Region controls use `#settings-stream-region-trigger`, `.region-dropdown`, and `.region-dropdown-search-input`. +- Numeric mouse controls are `#settings-input-mouse-sensitivity-number` and `#settings-input-mouse-acceleration-number`. +- Resize through `window.resizeTo(1024, 680)` and `window.resizeTo(1400, 900)`; verify `outerWidth`/`outerHeight` afterward. +- Switch Interface theme through `#appTheme`, capture light and dark screenshots, then restore the original theme. + +For popup Escape behavior, use real CDP key events: the first Escape closes the focused popup while Settings remains open; after the close transition, the next Escape closes Settings. Restore any changed theme, region, toggle, slider, or numeric setting before finishing. + +## Evidence + +Capture screenshots to a stable local path and read them visually. Useful frames include an open grouped resolution dropdown, filtered region results, focused toggle/slider/chip/region trigger, Account action sizing, About action sizing, and Native Streamer diagnostics at both 1400×900 and 1024×680. diff --git a/opennow-stable/bun.lock b/opennow-stable/bun.lock index 5d371953b..cbbebe890 100644 --- a/opennow-stable/bun.lock +++ b/opennow-stable/bun.lock @@ -5,38 +5,70 @@ "": { "name": "opennow-stable", "dependencies": { + "@formkit/auto-animate": "^0.10.0", + "@react-three/fiber": "^9.6.1", "discord-rpc": "^4.0.1", - "electron-updater": "^6.8.3", - "lucide-react": "^1.17.0", - "motion": "^12.42.0", + "electron-updater": "^6.8.9", + "lucide-react": "^1.25.0", + "motion": "^12.42.2", + "posthog-js": "^1.406.2", + "posthog-node": "^5.46.0", "qrcode": "^1.5.4", - "react": "^19.2.6", - "react-dom": "^19.2.6", - "ws": "^8.21.0", + "react": "^19.2.7", + "react-dom": "^19.2.7", + "three": "^0.185.1", + "ws": "^8.21.1", }, "devDependencies": { - "@types/discord-rpc": "^4.0.10", - "@types/node": "^22.19.17", + "@types/discord-rpc": "^4.0.11", + "@types/node": "^22.20.1", "@types/qrcode": "^1.5.6", - "@types/react": "^19.2.14", + "@types/react": "^19.2.17", "@types/react-dom": "^19.2.3", + "@types/three": "^0.185.1", "@types/ws": "^8.18.1", "@vitejs/plugin-react": "^5.2.0", "cross-env": "^10.1.0", - "electron": "^42.3.3", - "electron-builder": "^26.8.1", + "electron": "^43.1.1", + "electron-builder": "^26.15.3", "electron-vite": "^5.0.0", - "oxlint": "^1.67.0", - "react-scan": "^0.5.3", - "tsx": "^4.22.3", + "oxlint": "^1.74.0", + "react-scan": "^0.5.7", + "tsx": "^4.23.1", "typescript": "^6.0.3", - "vite": "^7.3.5", + "vite": "^7.3.6", }, }, }, + "overrides": { + "@xmldom/xmldom": "0.8.13", + "ajv@6": "6.15.0", + "brace-expansion@1": "1.1.13", + "brace-expansion@2": "2.0.3", + "brace-expansion@5": "5.0.7", + "ip-address": "10.2.0", + "js-yaml": "4.3.0", + "lodash": "4.18.1", + "minimatch@10": "10.2.5", + "minimatch@3": "3.1.5", + "minimatch@5": "5.1.9", + "minimatch@9": "9.0.9", + "postcss": "8.5.20", + "rollup": "4.62.2", + "tar": "7.5.20", + "tmp": "0.2.7", + "undici": "7.28.0", + "ws": "^8.21.1", + }, "packages": { "7zip-bin": ["7zip-bin@5.2.0", "", {}, "sha512-ukTPVhqG4jNzMro2qA9HSCSSVJN3aN7tlb+hfqYCt3ER0yWroeA2VR38MNrOHLQ/cVj+DaIMad0kFCtWWowh/A=="], + "@apm-js-collab/code-transformer": ["@apm-js-collab/code-transformer@0.18.0", "", { "dependencies": { "@types/estree": "^1.0.8", "astring": "^1.9.0", "esquery": "^1.7.0", "meriyah": "^6.1.4", "semifies": "^1.0.0", "source-map": "^0.6.0" }, "bin": { "code-transformer": "cli.js" } }, "sha512-aN3Oq8r1J3gPJtCwErP664gM0+HhM1I1lujPr9TMTCcEl/joQQbpGpeMdts9B1+W2wHMsvioDMv5F4PvMWE6gw=="], + + "@apm-js-collab/code-transformer-bundler-plugins": ["@apm-js-collab/code-transformer-bundler-plugins@0.6.2", "", { "dependencies": { "@apm-js-collab/code-transformer": "^0.18.0", "es-module-lexer": "^2.1.0", "magic-string": "^0.30.21", "module-details-from-path": "^1.0.4" } }, "sha512-5vBrtIEL+UVbO0YWWoyYG4QMgR+ZfnIL3xlteIkAmU7YaAPhc28k3md/NM14tnkfXKjKOn9yUzEA9AYUmNpvJg=="], + + "@apm-js-collab/tracing-hooks": ["@apm-js-collab/tracing-hooks@0.13.0", "", { "dependencies": { "@apm-js-collab/code-transformer": "^0.18.0", "debug": "^4.4.1", "module-details-from-path": "^1.0.4" } }, "sha512-mTvWz9rnQwx1U3h0XPTHaX7bgfkpipLLTQyjlC2cdhQpQEuoLT0AGzoydeoq2NxfEVv6fWOOETcSbb2nptleyw=="], + "@babel/code-frame": ["@babel/code-frame@7.29.0", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw=="], "@babel/compat-data": ["@babel/compat-data@7.29.0", "", {}, "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg=="], @@ -71,6 +103,8 @@ "@babel/plugin-transform-react-jsx-source": ["@babel/plugin-transform-react-jsx-source@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw=="], + "@babel/runtime": ["@babel/runtime@7.29.7", "", {}, "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw=="], + "@babel/template": ["@babel/template@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ=="], "@babel/traverse": ["@babel/traverse@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/types": "^7.29.0", "debug": "^4.3.1" } }, "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA=="], @@ -79,6 +113,10 @@ "@develar/schema-utils": ["@develar/schema-utils@2.6.5", "", { "dependencies": { "ajv": "^6.12.0", "ajv-keywords": "^3.4.1" } }, "sha512-0cp4PsWQ/9avqTVMCtZ+GirikIA36ikvjtHweU4/j8yLtgObI0+JUPhYFScgwlteveGB1rt3Cm8UhN04XayDig=="], + "@dimforge/rapier3d-compat": ["@dimforge/rapier3d-compat@0.12.0", "", {}, "sha512-uekIGetywIgopfD97oDL5PfeezkFpNhwlzlaEYNOA0N6ghdsOvh/HYjSMek5Q2O1PYvRSDFcqFVJl4r4ZBwOow=="], + + "@electron-internal/extract-zip": ["@electron-internal/extract-zip@1.0.4", "", {}, "sha512-Zr1Vs7E9tpCNhZHDAbFVXc2gEVCG9RqPDjrno5+bdgB6LRAuvgyMHJut4NCVyYwtAieapMzc3fiQ3CSTi75ARg=="], + "@electron/asar": ["@electron/asar@3.4.1", "", { "dependencies": { "commander": "^5.0.0", "glob": "^7.1.6", "minimatch": "^3.0.4" }, "bin": { "asar": "bin/asar.js" } }, "sha512-i4/rNPRS84t0vSRa2HorerGRXWyF4vThfHesw0dmcWHp+cspK743UanA0suA5Q5y8kzY2y6YKrvbIUn69BCAiA=="], "@electron/fuses": ["@electron/fuses@1.8.0", "", { "dependencies": { "chalk": "^4.1.1", "fs-extra": "^9.0.1", "minimist": "^1.2.5" }, "bin": { "electron-fuses": "dist/bin.js" } }, "sha512-zx0EIq78WlY/lBb1uXlziZmDZI4ubcCXIMJ4uGjXzZW0nS19TjSPeXPAjzzTmKQlJUZm0SbmZhPKP7tuQ1SsEw=="], @@ -89,10 +127,16 @@ "@electron/osx-sign": ["@electron/osx-sign@1.3.3", "", { "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" } }, "sha512-KZ8mhXvWv2rIEgMbWZ4y33bDHyUKMXnx4M0sTyPNK/vcB81ImdeY9Ggdqy0SWbMDgmbqyQ+phgejh6V3R2QuSg=="], - "@electron/rebuild": ["@electron/rebuild@4.0.3", "", { "dependencies": { "@malept/cross-spawn-promise": "^2.0.0", "debug": "^4.1.1", "detect-libc": "^2.0.1", "got": "^11.7.0", "graceful-fs": "^4.2.11", "node-abi": "^4.2.0", "node-api-version": "^0.2.1", "node-gyp": "^11.2.0", "ora": "^5.1.0", "read-binary-file-arch": "^1.0.6", "semver": "^7.3.5", "tar": "^7.5.6", "yargs": "^17.0.1" }, "bin": { "electron-rebuild": "lib/cli.js" } }, "sha512-u9vpTHRMkOYCs/1FLiSVAFZ7FbjsXK+bQuzviJZa+lG7BHZl1nz52/IcGvwa3sk80/fc3llutBkbCq10Vh8WQA=="], + "@electron/rebuild": ["@electron/rebuild@4.2.0", "", { "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" } }, "sha512-RKL/O+jGoXJMxrx/5771y1n0xTKmFuOYGO3gMmwypBM6rsH0kou0mswwdXA2JrhIkE4xyC7v9vGk0n6NPzgOxQ=="], "@electron/universal": ["@electron/universal@2.0.3", "", { "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" } }, "sha512-Wn9sPYIVFRFl5HmwMJkARCCf7rqK/EurkfQ/rJZ14mHP3iYTjZSIOSVonEAnhWeAXwtw7zOekGRlc6yTtZ0t+g=="], + "@emnapi/core": ["@emnapi/core@1.11.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.2", "tslib": "^2.4.0" } }, "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ=="], + + "@emnapi/runtime": ["@emnapi/runtime@1.11.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw=="], + + "@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA=="], + "@epic-web/invariant": ["@epic-web/invariant@1.0.0", "", {}, "sha512-lrTPqgvfFQtR/eY/qkIzp98OGdNJu0m5ji3q/nJI8v3SXkRKEnWiOxMmbvcSoAIzv/cGiuvRy57k4suKQSAdwA=="], "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="], @@ -147,8 +191,36 @@ "@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="], + "@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.9.1", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ=="], + + "@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.2", "", {}, "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew=="], + + "@eslint/config-array": ["@eslint/config-array@0.23.5", "", { "dependencies": { "@eslint/object-schema": "^3.0.5", "debug": "^4.3.1", "minimatch": "^10.2.4" } }, "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA=="], + + "@eslint/config-helpers": ["@eslint/config-helpers@0.6.0", "", { "dependencies": { "@eslint/core": "^1.2.1" } }, "sha512-ii6Bw9jJ2zi2cWA2Z+9/QZ/+3DX6kwaV5Q986D/CdP3Lap3w/pgQZ373FV7byY/i7L4IRH/G43I5dz1ClsCbpA=="], + + "@eslint/core": ["@eslint/core@1.2.1", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ=="], + + "@eslint/object-schema": ["@eslint/object-schema@3.0.5", "", {}, "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw=="], + + "@eslint/plugin-kit": ["@eslint/plugin-kit@0.7.2", "", { "dependencies": { "@eslint/core": "^1.2.1", "levn": "^0.4.1" } }, "sha512-+CNAzxglkrpNf/kKywqQfk74QjtceuOE7Qm+AF8miRvPF/wmmK5+OJOgVh3AVTT3RP2mH3+FOaxlE5v72owk0A=="], + + "@formkit/auto-animate": ["@formkit/auto-animate@0.10.0", "", {}, "sha512-KGomRttjUfORuPUaR/ZGQw+6xfMrTM+sxnILv7JAd9AmabU9rg9i6gF/iC0Ih+QpKCubJpCA/1DX9UHKE8cX+A=="], + "@gar/promisify": ["@gar/promisify@1.1.3", "", {}, "sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw=="], + "@humanfs/core": ["@humanfs/core@0.19.2", "", { "dependencies": { "@humanfs/types": "^0.15.0" } }, "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA=="], + + "@humanfs/node": ["@humanfs/node@0.16.8", "", { "dependencies": { "@humanfs/core": "^0.19.2", "@humanfs/types": "^0.15.0", "@humanwhocodes/retry": "^0.4.0" } }, "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ=="], + + "@humanfs/types": ["@humanfs/types@0.15.0", "", {}, "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q=="], + + "@humanwhocodes/module-importer": ["@humanwhocodes/module-importer@1.0.1", "", {}, "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA=="], + + "@humanwhocodes/retry": ["@humanwhocodes/retry@0.4.3", "", {}, "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ=="], + + "@iarna/toml": ["@iarna/toml@2.2.5", "", {}, "sha512-trnsAYxU3xnS1gPHPyU961coFyLkh4gAD/0zQ5mymY4yOZ+CYvsPqUbOFSw0aDM4y0tV7tiFxL/1XfXPNC6IPg=="], + "@isaacs/cliui": ["@isaacs/cliui@9.0.0", "", {}, "sha512-AokJm4tuBHillT+FpMtxQ60n8ObyXBatq7jD2/JA9dxbDDokKQm8KMht5ibGzLVU9IJDIKK4TPKgMHEYMn3lMg=="], "@isaacs/fs-minipass": ["@isaacs/fs-minipass@4.0.1", "", { "dependencies": { "minipass": "^7.0.4" } }, "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w=="], @@ -167,109 +239,243 @@ "@malept/flatpak-bundler": ["@malept/flatpak-bundler@0.4.0", "", { "dependencies": { "debug": "^4.1.1", "fs-extra": "^9.0.0", "lodash": "^4.17.15", "tmp-promise": "^3.0.2" } }, "sha512-9QOtNffcOF/c1seMCDnjckb3R9WHcG34tky+FHpNKKCW0wc/scYLwMtO+ptyGUfMW0/b/n4qRiALlaFHc9Oj7Q=="], - "@npmcli/agent": ["@npmcli/agent@3.0.0", "", { "dependencies": { "agent-base": "^7.1.0", "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.1", "lru-cache": "^10.0.1", "socks-proxy-agent": "^8.0.3" } }, "sha512-S79NdEgDQd/NGCay6TCoVzXSj74skRZIKJcpJjC5lOq34SZzyI6MqtiiWoiVWoVrTcGjNeC4ipbh1VIHlpfF5Q=="], + "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.6", "", { "dependencies": { "@tybys/wasm-util": "^0.10.3" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" } }, "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg=="], + + "@noble/hashes": ["@noble/hashes@2.2.0", "", {}, "sha512-IYqDGiTXab6FniAgnSdZwgWbomxpy9FtYvLKs7wCUs2a8RkITG+DFGO1DM9cr+E3/RgADRpFjrKVaJ1z6sjtEg=="], + + "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="], + + "@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="], - "@npmcli/fs": ["@npmcli/fs@4.0.0", "", { "dependencies": { "semver": "^7.3.5" } }, "sha512-/xGlezI6xfGO9NwuJlnwz/K14qD1kCSAGtacBHnGzeAIuJGazcp45KP5NuyARXoKb7cwulAGWVsbeSxdG/cb0Q=="], + "@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="], + + "@npmcli/fs": ["@npmcli/fs@2.1.2", "", { "dependencies": { "@gar/promisify": "^1.1.3", "semver": "^7.3.5" } }, "sha512-yOJKRvohFOaLqipNtwYB9WugyZKhC/DZC4VYPmpaCzDBrA8YpK3qHZ8/HGscMnE4GqbkLNuVcCnxkeQEdGt6LQ=="], "@npmcli/move-file": ["@npmcli/move-file@2.0.1", "", { "dependencies": { "mkdirp": "^1.0.4", "rimraf": "^3.0.2" } }, "sha512-mJd2Z5TjYWq/ttPLLGqArdtnC74J6bOzg4rMDnN+p1xTacZ2yPRCk2y0oSWQtygLR9YVQXgOcONrwtnk3JupxQ=="], - "@oxlint/binding-android-arm-eabi": ["@oxlint/binding-android-arm-eabi@1.67.0", "", { "os": "android", "cpu": "arm" }, "sha512-VrSi571rDv1N8HaEDM+DEX8nmT0y9jJo8tzzW13vsOWTx59xQczCIJx68n2zWOXRT5YKZsOZXp4qkHN/10x4mw=="], + "@opentelemetry/api": ["@opentelemetry/api@1.9.1", "", {}, "sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q=="], + + "@opentelemetry/api-logs": ["@opentelemetry/api-logs@0.220.0", "", { "dependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-CmVa4ImJ+ynfrPMNaAXHET6Bhb44SwzmfyVJFq9ni2jgXJR/l7C6gfVFddNmHP+ZOkP9cf4f9DBe68qVLTHc9w=="], + + "@opentelemetry/core": ["@opentelemetry/core@2.9.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-m2nckMT80NnmjTYSPjJQObBJ+8dgkoajEOUbznL8AHZ3T3yHRk2P7gI1PhEBc1+lOnrYE9UWrWHqJDsmqjmNbw=="], + + "@opentelemetry/instrumentation": ["@opentelemetry/instrumentation@0.220.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.220.0", "import-in-the-middle": "^3.0.0", "require-in-the-middle": "^8.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-xQx3E2WxP1mDvKzxLxX+CTCtNLa560YJZ3087qYHerl2YmiKpv7AH+dAy7vmx+eVrZ5BwhfWUAVoKOoxCNHcpw=="], + + "@opentelemetry/resources": ["@opentelemetry/resources@2.9.0", "", { "dependencies": { "@opentelemetry/core": "2.9.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-jyA5MBLQ+Dkl3+JsZkUoUvL7yHvU64kLsvpXKarWm6347Sl1t1bXFTFykUePNpT5WH5pm9a2Qtt03iIYQhZ1Fg=="], + + "@opentelemetry/sdk-trace": ["@opentelemetry/sdk-trace@2.9.0", "", { "dependencies": { "@opentelemetry/core": "2.9.0", "@opentelemetry/resources": "2.9.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-sGA19HvtrrSKYsseHphluH6j3p6Xa3fqc7c7y8f/7mYWejc1lyDFcpSdD1kYa50HCLUeEo4zA5bW0pniaPszuw=="], + + "@opentelemetry/sdk-trace-base": ["@opentelemetry/sdk-trace-base@2.9.0", "", { "dependencies": { "@opentelemetry/core": "2.9.0", "@opentelemetry/resources": "2.9.0", "@opentelemetry/sdk-trace": "2.9.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-cp9zmTl62R8PJrpvFcmc8N2JQU/xfa0S+61q511Nji+QxCfZ8Ifvg7H27G8cANe4crg4RTrWsVvanHiXjSp6ag=="], + + "@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.43.0", "", {}, "sha512-eSYWTm620tTk45EKSedaUL8MFYI8hW164hIXsgIHyxu3VobUB3fFCu5t0hQby6OoWRPsG1KkKUG2M5UadiLiVg=="], + + "@oxc-parser/binding-android-arm-eabi": ["@oxc-parser/binding-android-arm-eabi@0.138.0", "", { "os": "android", "cpu": "arm" }, "sha512-hSYAD+F9W2Qh8SETMqBsQRx6YHvB4z+i/i36shlC7tfdZQauMs4vf3G/EQwKOkNlN7rkTiKINvsNmQb9q2MWcQ=="], + + "@oxc-parser/binding-android-arm64": ["@oxc-parser/binding-android-arm64@0.138.0", "", { "os": "android", "cpu": "arm64" }, "sha512-Ns5LLTp8cVyP8DsYqD482h0HE84xiGYRgtm7g4LtTinq209NAiMF768e/8r2NHaa0UMirS5mrT1m1VwiVmBi4Q=="], + + "@oxc-parser/binding-darwin-arm64": ["@oxc-parser/binding-darwin-arm64@0.138.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Yka0m4YhKUHBIZufafSLAeO+DUrfHPtNXBlZSj7DxshquIl41x/a+i/MbRnbOy8heuLiYU1STa6h0FAAzT7Pbw=="], + + "@oxc-parser/binding-darwin-x64": ["@oxc-parser/binding-darwin-x64@0.138.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-MWLUZZzmNRUqTWueZF27ncreaZ1wZ0gboWL2QMPxRQA2xgOmBPlGg2H9pAKJSPBlwEHcWa9TdWRiehAS+yls8w=="], + + "@oxc-parser/binding-freebsd-x64": ["@oxc-parser/binding-freebsd-x64@0.138.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-Vae5tzsrzZ/lCDVCZUMi/vzSiiHEgcOEfsyIfWOHmjZ2ji+gT+n96T757yX5/f7/7JIJuiannAHJKV5ARaF6ng=="], + + "@oxc-parser/binding-linux-arm-gnueabihf": ["@oxc-parser/binding-linux-arm-gnueabihf@0.138.0", "", { "os": "linux", "cpu": "arm" }, "sha512-qkU8wv5mYexrCw0X4DHFgxGbRScwGLIIKUkHXU7xXEiLoMnQzELak2gujxfa9GFrlEgPjbyLUDFHWm67Zs38ng=="], + + "@oxc-parser/binding-linux-arm-musleabihf": ["@oxc-parser/binding-linux-arm-musleabihf@0.138.0", "", { "os": "linux", "cpu": "arm" }, "sha512-3HgULIvoDV7h2ZfVYzxQwOSOJnAjMwYmyUBzndNuLRGgBNI549ED0P6AGmN9y2TnSvrwJ+Q8zqdxqssMnGXitA=="], - "@oxlint/binding-android-arm64": ["@oxlint/binding-android-arm64@1.67.0", "", { "os": "android", "cpu": "arm64" }, "sha512-l6+NdYxMoRohix5r5bbigW16LPicceCwGcQ6LKKuE1kUdjgFfQolJjrJsQYPFetIs78Gxj/G/f5TEGoTCwj9nQ=="], + "@oxc-parser/binding-linux-arm64-gnu": ["@oxc-parser/binding-linux-arm64-gnu@0.138.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-pIonbH2p0KLCwz4CNPCi0xGqci4numpMQDCLJwLfsrEky7NUuByKDFhCjzE0E7vR3aj/lBjyMoTskHBo/qSg8g=="], - "@oxlint/binding-darwin-arm64": ["@oxlint/binding-darwin-arm64@1.67.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-jOzXxS1AxFxhImLIRbtGIMrEwaXcgMw3gR57WB1cRk8ai+vpr6726kxXqVvlNsrXtJ/FrmOm8RxlC0m8SW24Qg=="], + "@oxc-parser/binding-linux-arm64-musl": ["@oxc-parser/binding-linux-arm64-musl@0.138.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-cT5L1Xz/5m6Ga1hD3922gLc+fePOauJZJdApPTI/2Vu0EmYo62uHG9V5Dq65hhgU9TW10oDi2840y9cGdd7BIg=="], - "@oxlint/binding-darwin-x64": ["@oxlint/binding-darwin-x64@1.67.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-3DFAVY94OqjIZHXIPz37yGRSWwOFTAqChQ64/M69GYLawzP0KiwdhDNfqdKKYT0bTR/DNxmMnQsj3ns+8+X/Lg=="], + "@oxc-parser/binding-linux-ppc64-gnu": ["@oxc-parser/binding-linux-ppc64-gnu@0.138.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-hKy/vvejKk3LNE/FsRbekWejLa046//TnLWtSo7ur29NIsNbSIvnOVYIirSVC7fsd6NO8UFzwDdcoZfCyBvSBA=="], - "@oxlint/binding-freebsd-x64": ["@oxlint/binding-freebsd-x64@1.67.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-e4dDKZuLu8TR9DEBssWSDahlPgZBwojTTHZUvnjBRJfJJbpxYCjfjKfi0Z1+CSLMiJBwI2yCDtRM1XJQaARjmg=="], + "@oxc-parser/binding-linux-riscv64-gnu": ["@oxc-parser/binding-linux-riscv64-gnu@0.138.0", "", { "os": "linux", "cpu": "none" }, "sha512-bh6tjNGq0v0b9GAMu0pTv/YpTqepCFy0TIOtQHm8+41fZwLXTaB6xiEWVUSarNCXqc5kyzYcH6EOfwW1sJxJOw=="], - "@oxlint/binding-linux-arm-gnueabihf": ["@oxlint/binding-linux-arm-gnueabihf@1.67.0", "", { "os": "linux", "cpu": "arm" }, "sha512-BKytFdcQzbITV3xlnzDUDTEDtbUMCCiC4EaNTDZ4FyT8gdNvBC4gfiLucXp/sQl0XU3p7syTlorUWVVVBZab2g=="], + "@oxc-parser/binding-linux-riscv64-musl": ["@oxc-parser/binding-linux-riscv64-musl@0.138.0", "", { "os": "linux", "cpu": "none" }, "sha512-HhOkddcClSTtTxY10f/mACblKcQdxWy4lYYwX12G23j+S5eiJ5y1kpo1r7kKng+2bdnCBO+lCDWOVVc9kVl9+g=="], - "@oxlint/binding-linux-arm-musleabihf": ["@oxlint/binding-linux-arm-musleabihf@1.67.0", "", { "os": "linux", "cpu": "arm" }, "sha512-XYAv0esBDX7BpTzRDjVX2Vdj+zndd8ll2dFQiaeQ6zTZr7A8GRDTN7fH3FP3jU+O0vCDx85oH/EtG7BzPgAXuw=="], + "@oxc-parser/binding-linux-s390x-gnu": ["@oxc-parser/binding-linux-s390x-gnu@0.138.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-5mi+wtbeJiEa4waGG88EcEGgJBBNJdDeIcayPPcrLNMXbCrgdtbb80q0Nrat7A8NglLUVzhuTAAp7K6PjmUO8Q=="], - "@oxlint/binding-linux-arm64-gnu": ["@oxlint/binding-linux-arm64-gnu@1.67.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-zizRMjA0i6u/2B0evgda04iycu+MoNuf1pBy6Eh+1CjC5wMEG7qN5zdDKTCvFc0KSYSDM9QTG3gjZHirgtQuKg=="], + "@oxc-parser/binding-linux-x64-gnu": ["@oxc-parser/binding-linux-x64-gnu@0.138.0", "", { "os": "linux", "cpu": "x64" }, "sha512-ckbq3AMI7lI8AhQtE8KdqYRmzmzwKfCU12QN/PBKXO72PfWdvvZQN0hFShDX/XRNsPqjddLmvXaQMT3zfYtNlw=="], - "@oxlint/binding-linux-arm64-musl": ["@oxlint/binding-linux-arm64-musl@1.67.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-zB/Tf6sUjmmvvbva9Gj3JTJ8rJ9t4I8/U0o6vSRtd0DRIsIuyegBwJAzhSUFQHdMijIRJkW0exs/yBhpw2S20w=="], + "@oxc-parser/binding-linux-x64-musl": ["@oxc-parser/binding-linux-x64-musl@0.138.0", "", { "os": "linux", "cpu": "x64" }, "sha512-JrCOzHO9BYEs5Xz5JHYBxSc/hYKxfXUj5QQb64sERSbkQot6+KEgMTOR2C9hLrhaqOui65OYcFyTTS+YxXDtnA=="], - "@oxlint/binding-linux-ppc64-gnu": ["@oxlint/binding-linux-ppc64-gnu@1.67.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-kgU40Gt74CK0TCsF51KZymkIwN9U0BajKsMijB52zPqOeZU9NAHkA/NSQkZDHEaCakx42DxhXkODiAqf2b4Gug=="], + "@oxc-parser/binding-openharmony-arm64": ["@oxc-parser/binding-openharmony-arm64@0.138.0", "", { "os": "none", "cpu": "arm64" }, "sha512-eASMMfOOIfLHkWJRPSu8llByvVRM+c1M/lh18KjsjELM3y10+7B5iBbbrht9LdtsJXQ+mRuP/lJ7UWe3Ok3ehw=="], - "@oxlint/binding-linux-riscv64-gnu": ["@oxlint/binding-linux-riscv64-gnu@1.67.0", "", { "os": "linux", "cpu": "none" }, "sha512-tOYhkk/iaG9aD3FvGpBFd1Lrw0x0RaVoJBxjUkfNzS50rC5NS5BteNCwgr8A2zCdADrIIoze6D7u6U5Ic++/iQ=="], + "@oxc-parser/binding-wasm32-wasi": ["@oxc-parser/binding-wasm32-wasi@0.138.0", "", { "dependencies": { "@emnapi/core": "1.11.1", "@emnapi/runtime": "1.11.1", "@napi-rs/wasm-runtime": "^1.1.6" }, "cpu": "none" }, "sha512-BnTCO87Iwc57NufXS7vcrkrmpN+daeCeYr1+/xgPT6HjwNs0lBmJYeFrcOs4WkNN8yscdd6Rc4FxWh3+59hAFw=="], - "@oxlint/binding-linux-riscv64-musl": ["@oxlint/binding-linux-riscv64-musl@1.67.0", "", { "os": "linux", "cpu": "none" }, "sha512-sEtywrPb+0b+tHYl1SDCrw903fiC4eyKoNqzP3v+f2JT3Xcv4NEYG+P8rj+eEnX7IWhqV/xj8/JmcmVj21CXaA=="], + "@oxc-parser/binding-win32-arm64-msvc": ["@oxc-parser/binding-win32-arm64-msvc@0.138.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-+Zi47boD2wKNL0hOA47Vkwk6njMZ8sOsr4Geu/56EUtlooDh9crNOU41U6bXGS0UjC4Y72HtRA1iuB6qx1ARUw=="], - "@oxlint/binding-linux-s390x-gnu": ["@oxlint/binding-linux-s390x-gnu@1.67.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-BvR8Moa0zCLxroOx4vZaZN9nUfwAUpSTwjZdxZyKy4bv3PrzrXrxKR/ZQ0L9wNSvlPhnMJeZfa3q5w6ZCTuN6Q=="], + "@oxc-parser/binding-win32-ia32-msvc": ["@oxc-parser/binding-win32-ia32-msvc@0.138.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-SYcV674Wi2WuoBefUFgf0PBMNlZe5IF0YZ0TnP7DK+EusMVpEWq6iz+7r64svjAb7vjthzlas0FUCSlz8YkqYg=="], - "@oxlint/binding-linux-x64-gnu": ["@oxlint/binding-linux-x64-gnu@1.67.0", "", { "os": "linux", "cpu": "x64" }, "sha512-mm2cxM6fksOpq6l0uFws8BUGKAR4dNa/cZCn37Npq7PFbhD5HDJqWfnoIvTaeRKMy5XdS2tO0MA0qbHDrnXAAA=="], + "@oxc-parser/binding-win32-x64-msvc": ["@oxc-parser/binding-win32-x64-msvc@0.138.0", "", { "os": "win32", "cpu": "x64" }, "sha512-QZplnCxS4vPe4StAVBtvD2bW3pELlidf0Ek6iQ/HHiCjbEtrs5pFZZfLAoPhKLJyDzyxoGAdic9bSIYrJYTZcg=="], - "@oxlint/binding-linux-x64-musl": ["@oxlint/binding-linux-x64-musl@1.67.0", "", { "os": "linux", "cpu": "x64" }, "sha512-WmbMuLapKyDlobMkXAaAL0Y+Uczh4LETfIfQsUpbId4Ip8Ai82/jqeYTOoUCkuuhBFapgqP253+d83tLKOksJg=="], + "@oxc-project/types": ["@oxc-project/types@0.138.0", "", {}, "sha512-1a7ZKmrRTCoN1XMZ4L0PyyqrMnrNlLyPuOkdSX2MZg7IiIGRUyurNhAm73ptDOraoBcIordsIGKNPKUzy3ZmfA=="], - "@oxlint/binding-openharmony-arm64": ["@oxlint/binding-openharmony-arm64@1.67.0", "", { "os": "none", "cpu": "arm64" }, "sha512-9g/PqxYJelzzTAOR5Y+RiRqdeydhEuXv2KxNeFcAKQ7UsvnWSY1OP4MsuPMbTO2Pf70tz7mFhl1j13H3fyh+8g=="], + "@oxc-resolver/binding-android-arm-eabi": ["@oxc-resolver/binding-android-arm-eabi@11.24.2", "", { "os": "android", "cpu": "arm" }, "sha512-y09e0L0SRI2OA2tUIrjBgoV3eH5hvUKXNkJqXmNo5V2WxIjyC7I7aJfRLMEVpA8yi95f90gFDvO0VMgrDw+vwA=="], - "@oxlint/binding-win32-arm64-msvc": ["@oxlint/binding-win32-arm64-msvc@1.67.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-2VhwE6Gatb0vJGnN0TBuQMbKCOiZlSQ/zJvVWYLK4a9d4iDiJOen/yVQkGpmsJ90MuH66fzi0kEKI0jRQMDxGA=="], + "@oxc-resolver/binding-android-arm64": ["@oxc-resolver/binding-android-arm64@11.24.2", "", { "os": "android", "cpu": "arm64" }, "sha512-cl4icWaZFnLdg8m6qtnh5rBMuGbxc/ptStFHLeCNwr+2cZjkjNwQu/jYRS0CHlnPecOJMpuS5M6/BH+0J/YkEg=="], - "@oxlint/binding-win32-ia32-msvc": ["@oxlint/binding-win32-ia32-msvc@1.67.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-EQ3VExXfeM1InbE5+JjufhZZTWy+kHUwgt3yZR7gQ47Je/mE0WspQPan0OJznh493L5anM210YNJtH1PXjTSFg=="], + "@oxc-resolver/binding-darwin-arm64": ["@oxc-resolver/binding-darwin-arm64@11.24.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-At29QEMF6HajbQvgY8K6OXnHD1x9rad74xBEfmCB6ZqCGsdq75aK7tOYcTbOanMy8qdIBrfL3SMr3p/lfSlb9w=="], - "@oxlint/binding-win32-x64-msvc": ["@oxlint/binding-win32-x64-msvc@1.67.0", "", { "os": "win32", "cpu": "x64" }, "sha512-bw24y+/1MHS4QDkons3YyHkPT9uCMoLHHgQhb+mb8NOjTYwub1CZ+K9Ngr8aO5DMrDrkqHwTzlTwFP2vS8Y/ZQ=="], + "@oxc-resolver/binding-darwin-x64": ["@oxc-resolver/binding-darwin-x64@11.24.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-A5Kqr1EUj4oIL5CF4WRssq/o5P0Y11cwoFouMRmQ7YnC/A8V93nv1nb7aSU8HwcgmXropjLNkVTl4MN87cu28Q=="], + + "@oxc-resolver/binding-freebsd-x64": ["@oxc-resolver/binding-freebsd-x64@11.24.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-R5xkRBRRz7ceH/P5Jrc6G7FmdUdgpLYyESFAUDVTNQ9K0sGPxcp4ljiwEwEqsvNcQ4sYbMRrWcHHBCu7ksAJVw=="], + + "@oxc-resolver/binding-linux-arm-gnueabihf": ["@oxc-resolver/binding-linux-arm-gnueabihf@11.24.2", "", { "os": "linux", "cpu": "arm" }, "sha512-k/RuYL4L/R58IBn3wT5ma3Wh4k62bp1eYCFRWCmMsasUOqL+H6sW0VGFadEzKWXFFlz+2uIMoeMk9ySSZJHgbg=="], + + "@oxc-resolver/binding-linux-arm-musleabihf": ["@oxc-resolver/binding-linux-arm-musleabihf@11.24.2", "", { "os": "linux", "cpu": "arm" }, "sha512-bnHAak3ujYfH5pKk4NieFNbvYvernfoQDgwLddbZ3OtMYrem87/qjlA+u+aKG0oZcqSLGCful/6/CEA+aeAgaA=="], + + "@oxc-resolver/binding-linux-arm64-gnu": ["@oxc-resolver/binding-linux-arm64-gnu@11.24.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-vDT3KHgzYp47gmtNOqL2VNhCyl5Zv643eyxm//A68J8DeUGXrvD1pZFiaT4jSfe+RInfnn1R2yVHye4enx6RnA=="], + + "@oxc-resolver/binding-linux-arm64-musl": ["@oxc-resolver/binding-linux-arm64-musl@11.24.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-+kMlQvbzfyEYtu5FcjE4p+ttBLpKW4d/AsAsuE69BxV6V4twZJeIQZFfD8gh/wqglY0MkPSezWXQH0jBV13MUw=="], + + "@oxc-resolver/binding-linux-ppc64-gnu": ["@oxc-resolver/binding-linux-ppc64-gnu@11.24.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-shjfMhmZ3gq9fv/w7bi3PnZlgOPG+2QAOFf0BJF0EgBSIGZ6PMLN2zbGEblTUYB/NKVDRyYhE2ff3dJ1QqNPkA=="], + + "@oxc-resolver/binding-linux-riscv64-gnu": ["@oxc-resolver/binding-linux-riscv64-gnu@11.24.2", "", { "os": "linux", "cpu": "none" }, "sha512-zGelwFR5oRo+b69k8Lrzun86DyUHzfKN6cnjbR9l7Z7NIRznOE/2ZvPa1IUKqAL2PzAXOdwkfVqNvO1H2RlpAw=="], + + "@oxc-resolver/binding-linux-riscv64-musl": ["@oxc-resolver/binding-linux-riscv64-musl@11.24.2", "", { "os": "linux", "cpu": "none" }, "sha512-qxZ1SWCXJY0eyhAlP6Lmo9F2Nrtx7EkYj9oCgL8apDPCwXwCEDA2U697bbT81JIc2IrVjxO4KX6WU2N+oN9Z4w=="], + + "@oxc-resolver/binding-linux-s390x-gnu": ["@oxc-resolver/binding-linux-s390x-gnu@11.24.2", "", { "os": "linux", "cpu": "s390x" }, "sha512-sGCecF3cx2DFlH4t/z7ApnOnXqN48p5p5mlHDEnHTAukQa2P+qMVE4CwyWE9W+q/m3QJ7kKfGrIjax31f44oFQ=="], + + "@oxc-resolver/binding-linux-x64-gnu": ["@oxc-resolver/binding-linux-x64-gnu@11.24.2", "", { "os": "linux", "cpu": "x64" }, "sha512-k/VlMMcSzMlahb3/fENM4rTlsJ0s3fFROA0KXPBmKggqmTSaE383sl8F3KCOXPLmVsYfW6hCitMhXCEtNeZxxg=="], + + "@oxc-resolver/binding-linux-x64-musl": ["@oxc-resolver/binding-linux-x64-musl@11.24.2", "", { "os": "linux", "cpu": "x64" }, "sha512-8hbnZyNi97b/8wapYaIF9+t9GmZKBW2vunaOc3h9HGJptH7b7XpvZqOTBSm/MpTjr7H497BlgOaSfLUdhmy2bw=="], + + "@oxc-resolver/binding-openharmony-arm64": ["@oxc-resolver/binding-openharmony-arm64@11.24.2", "", { "os": "none", "cpu": "arm64" }, "sha512-MvyGik3a6pVgZ0t/kWlbmFxFLmXQJwgLsY2eYFHLpy0wGwRbfzeIGgDwQ3kXqE30z+kSXennRkCrT7TUvkptNg=="], + + "@oxc-resolver/binding-wasm32-wasi": ["@oxc-resolver/binding-wasm32-wasi@11.24.2", "", { "dependencies": { "@emnapi/core": "1.11.2", "@emnapi/runtime": "1.11.2", "@napi-rs/wasm-runtime": "^1.1.6" }, "cpu": "none" }, "sha512-vHcssMPwO08RTvj/c0iOBz90attxyG3wQJ0dTcyEQK43LRpcdLWZlV5feBhv6Isn6ahbQIzHbCgfa81+RiML0Q=="], + + "@oxc-resolver/binding-win32-arm64-msvc": ["@oxc-resolver/binding-win32-arm64-msvc@11.24.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-uokJqro2iBqkFvJdKQLP7d8/BUmFwESQFVmIJUQKj1Xn1a/LysJoe1vmeECLF5b3jsV8CAL5sEMJXX6SdK9Nhg=="], + + "@oxc-resolver/binding-win32-x64-msvc": ["@oxc-resolver/binding-win32-x64-msvc@11.24.2", "", { "os": "win32", "cpu": "x64" }, "sha512-UqGPmo56KDfLlfXFAFIrNflHT8tFxWGEivWg3Zeyp4Uy2NlKN1FGPr6/BxcLGG3+kZ6Wp14g5Uj+n71boqZfiw=="], + + "@oxlint/binding-android-arm-eabi": ["@oxlint/binding-android-arm-eabi@1.74.0", "", { "os": "android", "cpu": "arm" }, "sha512-+gHd12muVI9ZLBaWLPkHt3Fj7jihFjgQ1MGtBaRL8vWrWrI0P7dLUty/cHrHS0oqPYIRgQUJsPu2CExQuMcwNw=="], + + "@oxlint/binding-android-arm64": ["@oxlint/binding-android-arm64@1.74.0", "", { "os": "android", "cpu": "arm64" }, "sha512-xjKdoMB+H+RCOByv/7l7nfIGW9mlOisqYdcyC75UqYuQecLpReAeEYUf2CNeDEI3KtmUgxpRw/+c63y4AeF/Bw=="], + + "@oxlint/binding-darwin-arm64": ["@oxlint/binding-darwin-arm64@1.74.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-iUK7wvc6sejMKsC+Pt67mntoF5weFcyEunhZfLJceU6gL419mexz5wBkSx/EnkFBExMLNtOi9fnDSc5xfK0IzQ=="], + + "@oxlint/binding-darwin-x64": ["@oxlint/binding-darwin-x64@1.74.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-ggKc/tn5SJ1u2yG2izC6VKODfYKV8MQ2AicJlNzOjuyrC29udvOef6/JzK2r32xqCnBDLFouR1VCkjzEI0/N9Q=="], + + "@oxlint/binding-freebsd-x64": ["@oxlint/binding-freebsd-x64@1.74.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-u++dH/43jy9hTLbneaWlS0gla/Bp1JdwJ2zgevCl8nDFUh6qRCGMxcL0f0lb7By3A9p/LfFr+7cG4HU1hG856g=="], + + "@oxlint/binding-linux-arm-gnueabihf": ["@oxlint/binding-linux-arm-gnueabihf@1.74.0", "", { "os": "linux", "cpu": "arm" }, "sha512-Sj1zmtFDVTPeIbIz4ZfcXAbFHqCmKCXdCUlAJzvTF7I20NTH1RDpoF2PhkqNODutJzVhJYmm3oz0GwgY+tvE2g=="], + + "@oxlint/binding-linux-arm-musleabihf": ["@oxlint/binding-linux-arm-musleabihf@1.74.0", "", { "os": "linux", "cpu": "arm" }, "sha512-//PKyQb/tQXcHArx2f7z+oVI/eMS2Jpv+edNuAtOrgIhWdGcpHxogveAxzmF2rpH1AIHp4Hq04RF/rgJdiICnQ=="], + + "@oxlint/binding-linux-arm64-gnu": ["@oxlint/binding-linux-arm64-gnu@1.74.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-/k1Me+aX2tjuH10K62mLS0y8cLkJBHX6Ce0xPK+eWeel4bSdEGZ8dv4+hYMzg0GrSmjwy4yAYsDPeEeKBft/2w=="], + + "@oxlint/binding-linux-arm64-musl": ["@oxlint/binding-linux-arm64-musl@1.74.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-3tFSjBxc5D8/zvjEuLvOqcA8ZXKD0+6NuaVO/edeamNc49MoAsbfaC9s1UiwODwgF6slGaF8yJA2TPkukd77tg=="], + + "@oxlint/binding-linux-ppc64-gnu": ["@oxlint/binding-linux-ppc64-gnu@1.74.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-9QggtPkSPXOCTu8Szis7auOK/sC7KdQaN+/TujP7YVVhzCAOhgdRfgv8uEz0r2tk5xdgus5rLYUrCDoZNtiRUw=="], + + "@oxlint/binding-linux-riscv64-gnu": ["@oxlint/binding-linux-riscv64-gnu@1.74.0", "", { "os": "linux", "cpu": "none" }, "sha512-VM5VPUJ4DJIWiK+AZn8FScUqMr6OFrCAYybMYjEEi7W13ParI64MByiXTkKMqZpBmvQ9zxl9Ebq2VUOiZRJYUg=="], + + "@oxlint/binding-linux-riscv64-musl": ["@oxlint/binding-linux-riscv64-musl@1.74.0", "", { "os": "linux", "cpu": "none" }, "sha512-SaDY1gh9rOA592J54g+gu5hkOFFQBZsMmIYHs+NRHG+Uq0OxtuuCXMWQ3vu1830Eugv5uMXyjG+bv2Z9y4IXjw=="], + + "@oxlint/binding-linux-s390x-gnu": ["@oxlint/binding-linux-s390x-gnu@1.74.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-ZATQeHZCyr6MbDveg0obD5sxLHFOghtOdC5jwVwYlvFWqtFOxctgFEG6Ef/64hYvZrWyhyCckB10AelqLopeDA=="], + + "@oxlint/binding-linux-x64-gnu": ["@oxlint/binding-linux-x64-gnu@1.74.0", "", { "os": "linux", "cpu": "x64" }, "sha512-+aIvJyrdeD7LwCQ2WYLMUWNmnbeDRSPb40aBYtPjD9+PTqUwgJnk+HK5yLfSMeqXrMrDhE9uTmtt2y50tvjhHw=="], + + "@oxlint/binding-linux-x64-musl": ["@oxlint/binding-linux-x64-musl@1.74.0", "", { "os": "linux", "cpu": "x64" }, "sha512-XyktaR8lhK2qWiCK0Tk8oYD+/cgn+oHA6ddRnxSSXUKkkojkV78CmShZUxQF+yrBFs0SuW+JBOPG6hecyc/iZg=="], + + "@oxlint/binding-openharmony-arm64": ["@oxlint/binding-openharmony-arm64@1.74.0", "", { "os": "none", "cpu": "arm64" }, "sha512-mzbjrPl4neaVUiJ1fUiEUxTGaSZBoiKtaoB6jmIpz9S+VOA2vDYmJpihQ82w6178V5jxziclTg8Cgj5yF6tTDg=="], + + "@oxlint/binding-win32-arm64-msvc": ["@oxlint/binding-win32-arm64-msvc@1.74.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-vUAe9okpS2Oa5+lX67lqHMuNUvfkleRKwrUDJ/WJBsgmddvZ1mrsh2HVmuFDRzqFELhaJhFaCNOuR6a7L3rtIA=="], + + "@oxlint/binding-win32-ia32-msvc": ["@oxlint/binding-win32-ia32-msvc@1.74.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-yyXXJyYYSXL4I8K8jAWjJs+J3fa9gH2JmEbo4f5adm+1tNC9itseicBNuwK7BDHvqQ5J534s+yDULu89vYL2ZQ=="], + + "@oxlint/binding-win32-x64-msvc": ["@oxlint/binding-win32-x64-msvc@1.74.0", "", { "os": "win32", "cpu": "x64" }, "sha512-VTC9IYTIMrVUk/i6Ms1ohzzDKZFkWn0KU2OBbPBzgmVZ2V30165T/zK4LztTr0Xgp9fZ1qQZ1rsZAu/rEmySlA=="], + + "@peculiar/asn1-schema": ["@peculiar/asn1-schema@2.8.0", "", { "dependencies": { "@peculiar/utils": "^2.0.2", "asn1js": "^3.0.10", "tslib": "^2.8.1" } }, "sha512-7YT0U/ze0tF2QOBbE15gKZwy5tvgGyLRiRHLzhlbOpf7BT032oBSd0haZqXn5W6l26WLlu3dyxzjM+2638/z2Q=="], + + "@peculiar/json-schema": ["@peculiar/json-schema@1.1.12", "", { "dependencies": { "tslib": "^2.0.0" } }, "sha512-coUfuoMeIB7B8/NMekxaDzLhaYmp0HZNPEjYRm9goRou8UZIC3z21s0sL9AWoCw4EG876QyO3kYrc61WNF9B/w=="], + + "@peculiar/utils": ["@peculiar/utils@2.0.3", "", { "dependencies": { "tslib": "^2.8.1" } }, "sha512-+oL3HPFRIZ1St2K50lWCXiioIgSoxzz7R1J3uF6neO2yl1sgmpgY6XXJH4BdpoDkMWznQTeYF6oWNDZLCdQ4eQ=="], + + "@peculiar/webcrypto": ["@peculiar/webcrypto@1.7.1", "", { "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" } }, "sha512-ODOov0sGMJMf3jPonOkgGqPknTsu+DdQ7kD++gz8aI+aFMOMHFbWAA2taqXXVTdP+OTOQR/znGvSpmkeI0WTYQ=="], "@pkgjs/parseargs": ["@pkgjs/parseargs@0.11.0", "", {}, "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg=="], - "@preact/signals": ["@preact/signals@1.3.4", "", { "dependencies": { "@preact/signals-core": "^1.7.0" }, "peerDependencies": { "preact": "10.x" } }, "sha512-TPMkStdT0QpSc8FpB63aOwXoSiZyIrPsP9Uj347KopdS6olZdAYeeird/5FZv/M1Yc1ge5qstub2o8VDbvkT4g=="], + "@posthog/browser-common": ["@posthog/browser-common@0.2.0", "", { "dependencies": { "@posthog/core": "^1.44.0", "@posthog/types": "^1.397.1" } }, "sha512-w+/0/Be/x48uNM7d/M37ZimSGwhuh8WBSvx61xzKzFnuoV5HqmH7GNDhaM3E3exXoBZmWNPS8hM/Ufy8zoOQSg=="], + + "@posthog/core": ["@posthog/core@1.44.0", "", { "dependencies": { "@posthog/types": "^1.397.0" } }, "sha512-uE+mdKvetxNQC6gWQf4MHIH8bGt+JN6z7ho0A4G3t9G1VGQTx9wHYHNwkKf3gzi+1oAXS9ej2VVY4pjWUU2PWg=="], + + "@posthog/types": ["@posthog/types@1.397.1", "", {}, "sha512-W/LpWbKVaaUnfZKuFuHa+Dg03D+fC87cM+PQbG+59JcSPW8F0JcBtSoXmpfrqbpuxUToMo+gktutrUkAb/KQBw=="], + + "@preact/signals": ["@preact/signals@2.9.4", "", { "dependencies": { "@preact/signals-core": "^1.14.4" }, "peerDependencies": { "preact": ">= 10.25.0 || >=11.0.0-0" } }, "sha512-JzAZXcRkmCNf9wVuxNI7UWseu3++Mm0dZ6UFyjFby44CyKIl7Y06oOsW3KA5Nx0L8tpE87OY5pLe0uQ0xcWKrg=="], - "@preact/signals-core": ["@preact/signals-core@1.14.1", "", {}, "sha512-vxPpfXqrwUe9lpjqfYNjAF/0RF/eFGeLgdJzdmIIZjpOnTmGmAB4BjWone562mJGMRP4frU6iZ6ei3PDsu52Ng=="], + "@preact/signals-core": ["@preact/signals-core@1.14.4", "", {}, "sha512-HNB6HYeYKhQbJ1aKl+YRjrS4+QWHLKX6qKoUsfS/m0vqzsVaEBiZiaKbG/e+NKk2ch5ALQr/ihWaMHxiCuuWHA=="], + + "@react-grab/cli": ["@react-grab/cli@0.1.48", "", { "dependencies": { "agent-install": "^0.0.6", "commander": "^14.0.3", "ignore": "^7.0.5", "ora": "^9.4.0", "package-manager-detector": "^1.6.0", "picocolors": "^1.1.1", "prompts": "^2.4.2", "tinyexec": "^1.1.2" }, "bin": { "react-grab": "bin/cli.js" } }, "sha512-KXRZFN0b78BeVa4Tq1FC9kiXPpC5lS4pQp/mvQ1azy9dZUJ3zfc7Ei84+yvGh+WoYdceMCFxXfBp6qhU/G056g=="], + + "@react-three/fiber": ["@react-three/fiber@9.6.1", "", { "dependencies": { "@babel/runtime": "^7.17.8", "@types/webxr": "*", "base64-js": "^1.5.1", "buffer": "^6.0.3", "its-fine": "^2.0.0", "react-use-measure": "^2.1.7", "scheduler": "^0.27.0", "suspend-react": "^0.1.3", "use-sync-external-store": "^1.4.0", "zustand": "^5.0.3" }, "peerDependencies": { "expo": ">=43.0", "expo-asset": ">=8.4", "expo-file-system": ">=11.0", "expo-gl": ">=11.0", "react": ">=19 <19.3", "react-dom": ">=19 <19.3", "react-native": ">=0.78", "three": ">=0.156" }, "optionalPeers": ["expo", "expo-asset", "expo-file-system", "expo-gl", "react-dom", "react-native"] }, "sha512-zF0rsKcVYpcJwbFEnv2HkHX9cvOEgsfQo/X8lwmR2dn13S4qEQJXir9fxf5js2LQFoXqxOY7MDkOkYx2uZ4gSg=="], "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.3", "", {}, "sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q=="], "@rollup/pluginutils": ["@rollup/pluginutils@5.3.0", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-walker": "^2.0.2", "picomatch": "^4.0.2" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q=="], - "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.57.1", "", { "os": "android", "cpu": "arm" }, "sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg=="], + "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.62.2", "", { "os": "android", "cpu": "arm" }, "sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg=="], + + "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.62.2", "", { "os": "android", "cpu": "arm64" }, "sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw=="], + + "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.62.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A=="], + + "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.62.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA=="], + + "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.62.2", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw=="], - "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.57.1", "", { "os": "android", "cpu": "arm64" }, "sha512-dQaAddCY9YgkFHZcFNS/606Exo8vcLHwArFZ7vxXq4rigo2bb494/xKMMwRRQW6ug7Js6yXmBZhSBRuBvCCQ3w=="], + "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.62.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg=="], - "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.57.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-crNPrwJOrRxagUYeMn/DZwqN88SDmwaJ8Cvi/TN1HnWBU7GwknckyosC2gd0IqYRsHDEnXf328o9/HC6OkPgOg=="], + "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.62.2", "", { "os": "linux", "cpu": "arm" }, "sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg=="], - "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.57.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-Ji8g8ChVbKrhFtig5QBV7iMaJrGtpHelkB3lsaKzadFBe58gmjfGXAOfI5FV0lYMH8wiqsxKQ1C9B0YTRXVy4w=="], + "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.62.2", "", { "os": "linux", "cpu": "arm" }, "sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA=="], - "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.57.1", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-R+/WwhsjmwodAcz65guCGFRkMb4gKWTcIeLy60JJQbXrJ97BOXHxnkPFrP+YwFlaS0m+uWJTstrUA9o+UchFug=="], + "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.62.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA=="], - "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.57.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-IEQTCHeiTOnAUC3IDQdzRAGj3jOAYNr9kBguI7MQAAZK3caezRrg0GxAb6Hchg4lxdZEI5Oq3iov/w/hnFWY9Q=="], + "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.62.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ=="], - "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.57.1", "", { "os": "linux", "cpu": "arm" }, "sha512-F8sWbhZ7tyuEfsmOxwc2giKDQzN3+kuBLPwwZGyVkLlKGdV1nvnNwYD0fKQ8+XS6hp9nY7B+ZeK01EBUE7aHaw=="], + "@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.62.2", "", { "os": "linux", "cpu": "none" }, "sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg=="], - "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.57.1", "", { "os": "linux", "cpu": "arm" }, "sha512-rGfNUfn0GIeXtBP1wL5MnzSj98+PZe/AXaGBCRmT0ts80lU5CATYGxXukeTX39XBKsxzFpEeK+Mrp9faXOlmrw=="], + "@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.62.2", "", { "os": "linux", "cpu": "none" }, "sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ=="], - "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.57.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-MMtej3YHWeg/0klK2Qodf3yrNzz6CGjo2UntLvk2RSPlhzgLvYEB3frRvbEF2wRKh1Z2fDIg9KRPe1fawv7C+g=="], + "@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.62.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A=="], - "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.57.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-1a/qhaaOXhqXGpMFMET9VqwZakkljWHLmZOX48R0I/YLbhdxr1m4gtG1Hq7++VhVUmf+L3sTAf9op4JlhQ5u1Q=="], + "@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.62.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w=="], - "@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.57.1", "", { "os": "linux", "cpu": "none" }, "sha512-QWO6RQTZ/cqYtJMtxhkRkidoNGXc7ERPbZN7dVW5SdURuLeVU7lwKMpo18XdcmpWYd0qsP1bwKPf7DNSUinhvA=="], + "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.62.2", "", { "os": "linux", "cpu": "none" }, "sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg=="], - "@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.57.1", "", { "os": "linux", "cpu": "none" }, "sha512-xpObYIf+8gprgWaPP32xiN5RVTi/s5FCR+XMXSKmhfoJjrpRAjCuuqQXyxUa/eJTdAE6eJ+KDKaoEqjZQxh3Gw=="], + "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.62.2", "", { "os": "linux", "cpu": "none" }, "sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q=="], - "@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.57.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-4BrCgrpZo4hvzMDKRqEaW1zeecScDCR+2nZ86ATLhAoJ5FQ+lbHVD3ttKe74/c7tNT9c6F2viwB3ufwp01Oh2w=="], + "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.62.2", "", { "os": "linux", "cpu": "s390x" }, "sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg=="], - "@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.57.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-NOlUuzesGauESAyEYFSe3QTUguL+lvrN1HtwEEsU2rOwdUDeTMJdO5dUYl/2hKf9jWydJrO9OL/XSSf65R5+Xw=="], + "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.62.2", "", { "os": "linux", "cpu": "x64" }, "sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A=="], - "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.57.1", "", { "os": "linux", "cpu": "none" }, "sha512-ptA88htVp0AwUUqhVghwDIKlvJMD/fmL/wrQj99PRHFRAG6Z5nbWoWG4o81Nt9FT+IuqUQi+L31ZKAFeJ5Is+A=="], + "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.62.2", "", { "os": "linux", "cpu": "x64" }, "sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg=="], - "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.57.1", "", { "os": "linux", "cpu": "none" }, "sha512-S51t7aMMTNdmAMPpBg7OOsTdn4tySRQvklmL3RpDRyknk87+Sp3xaumlatU+ppQ+5raY7sSTcC2beGgvhENfuw=="], + "@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.62.2", "", { "os": "openbsd", "cpu": "x64" }, "sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg=="], - "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.57.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-Bl00OFnVFkL82FHbEqy3k5CUCKH6OEJL54KCyx2oqsmZnFTR8IoNqBF+mjQVcRCT5sB6yOvK8A37LNm/kPJiZg=="], + "@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.62.2", "", { "os": "none", "cpu": "arm64" }, "sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA=="], - "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.57.1", "", { "os": "linux", "cpu": "x64" }, "sha512-ABca4ceT4N+Tv/GtotnWAeXZUZuM/9AQyCyKYyKnpk4yoA7QIAuBt6Hkgpw8kActYlew2mvckXkvx0FfoInnLg=="], + "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.62.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg=="], - "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.57.1", "", { "os": "linux", "cpu": "x64" }, "sha512-HFps0JeGtuOR2convgRRkHCekD7j+gdAuXM+/i6kGzQtFhlCtQkpwtNzkNj6QhCDp7DRJ7+qC/1Vg2jt5iSOFw=="], + "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.62.2", "", { "os": "win32", "cpu": "ia32" }, "sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q=="], - "@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.57.1", "", { "os": "openbsd", "cpu": "x64" }, "sha512-H+hXEv9gdVQuDTgnqD+SQffoWoc0Of59AStSzTEj/feWTBAnSfSD3+Dql1ZruJQxmykT/JVY0dE8Ka7z0DH1hw=="], + "@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.62.2", "", { "os": "win32", "cpu": "x64" }, "sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg=="], - "@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.57.1", "", { "os": "none", "cpu": "arm64" }, "sha512-4wYoDpNg6o/oPximyc/NG+mYUejZrCU2q+2w6YZqrAs2UcNUChIZXjtafAiiZSUc7On8v5NyNj34Kzj/Ltk6dQ=="], + "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.62.2", "", { "os": "win32", "cpu": "x64" }, "sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA=="], - "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.57.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-O54mtsV/6LW3P8qdTcamQmuC990HDfR71lo44oZMZlXU4tzLrbvTii87Ni9opq60ds0YzuAlEr/GNwuNluZyMQ=="], + "@sentry/conventions": ["@sentry/conventions@0.16.0", "", {}, "sha512-fO9PLmHdVURcSPUpWCItWAtgKiMwGdJHbovoSEyLplX5sxs2ugvI4CBPTrkkgqhObnZOD0CnWBKDzSVQYBKEyQ=="], - "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.57.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-P3dLS+IerxCT/7D2q2FYcRdWRl22dNbrbBEtxdWhXrfIMPP9lQhb5h4Du04mdl5Woq05jVCDPCMF7Ub0NAjIew=="], + "@sentry/core": ["@sentry/core@10.66.0", "", { "dependencies": { "@sentry/conventions": "^0.16.0" } }, "sha512-9UbgSvds7bMJsP561eWmeyMLcfOmnwxtnx2QuW3yLobzP2Ob7CyJCOzP4tGzlTAGDrzShkFEZhiyuBUKiEK2oQ=="], - "@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.57.1", "", { "os": "win32", "cpu": "x64" }, "sha512-VMBH2eOOaKGtIJYleXsi2B8CPVADrh+TyNxJ4mWPnKfLB/DBUmzW+5m1xUrcwWoMfSLagIRpjUFeW5CO5hyciQ=="], + "@sentry/node": ["@sentry/node@10.66.0", "", { "dependencies": { "@opentelemetry/api": "^1.9.1", "@opentelemetry/instrumentation": "^0.220.0", "@opentelemetry/sdk-trace-base": "^2.9.0", "@sentry/conventions": "^0.16.0", "@sentry/core": "10.66.0", "@sentry/node-core": "10.66.0", "@sentry/opentelemetry": "10.66.0", "@sentry/server-utils": "10.66.0", "import-in-the-middle": "^3.0.0" } }, "sha512-5Ow7iQiRjaSaEOmqEIkYV368hFFzShIZCXPoj+wX3JtOmKFrsNu6lr8/VJlQs7cyjFS6tzE50PrLrD8iAPS/8w=="], - "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.57.1", "", { "os": "win32", "cpu": "x64" }, "sha512-mxRFDdHIWRxg3UfIIAwCm6NzvxG0jDX/wBN6KsQFTvKFqqg9vTrWUE68qEjHt19A5wwx5X5aUi2zuZT7YR0jrA=="], + "@sentry/node-core": ["@sentry/node-core@10.66.0", "", { "dependencies": { "@sentry/conventions": "^0.16.0", "@sentry/core": "10.66.0", "@sentry/opentelemetry": "10.66.0", "import-in-the-middle": "^3.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.9.0", "@opentelemetry/core": "^1.30.1 || ^2.1.0", "@opentelemetry/exporter-trace-otlp-http": ">=0.57.0 <1", "@opentelemetry/instrumentation": ">=0.57.1 <1", "@opentelemetry/sdk-trace-base": "^1.30.1 || ^2.1.0" }, "optionalPeers": ["@opentelemetry/api", "@opentelemetry/core", "@opentelemetry/exporter-trace-otlp-http", "@opentelemetry/instrumentation", "@opentelemetry/sdk-trace-base"] }, "sha512-SUnXHROqSdSetKgZC1goDEKCuMz3OmQ1h4rxzWeexLyqan+pensZXfLouP6jzZXlA8e/HP7uQg78LnfqYDcZlQ=="], + + "@sentry/opentelemetry": ["@sentry/opentelemetry@10.66.0", "", { "dependencies": { "@sentry/conventions": "^0.16.0", "@sentry/core": "10.66.0" }, "peerDependencies": { "@opentelemetry/api": "^1.9.0", "@opentelemetry/core": "^1.30.1 || ^2.1.0", "@opentelemetry/sdk-trace-base": "^1.30.1 || ^2.1.0" } }, "sha512-K5Y9IettN9yIOnpqCs40HRLGqaGUoaQ50+ZsLqX2kPCk3TzJVpKZ9icKwaJ4Nxm72QgGVT5Ovfr/9FXbgd3b/Q=="], + + "@sentry/server-utils": ["@sentry/server-utils@10.66.0", "", { "dependencies": { "@apm-js-collab/code-transformer": "^0.18.0", "@apm-js-collab/code-transformer-bundler-plugins": "^0.6.1", "@apm-js-collab/tracing-hooks": "^0.13.0", "@sentry/conventions": "^0.16.0", "@sentry/core": "10.66.0" } }, "sha512-h9EM9Wz9Mc6w2Vn7Fyh6ozjy4JC2UbUvWf9Ra1HYA4KMeYqjFA5/o0oB4pg4w/TF+rb+9OF/HNqxjuczxpudpA=="], "@sindresorhus/is": ["@sindresorhus/is@4.6.0", "", {}, "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw=="], @@ -277,6 +483,10 @@ "@tootallnate/once": ["@tootallnate/once@2.0.0", "", {}, "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A=="], + "@tweenjs/tween.js": ["@tweenjs/tween.js@23.1.3", "", {}, "sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA=="], + + "@tybys/wasm-util": ["@tybys/wasm-util@0.10.3", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg=="], + "@types/babel__core": ["@types/babel__core@7.20.5", "", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="], "@types/babel__generator": ["@types/babel__generator@7.27.0", "", { "dependencies": { "@babel/types": "^7.0.0" } }, "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg=="], @@ -289,7 +499,9 @@ "@types/debug": ["@types/debug@4.1.12", "", { "dependencies": { "@types/ms": "*" } }, "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ=="], - "@types/discord-rpc": ["@types/discord-rpc@4.0.10", "", { "dependencies": { "@types/events": "*" } }, "sha512-V7uQUUjYHwQ+8LyTcPZvExOi8yv310XZgZxzsVljTXhmik4apgRuDj+oqz15n0On0qXunEuvDdhD7VZyQgXm6g=="], + "@types/discord-rpc": ["@types/discord-rpc@4.0.11", "", { "dependencies": { "@types/events": "*" } }, "sha512-w2WgzgtyDNHMbmIeogN4f1uy1Mz2Woe+27XcvyjPRcCI9QdWyJifHHIJopwXm8f04fHia/vkhId/eyqxmbLoHw=="], + + "@types/esrecurse": ["@types/esrecurse@4.3.1", "", {}, "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw=="], "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], @@ -299,43 +511,61 @@ "@types/http-cache-semantics": ["@types/http-cache-semantics@4.2.0", "", {}, "sha512-L3LgimLHXtGkWikKnsPg0/VFx9OGZaC+eN1u4r+OB1XRqH3meBIAVC2zr1WdMH+RHmnRkqliQAOHNJ/E0j/e0Q=="], + "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="], + "@types/keyv": ["@types/keyv@3.1.4", "", { "dependencies": { "@types/node": "*" } }, "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg=="], "@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="], - "@types/node": ["@types/node@22.19.17", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-wGdMcf+vPYM6jikpS/qhg6WiqSV/OhG+jeeHT/KlVqxYfD40iYJf9/AE1uQxVWFvU7MipKRkRv8NSHiCGgPr8Q=="], + "@types/node": ["@types/node@22.20.1", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-EANqOCF9QFyra+4pfxUcX9STKJpCLjMbObVzljIJomAWSnuSIEAvyzEU53GaajbXJEgdh0iEcPL+DGvpUd4k1Q=="], "@types/plist": ["@types/plist@3.0.5", "", { "dependencies": { "@types/node": "*", "xmlbuilder": ">=11.0.1" } }, "sha512-E6OCaRmAe4WDmWNsL/9RMqdkkzDCY1etutkflWk4c+AcjDU07Pcz1fQwTX0TQz+Pxqn9i4L1TU3UFpjnrcDgxA=="], "@types/qrcode": ["@types/qrcode@1.5.6", "", { "dependencies": { "@types/node": "*" } }, "sha512-te7NQcV2BOvdj2b1hCAHzAoMNuj65kNBMz0KBaxM6c3VGBOhU0dURQKOtH8CFNI/dsKkwlv32p26qYQTWoB5bw=="], - "@types/react": ["@types/react@19.2.14", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w=="], + "@types/react": ["@types/react@19.2.17", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw=="], "@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="], + "@types/react-reconciler": ["@types/react-reconciler@0.28.9", "", { "peerDependencies": { "@types/react": "*" } }, "sha512-HHM3nxyUZ3zAylX8ZEyrDNd2XZOnQ0D5XfunJF5FLQnZbHHYq4UWvW1QfelQNXv1ICNkwYhfxjwfnqivYB6bFg=="], + "@types/responselike": ["@types/responselike@1.0.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw=="], + "@types/stats.js": ["@types/stats.js@0.17.4", "", {}, "sha512-jIBvWWShCvlBqBNIZt0KAshWpvSjhkwkEu4ZUcASoAvhmrgAUI2t1dXrjSL4xXVLB4FznPrIsX3nKXFl/Dt4vA=="], + + "@types/three": ["@types/three@0.185.1", "", { "dependencies": { "@dimforge/rapier3d-compat": "~0.12.0", "@tweenjs/tween.js": "~23.1.3", "@types/stats.js": "*", "@types/webxr": ">=0.5.17", "fflate": "~0.8.2", "meshoptimizer": "~1.1.1" } }, "sha512-db1xTb+EgYF2didW+eudSvVPtn75zo+fGsY8ShQrJY/B5ZBmC2Fiaykv3aImHAlCNEGuMPkPGXBJGLwzu5mC7A=="], + + "@types/trusted-types": ["@types/trusted-types@2.0.7", "", {}, "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw=="], + "@types/verror": ["@types/verror@1.10.11", "", {}, "sha512-RlDm9K7+o5stv0Co8i8ZRGxDbrTxhJtgjqjFyVh/tXQyl/rYtTKlnTvZ88oSTeYREWurwx20Js4kTuKCsFkUtg=="], + "@types/webxr": ["@types/webxr@0.5.24", "", {}, "sha512-h8fgEd/DpoS9CBrjEQXR+dIDraopAEfu4wYVNY2tEPwk60stPWhvZMf4Foo5FakuQ7HFZoa8WceaWFervK2Ovg=="], + "@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="], - "@types/yauzl": ["@types/yauzl@2.10.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q=="], + "@typescript-eslint/types": ["@typescript-eslint/types@8.64.0", "", {}, "sha512-qjhfuTfLXjA4IOzXvz0rTjT01BqEiIgPoUeMwiEjnaHKJMTNo8rH5pYW1a2L/0Dnux2fPC85AeyJoWaGa8WxTA=="], "@vitejs/plugin-react": ["@vitejs/plugin-react@5.2.0", "", { "dependencies": { "@babel/core": "^7.29.0", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-rc.3", "@types/babel__core": "^7.20.5", "react-refresh": "^0.18.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-YmKkfhOAi3wsB1PhJq5Scj3GXMn3WvtQ/JC0xoopuHoXSdmtdStOpFrYaT1kie2YgFBcIe64ROzMYRjCrYOdYw=="], - "@xmldom/xmldom": ["@xmldom/xmldom@0.8.11", "", {}, "sha512-cQzWCtO6C8TQiYl1ruKNn2U6Ao4o4WBBcbL61yJl84x+j5sOWWFU9X7DpND8XZG3daDppSsigMdfAIl2upQBRw=="], + "@xmldom/xmldom": ["@xmldom/xmldom@0.8.13", "", {}, "sha512-KRYzxepc14G/CEpEGc3Yn+JKaAeT63smlDr+vjB8jRfgTBBI9wRj/nkQEO+ucV8p8I9bfKLWp37uHgFrbntPvw=="], - "abbrev": ["abbrev@3.0.1", "", {}, "sha512-AO2ac6pjRB3SJmGJo+v5/aK6Omggp6fsLrs6wN9bd35ulu4cCwaAU9+7ZhXjeqHVkaHThLuzH0nZr0YpCDhygg=="], + "abbrev": ["abbrev@4.0.0", "", {}, "sha512-a1wflyaL0tHtJSmLSOVybYhy22vRih4eduhhrkcjgrWGnRfrZtovJ2FRjxuTtkkj47O/baf0R86QU5OuYpz8fA=="], "acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="], + "acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="], + "agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="], + "agent-install": ["agent-install@0.0.5", "", { "dependencies": { "@iarna/toml": "^2.2.5", "commander": "^14.0.0", "jsonc-parser": "^3.3.1", "picocolors": "^1.1.1", "prompts": "^2.4.2", "yaml": "^2.8.3" }, "bin": { "agent-install": "bin/agent-install.mjs" } }, "sha512-nHlms9BkP8ZiY79HrwCGiA2DcNaXrAaJrCM/BEqQ7MEsSKyCk+2A76xPGylIfASZSZE0SaU3T0bNSg4rBPIJAQ=="], + "agentkeepalive": ["agentkeepalive@4.6.0", "", { "dependencies": { "humanize-ms": "^1.2.1" } }, "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ=="], "aggregate-error": ["aggregate-error@3.1.0", "", { "dependencies": { "clean-stack": "^2.0.0", "indent-string": "^4.0.0" } }, "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA=="], - "ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="], + "ajv": ["ajv@8.20.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA=="], + + "ajv-formats": ["ajv-formats@3.0.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ=="], "ajv-keywords": ["ajv-keywords@3.5.2", "", { "peerDependencies": { "ajv": "^6.9.1" } }, "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ=="], @@ -343,9 +573,9 @@ "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], - "app-builder-bin": ["app-builder-bin@5.0.0-alpha.12", "", {}, "sha512-j87o0j6LqPL3QRr8yid6c+Tt5gC7xNfYo6uQIQkorAC6MpeayVMZrEDzKmJJ/Hlv7EnOQpaRm53k6ktDYZyB6w=="], + "app-builder-bin": ["app-builder-bin@5.0.0-alpha.10", "", {}, "sha512-Ev4jj3D7Bo+O0GPD2NMvJl+PGiBAfS7pUGawntBNpCbxtpncfUixqFj9z9Jme7V7s3LBGqsWZZP54fxBX3JKJw=="], - "app-builder-lib": ["app-builder-lib@26.8.1", "", { "dependencies": { "@develar/schema-utils": "~2.6.5", "@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.3", "@electron/universal": "2.0.3", "@malept/flatpak-bundler": "^0.4.0", "@types/fs-extra": "9.0.13", "async-exit-hook": "^2.0.1", "builder-util": "26.8.1", "builder-util-runtime": "9.5.1", "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.8.1", "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.0.3", "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", "which": "^5.0.0" }, "peerDependencies": { "dmg-builder": "26.8.1", "electron-builder-squirrel-windows": "26.8.1" } }, "sha512-p0Im/Dx5C4tmz8QEE1Yn4MkuPC8PrnlRneMhWJj7BBXQfNTJUshM/bp3lusdEsDbvvfJZpXWnYesgSLvwtM2Zw=="], + "app-builder-lib": ["app-builder-lib@26.15.3", "", { "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" }, "peerDependencies": { "dmg-builder": "26.15.3", "electron-builder-squirrel-windows": "26.15.3" } }, "sha512-2VnyWkqsP5v5XbBhL3tD5Syx8iNPBYsoU7kY4S2fz7wg8Rj/nztWKCUzGKaFRTv0Xwf3/H058CR1Kvtd/3lRow=="], "aproba": ["aproba@2.1.0", "", {}, "sha512-tLIEcj5GuR2RSTnxNKdkK0dJ/GrC7P38sUkiDmDuHfsHmbagTFAxDVIBltoklXEVIQ/f14IL8IMJ5pn9Hez1Ew=="], @@ -357,10 +587,14 @@ "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], + "asn1js": ["asn1js@3.0.10", "", { "dependencies": { "pvtsutils": "^1.3.6", "pvutils": "^1.1.5", "tslib": "^2.8.1" } }, "sha512-S2s3aOytiKdFRdulw2qPE51MzjzVOisppcVv7jVFR+Kw0kxwvFrDcYA0h7Ndqbmj0HkMIXYWaoj7fli8kgx1eg=="], + "assert-plus": ["assert-plus@1.0.0", "", {}, "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw=="], "astral-regex": ["astral-regex@2.0.0", "", {}, "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ=="], + "astring": ["astring@1.9.0", "", { "bin": { "astring": "bin/astring" } }, "sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg=="], + "async": ["async@3.2.6", "", {}, "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA=="], "async-exit-hook": ["async-exit-hook@2.0.1", "", {}, "sha512-NW2cX8m1Q7KPA7a5M2ULQeZ2wR5qI5PAbw5L0UOMxdioVk9PMZ0h1TmyZEkPYrCvYjDlFICusOu1dlEKAAeXBw=="], @@ -369,6 +603,10 @@ "at-least-node": ["at-least-node@1.0.0", "", {}, "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg=="], + "atomically": ["atomically@2.1.1", "", { "dependencies": { "stubborn-fs": "^2.0.0", "when-exit": "^2.1.4" } }, "sha512-P4w9o2dqARji6P7MHprklbfiArZAWvo07yW7qs3pdljb3BWr12FIB7W+p0zJiuiVsUpRO0iZn1kFFcpPegg0tQ=="], + + "aws4": ["aws4@1.13.2", "", {}, "sha512-lHe62zvbTB5eEABUVi/AwVh0ZKY9rMMDhmm+eeyuuUQbQ3+J+fONVQOZyj+DdrvD4BY33uYniyRJ4UJIaSKAfw=="], + "balanced-match": ["balanced-match@4.0.2", "", { "dependencies": { "jackspeak": "^4.2.3" } }, "sha512-x0K50QvKQ97fdEz2kPehIerj+YTeptKF9hyYkKf6egnwmMWAkADiO0QCzSp0R5xN8FTZgYaBfSaue46Ej62nMg=="], "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], @@ -387,23 +625,27 @@ "boolean": ["boolean@3.2.0", "", {}, "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw=="], - "brace-expansion": ["brace-expansion@5.0.2", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-Pdk8c9poy+YhOgVWw1JNN22/HcivgKWwpxKq04M/jTmHyCZn12WPJebZxdjSa5TmBqISrUSgNYU3eRORljfCCw=="], + "brace-expansion": ["brace-expansion@5.0.7", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA=="], + + "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], "browserslist": ["browserslist@4.28.1", "", { "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", "electron-to-chromium": "^1.5.263", "node-releases": "^2.0.27", "update-browserslist-db": "^1.2.0" }, "bin": "cli.js" }, "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA=="], - "buffer": ["buffer@5.7.1", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="], + "buffer": ["buffer@6.0.3", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.2.1" } }, "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA=="], "buffer-crc32": ["buffer-crc32@0.2.13", "", {}, "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ=="], "buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="], - "builder-util": ["builder-util@26.8.1", "", { "dependencies": { "7zip-bin": "~5.2.0", "@types/debug": "^4.1.6", "app-builder-bin": "5.0.0-alpha.12", "builder-util-runtime": "9.5.1", "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" } }, "sha512-pm1lTYbGyc90DHgCDO7eo8Rl4EqKLciayNbZqGziqnH9jrlKe8ZANGdityLZU+pJh16dfzjAx2xQq9McuIPEtw=="], + "builder-util": ["builder-util@26.15.3", "", { "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" } }, "sha512-q2hn7Mbo2nFNkVekPiHFx6Nfo3hURmES3tfBn+k5Pqxl2RkmP3QGqZUhH/q9Pch/4G05NRhPjDlVj1O8q4Txvw=="], + + "builder-util-runtime": ["builder-util-runtime@9.7.0", "", { "dependencies": { "debug": "^4.3.4", "sax": "^1.2.4" } }, "sha512-g/kR520giAFYkSXTzcmF3kqQq7wi8F6N6SzeDgZrqTBN+VHdmgWOyTdD1yD7AATDId/yXLvuP34CxW46/BwCdw=="], - "builder-util-runtime": ["builder-util-runtime@9.5.1", "", { "dependencies": { "debug": "^4.3.4", "sax": "^1.2.4" } }, "sha512-qt41tMfgHTllhResqM5DcnHyDIWNgzHvuY2jDcYP9iaGpkWxTUzV6GQjDeLnlR1/DtdlcsWQbA7sByMpmJFTLQ=="], + "bytestreamjs": ["bytestreamjs@2.0.1", "", {}, "sha512-U1Z/ob71V/bXfVABvNr/Kumf5VyeQRBEm6Txb0PQ6S7V5GpBM3w4Cbqz/xPDicR5tN0uvDifng8C+5qECeGwyQ=="], "cac": ["cac@6.7.14", "", {}, "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ=="], - "cacache": ["cacache@19.0.1", "", { "dependencies": { "@npmcli/fs": "^4.0.0", "fs-minipass": "^3.0.0", "glob": "^10.2.2", "lru-cache": "^10.0.1", "minipass": "^7.0.3", "minipass-collect": "^2.0.1", "minipass-flush": "^1.0.5", "minipass-pipeline": "^1.2.4", "p-map": "^7.0.2", "ssri": "^12.0.0", "tar": "^7.4.3", "unique-filename": "^4.0.0" } }, "sha512-hdsUxulXCi5STId78vRVYEtDAjq99ICAUktLTeTYsLoTE6Z8dS0c8pWNCxwdrk9YfJeobDZc2Y186hD/5ZQgFQ=="], + "cacache": ["cacache@16.1.3", "", { "dependencies": { "@npmcli/fs": "^2.1.0", "@npmcli/move-file": "^2.0.0", "chownr": "^2.0.0", "fs-minipass": "^2.1.0", "glob": "^8.0.1", "infer-owner": "^1.0.4", "lru-cache": "^7.7.1", "minipass": "^3.1.6", "minipass-collect": "^1.0.2", "minipass-flush": "^1.0.5", "minipass-pipeline": "^1.2.4", "mkdirp": "^1.0.4", "p-map": "^4.0.0", "promise-inflight": "^1.0.1", "rimraf": "^3.0.2", "ssri": "^9.0.0", "tar": "^6.1.11", "unique-filename": "^2.0.0" } }, "sha512-/+Emcj9DAXxX4cwlLmRI9c166RuL3w30zp4R7Joiv2cQTtTtA+jeuCAjH3ZlGnYS3tKENSrKhAzVVP9GVyzeYQ=="], "cacheable-lookup": ["cacheable-lookup@5.0.4", "", {}, "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA=="], @@ -423,11 +665,13 @@ "ci-info": ["ci-info@4.4.0", "", {}, "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg=="], + "cjs-module-lexer": ["cjs-module-lexer@2.2.0", "", {}, "sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ=="], + "clean-stack": ["clean-stack@2.2.0", "", {}, "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A=="], - "cli-cursor": ["cli-cursor@3.1.0", "", { "dependencies": { "restore-cursor": "^3.1.0" } }, "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw=="], + "cli-cursor": ["cli-cursor@5.0.0", "", { "dependencies": { "restore-cursor": "^5.0.0" } }, "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw=="], - "cli-spinners": ["cli-spinners@2.9.2", "", {}, "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg=="], + "cli-spinners": ["cli-spinners@3.4.0", "", {}, "sha512-bXfOC4QcT1tKXGorxL3wbJm6XJPDqEnij2gQ2m7ESQuE+/z9YFIWnl/5RpTiKWbMq3EVKR4fRLJGn6DVfu0mpw=="], "cli-truncate": ["cli-truncate@2.1.0", "", { "dependencies": { "slice-ansi": "^3.0.0", "string-width": "^4.2.0" } }, "sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg=="], @@ -453,12 +697,18 @@ "concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="], + "conf": ["conf@15.1.0", "", { "dependencies": { "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "atomically": "^2.0.3", "debounce-fn": "^6.0.0", "dot-prop": "^10.0.0", "env-paths": "^3.0.0", "json-schema-typed": "^8.0.1", "semver": "^7.7.2", "uint8array-extras": "^1.5.0" } }, "sha512-Uy5YN9KEu0WWDaZAVJ5FAmZoaJt9rdK6kH+utItPyGsCqCgaTKkrmZx3zoE0/3q6S3bcp3Ihkk+ZqPxWxFK5og=="], + + "confbox": ["confbox@0.2.4", "", {}, "sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ=="], + "config-file-ts": ["config-file-ts@0.2.8-rc1", "", { "dependencies": { "glob": "^10.3.12", "typescript": "^5.4.3" } }, "sha512-GtNECbVI82bT4RiDIzBSVuTKoSHufnU7Ce7/42bkWZJZFLjmDF2WBpVsvRkhKCfKBnTBb3qZrBwPpFBU/Myvhg=="], "console-control-strings": ["console-control-strings@1.1.0", "", {}, "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ=="], "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="], + "core-js": ["core-js@3.49.0", "", {}, "sha512-es1U2+YTtzpwkxVLwAFdSpaIMyQaq0PBgm3YD1W3Qpsn1NAmO3KSgZfu+oGSWVu6NvLHoHCV/aYcsE5wiB7ALg=="], + "core-util-is": ["core-util-is@1.0.2", "", {}, "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ=="], "crc": ["crc@3.8.0", "", { "dependencies": { "buffer": "^5.1.0" } }, "sha512-iX3mfgcTMIq3ZKLIsVFAbv7+Mc10kxabAGQb8HvjA1o3T1PIYprbakQ65d3I+2HGHt6nSKkM9PYjgoJO2KcFBQ=="], @@ -473,12 +723,16 @@ "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], + "debounce-fn": ["debounce-fn@6.0.0", "", { "dependencies": { "mimic-function": "^5.0.0" } }, "sha512-rBMW+F2TXryBwB54Q0d8drNEI+TfoS9JpNTAoVpukbWEhjXQq4rySFYLaqXMFXwdv61Zb2OHtj5bviSoimqxRQ=="], + "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], "decamelize": ["decamelize@1.2.0", "", {}, "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA=="], "decompress-response": ["decompress-response@6.0.0", "", { "dependencies": { "mimic-response": "^3.1.0" } }, "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ=="], + "deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="], + "defaults": ["defaults@1.0.4", "", { "dependencies": { "clone": "^1.0.2" } }, "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A=="], "defer-to-connect": ["defer-to-connect@2.0.1", "", {}, "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg=="], @@ -491,6 +745,8 @@ "delegates": ["delegates@1.0.0", "", {}, "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ=="], + "deslop-js": ["deslop-js@0.8.1", "", { "dependencies": { "@oxc-project/types": "^0.138.0", "fast-glob": "^3.3.3", "minimatch": "^10.2.5", "oxc-parser": "^0.138.0", "oxc-resolver": "^11.23.0", "typescript": ">=5.0.4 <6" } }, "sha512-a8wpwOPo6HsYyRndn17C88mNzeQD6t17yhZ8qpyFWTxj5jQeWtXDj+zJfnuT8++5A4LaNeNAnKs1edVWuadNdQ=="], + "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], "detect-node": ["detect-node@2.1.0", "", {}, "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g=="], @@ -501,31 +757,37 @@ "discord-rpc": ["discord-rpc@4.0.1", "", { "dependencies": { "node-fetch": "^2.6.1", "ws": "^7.3.1" }, "optionalDependencies": { "register-scheme": "github:devsnek/node-register-scheme" } }, "sha512-HOvHpbq5STRZJjQIBzwoKnQ0jHplbEWFWlPDwXXKm/bILh4nzjcg7mNqll0UY7RsjFoaXA7e/oYb/4lvpda2zA=="], - "dmg-builder": ["dmg-builder@26.8.1", "", { "dependencies": { "app-builder-lib": "26.8.1", "builder-util": "26.8.1", "fs-extra": "^10.1.0", "iconv-lite": "^0.6.2", "js-yaml": "^4.1.0" }, "optionalDependencies": { "dmg-license": "^1.0.11" } }, "sha512-glMJgnTreo8CFINujtAhCgN96QAqApDMZ8Vl1r8f0QT8QprvC1UCltV4CcWj20YoIyLZx6IUskaJZ0NV8fokcg=="], + "dmg-builder": ["dmg-builder@26.15.3", "", { "dependencies": { "app-builder-lib": "26.15.3", "builder-util": "26.15.3", "fs-extra": "^10.1.0", "js-yaml": "^4.1.0" } }, "sha512-O3zJUFUYHJKgzPqioHxfxzBzlSC1eXCSr79gMSBKBP5AgjjpmrydMsMLotEg9fAJF36vdUncb+4ndRNxoPdlSQ=="], "dmg-license": ["dmg-license@1.0.11", "", { "dependencies": { "@types/plist": "^3.0.1", "@types/verror": "^1.10.3", "ajv": "^6.10.0", "crc": "^3.8.0", "iconv-corefoundation": "^1.1.7", "plist": "^3.0.4", "smart-buffer": "^4.0.2", "verror": "^1.10.0" }, "os": "darwin", "bin": "bin/dmg-license.js" }, "sha512-ZdzmqwKmECOWJpqefloC5OJy1+WZBBse5+MR88z9g9Zn4VY+WYUkAyojmhzJckH5YbbZGcYIuGAkY5/Ys5OM2Q=="], + "dompurify": ["dompurify@3.4.12", "", { "optionalDependencies": { "@types/trusted-types": "^2.0.7" } }, "sha512-zQvGet8Z2sWbQhCmfFz/T5QWH2oBmjnqK3qvOjaqaNLrLEF912WamU+ohnTp0TCep/MFVHpdJuCZEdFOdTnEFg=="], + + "dot-prop": ["dot-prop@10.1.0", "", { "dependencies": { "type-fest": "^5.0.0" } }, "sha512-MVUtAugQMOff5RnBy2d9N31iG0lNwg1qAoAOn7pOK5wf94WIaE3My2p3uwTQuvS2AcqchkcR3bHByjaM0mmi7Q=="], + "dotenv": ["dotenv@16.6.1", "", {}, "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow=="], "dotenv-expand": ["dotenv-expand@11.0.7", "", { "dependencies": { "dotenv": "^16.4.5" } }, "sha512-zIHwmZPRshsCdpMDyVsqGmgyP0yT8GAgXUnkdAoJisxvf33k7yO6OuoKmcTGuXPWSsm8Oh88nZicRLA9Y0rUeA=="], "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], + "duplexer2": ["duplexer2@0.1.4", "", { "dependencies": { "readable-stream": "^2.0.2" } }, "sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA=="], + "eastasianwidth": ["eastasianwidth@0.2.0", "", {}, "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="], "ejs": ["ejs@3.1.10", "", { "dependencies": { "jake": "^10.8.5" }, "bin": "bin/cli.js" }, "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA=="], - "electron": ["electron@42.3.3", "", { "dependencies": { "@electron/get": "^5.0.0", "@types/node": "^24.9.0", "extract-zip": "^2.0.1" }, "bin": { "electron": "cli.js", "install-electron": "install.js" } }, "sha512-0MwYp9wTb7TrtTalOYqeW+suqd9T/Znstr/nDLKqFGIjHdBZX339guo3mQqTPURRZ/UQmYM4uMpzKpI5wLptfQ=="], + "electron": ["electron@43.1.1", "", { "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" } }, "sha512-I5c5vfuVvaXpWx3IZdwvXgxQW44+e7OP1wXGVQkogLeSFSkUZ6sLCcWV05AdEcs65AO5tAIJJwbp7ixw+LdarA=="], - "electron-builder": ["electron-builder@26.8.1", "", { "dependencies": { "app-builder-lib": "26.8.1", "builder-util": "26.8.1", "builder-util-runtime": "9.5.1", "chalk": "^4.1.2", "ci-info": "^4.2.0", "dmg-builder": "26.8.1", "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" } }, "sha512-uWhx1r74NGpCagG0ULs/P9Nqv2nsoo+7eo4fLUOB8L8MdWltq9odW/uuLXMFCDGnPafknYLZgjNX0ZIFRzOQAw=="], + "electron-builder": ["electron-builder@26.15.3", "", { "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" } }, "sha512-a1KM5heqS3gQCZzizXEI8RjJy3QVogULPdeSknt76uLDpBIW/HDGsMg/XgP0riP6PI9COsRvFITKKGDqA8fJxA=="], "electron-builder-squirrel-windows": ["electron-builder-squirrel-windows@25.1.8", "", { "dependencies": { "app-builder-lib": "25.1.8", "archiver": "^5.3.1", "builder-util": "25.1.7", "fs-extra": "^10.1.0" } }, "sha512-2ntkJ+9+0GFP6nAISiMabKt6eqBB0kX1QqHNWFWAXgi0VULKGisM46luRFpIBiU3u/TDmhZMM8tzvo2Abn3ayg=="], - "electron-publish": ["electron-publish@26.8.1", "", { "dependencies": { "@types/fs-extra": "^9.0.11", "builder-util": "26.8.1", "builder-util-runtime": "9.5.1", "chalk": "^4.1.2", "form-data": "^4.0.5", "fs-extra": "^10.1.0", "lazy-val": "^1.0.5", "mime": "^2.5.2" } }, "sha512-q+jrSTIh/Cv4eGZa7oVR+grEJo/FoLMYBAnSL5GCtqwUpr1T+VgKB/dn1pnzxIxqD8S/jP1yilT9VrwCqINR4w=="], + "electron-publish": ["electron-publish@26.15.3", "", { "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" } }, "sha512-g/2bn8YTavY4cuS5F+jOS7zmZbXXBV8KZ8yHKfJjFPoKtzBqrpCdNPxBd3tqdBwP7BVd0lGzf7Bk2s0KesWZ4Q=="], "electron-to-chromium": ["electron-to-chromium@1.5.286", "", {}, "sha512-9tfDXhJ4RKFNerfjdCcZfufu49vg620741MNs26a9+bhLThdB+plgMeou98CAaHu/WATj2iHOOHTp1hWtABj2A=="], - "electron-updater": ["electron-updater@6.8.3", "", { "dependencies": { "builder-util-runtime": "9.5.1", "fs-extra": "^10.1.0", "js-yaml": "^4.1.0", "lazy-val": "^1.0.5", "lodash.escaperegexp": "^4.1.2", "lodash.isequal": "^4.5.0", "semver": "~7.7.3", "tiny-typed-emitter": "^2.1.0" } }, "sha512-Z6sgw3jgbikWKXei1ENdqFOxBP0WlXg3TtKfz0rgw2vIZFJUyI4pD7ZN7jrkm7EoMK+tcm/qTnPUdqfZukBlBQ=="], + "electron-updater": ["electron-updater@6.8.9", "", { "dependencies": { "builder-util-runtime": "9.7.0", "fs-extra": "^10.1.0", "js-yaml": "^4.1.0", "lazy-val": "^1.0.5", "lodash.escaperegexp": "^4.1.2", "lodash.isequal": "^4.5.0", "semver": "~7.7.3", "tiny-typed-emitter": "^2.1.0" } }, "sha512-ZhVxM9iGONUpZGI1FxdMRgJjUFXi7AYGVa5PwKlO1tV1/4zDxQmfKpXOHVztKrd6L9rLcFjERvi1Mf2vxyTkig=="], "electron-vite": ["electron-vite@5.0.0", "", { "dependencies": { "@babel/core": "^7.28.4", "@babel/plugin-transform-arrow-functions": "^7.27.1", "cac": "^6.7.14", "esbuild": "^0.25.11", "magic-string": "^0.30.19", "picocolors": "^1.1.1" }, "peerDependencies": { "@swc/core": "^1.0.0", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0" }, "optionalPeers": ["@swc/core"], "bin": { "electron-vite": "bin/electron-vite.js" } }, "sha512-OHp/vjdlubNlhNkPkL/+3JD34ii5ov7M0GpuXEVdQeqdQ3ulvVR7Dg/rNBLfS5XPIFwgoBLDf9sjjrL+CuDyRQ=="], @@ -543,6 +805,8 @@ "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="], + "es-module-lexer": ["es-module-lexer@2.3.1", "", {}, "sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA=="], + "es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="], "es-set-tostringtag": ["es-set-tostringtag@2.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA=="], @@ -555,33 +819,65 @@ "escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], - "estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="], + "eslint": ["eslint@10.7.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.2", "@eslint/config-array": "^0.23.5", "@eslint/config-helpers": "^0.6.0", "@eslint/core": "^1.2.1", "@eslint/plugin-kit": "^0.7.2", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.14.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^9.1.2", "eslint-visitor-keys": "^5.0.1", "espree": "^11.2.0", "esquery": "^1.7.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "minimatch": "^10.2.4", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-GVTD7s1vdIl6UYvAfriOPeY1Df8LIZjfofLvHwde+erDHGGuHyuM6xoxRxmHiebhYuD2p1vN4wWh0XzPARSGDQ=="], - "exponential-backoff": ["exponential-backoff@3.1.3", "", {}, "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA=="], + "eslint-plugin-react-hooks": ["eslint-plugin-react-hooks@7.1.1", "", { "dependencies": { "@babel/core": "^7.24.4", "@babel/parser": "^7.24.4", "hermes-parser": "^0.25.1", "zod": "^3.25.0 || ^4.0.0", "zod-validation-error": "^3.5.0 || ^4.0.0" }, "peerDependencies": { "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 || ^10.0.0" } }, "sha512-f2I7Gw6JbvCexzIInuSbZpfdQ44D7iqdWX01FKLvrPgqxoE7oMj8clOfto8U6vYiz4yd5oKu39rRSVOe1zRu0g=="], + + "eslint-scope": ["eslint-scope@9.1.2", "", { "dependencies": { "@types/esrecurse": "^4.3.1", "@types/estree": "^1.0.8", "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ=="], + + "eslint-visitor-keys": ["eslint-visitor-keys@5.0.1", "", {}, "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA=="], + + "espree": ["espree@11.2.0", "", { "dependencies": { "acorn": "^8.16.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^5.0.1" } }, "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw=="], + + "esquery": ["esquery@1.7.0", "", { "dependencies": { "estraverse": "^5.1.0" } }, "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g=="], - "extract-zip": ["extract-zip@2.0.1", "", { "dependencies": { "debug": "^4.1.1", "get-stream": "^5.1.0", "yauzl": "^2.10.0" }, "optionalDependencies": { "@types/yauzl": "^2.9.1" }, "bin": "cli.js" }, "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg=="], + "esrecurse": ["esrecurse@4.3.0", "", { "dependencies": { "estraverse": "^5.2.0" } }, "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag=="], + + "estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="], + + "estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="], + + "esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="], + + "exponential-backoff": ["exponential-backoff@3.1.3", "", {}, "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA=="], "extsprintf": ["extsprintf@1.4.1", "", {}, "sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA=="], "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], + "fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="], + "fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="], - "fd-slicer": ["fd-slicer@1.1.0", "", { "dependencies": { "pend": "~1.2.0" } }, "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g=="], + "fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="], + + "fast-uri": ["fast-uri@3.1.4", "", {}, "sha512-8JnbkQ4juDyvYs4mgFGQqg4yCYtFDtUtmp2QIQq11ZZe5CFQ5wcqm1rqDgAh/QdMySuBnPzMUiJUNZG5N/AiQw=="], + + "fastq": ["fastq@1.20.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw=="], "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], + "fflate": ["fflate@0.8.3", "", {}, "sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA=="], + + "file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="], + "file-uri-to-path": ["file-uri-to-path@1.0.0", "", {}, "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw=="], "filelist": ["filelist@1.0.4", "", { "dependencies": { "minimatch": "^5.0.1" } }, "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q=="], + "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], + "find-up": ["find-up@4.1.0", "", { "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" } }, "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw=="], + "flat-cache": ["flat-cache@4.0.1", "", { "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.4" } }, "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw=="], + + "flatted": ["flatted@3.4.2", "", {}, "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA=="], + "foreground-child": ["foreground-child@3.3.1", "", { "dependencies": { "cross-spawn": "^7.0.6", "signal-exit": "^4.0.1" } }, "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw=="], "form-data": ["form-data@4.0.6", "", { "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" } }, "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ=="], - "framer-motion": ["framer-motion@12.42.0", "", { "dependencies": { "motion-dom": "^12.42.0", "motion-utils": "^12.39.0", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-wp7EJnfWaaEScVygKv3e20udoRz+LbtxScsuTkakAxfXmt+ReC6WyPW2nINRAGvd+hG9odwcjBLyOTPjH5pBRA=="], + "framer-motion": ["framer-motion@12.42.2", "", { "dependencies": { "motion-dom": "^12.42.2", "motion-utils": "^12.39.0", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-5XY9luDiu0oHfHBjpDthFMh0ES+122w6p/papSJBweMkO8Sn+PW2QaEgRblQBpWFnuvZS5qvarpt/hO2pjGmnw=="], "fs-constants": ["fs-constants@1.0.0", "", {}, "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow=="], @@ -601,6 +897,8 @@ "get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="], + "get-east-asian-width": ["get-east-asian-width@1.6.0", "", {}, "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA=="], + "get-intrinsic": ["get-intrinsic@1.3.0", "", { "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" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="], "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="], @@ -609,6 +907,8 @@ "glob": ["glob@7.2.3", "", { "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" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], + "glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], + "global-agent": ["global-agent@3.0.0", "", { "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" } }, "sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q=="], "globalthis": ["globalthis@1.0.4", "", { "dependencies": { "define-properties": "^1.2.1", "gopd": "^1.0.1" } }, "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ=="], @@ -631,6 +931,10 @@ "hasown": ["hasown@2.0.4", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A=="], + "hermes-estree": ["hermes-estree@0.25.1", "", {}, "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw=="], + + "hermes-parser": ["hermes-parser@0.25.1", "", { "dependencies": { "hermes-estree": "0.25.1" } }, "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA=="], + "hosted-git-info": ["hosted-git-info@4.1.0", "", { "dependencies": { "lru-cache": "^6.0.0" } }, "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA=="], "http-cache-semantics": ["http-cache-semantics@4.2.0", "", {}, "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ=="], @@ -649,6 +953,10 @@ "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], + "ignore": ["ignore@7.0.6", "", {}, "sha512-BAg6QkE8W+TuQLrrw0Ugr7HegXduRuuj8/ti2kSOc+jz1dmx8/WNcjr6XGnq5YpDWxFwwaavqD0+jIUOKelTsw=="], + + "import-in-the-middle": ["import-in-the-middle@3.3.1", "", { "dependencies": { "cjs-module-lexer": "^2.2.0", "es-module-lexer": "^2.2.0", "module-details-from-path": "^1.0.4" } }, "sha512-0rymlHSFLwZ0ixx8DaQkoIyZojJPY2a0K2nEYslhKJ6jIYO/m0IcCb7iQsFPmS7WmKwISZiIrv5Icstrw/CmqA=="], + "imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="], "indent-string": ["indent-string@4.0.0", "", {}, "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg=="], @@ -659,17 +967,23 @@ "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], - "ip-address": ["ip-address@10.1.0", "", {}, "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q=="], + "ip-address": ["ip-address@10.2.0", "", {}, "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA=="], "is-ci": ["is-ci@3.0.1", "", { "dependencies": { "ci-info": "^3.2.0" }, "bin": "bin.js" }, "sha512-ZYvCgrefwqoQ6yTyYUbQu64HsITZ3NfKX1lzaEYdkTDcfKzzCI/wthRRYKkdjHKFVgNiXKAKm65Zo1pk2as/QQ=="], + "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="], + "is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], - "is-interactive": ["is-interactive@1.0.0", "", {}, "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w=="], + "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="], + + "is-interactive": ["is-interactive@2.0.0", "", {}, "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ=="], "is-lambda": ["is-lambda@1.0.1", "", {}, "sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ=="], - "is-unicode-supported": ["is-unicode-supported@0.1.0", "", {}, "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw=="], + "is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="], + + "is-unicode-supported": ["is-unicode-supported@2.1.0", "", {}, "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ=="], "isarray": ["isarray@1.0.0", "", {}, "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ=="], @@ -677,26 +991,34 @@ "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], + "its-fine": ["its-fine@2.0.0", "", { "dependencies": { "@types/react-reconciler": "^0.28.9" }, "peerDependencies": { "react": "^19.0.0" } }, "sha512-KLViCmWx94zOvpLwSlsx6yOCeMhZYaxrJV87Po5k/FoZzcPSahvK5qJ7fYhS61sZi5ikmh2S3Hz55A2l3U69ng=="], + "jackspeak": ["jackspeak@4.2.3", "", { "dependencies": { "@isaacs/cliui": "^9.0.0" } }, "sha512-ykkVRwrYvFm1nb2AJfKKYPr0emF6IiXDYUaFx4Zn9ZuIH7MrzEZ3sD5RlqGXNRpHtvUHJyOnCEFxOlNDtGo7wg=="], "jake": ["jake@10.9.4", "", { "dependencies": { "async": "^3.2.6", "filelist": "^1.0.4", "picocolors": "^1.1.1" }, "bin": "bin/cli.js" }, "sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA=="], - "jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="], + "jiti": ["jiti@2.7.0", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ=="], "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], - "js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": "bin/js-yaml.js" }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="], + "js-yaml": ["js-yaml@4.3.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q=="], "jsesc": ["jsesc@3.1.0", "", { "bin": "bin/jsesc" }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="], "json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="], - "json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], + "json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], + + "json-schema-typed": ["json-schema-typed@8.0.2", "", {}, "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA=="], + + "json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="], "json-stringify-safe": ["json-stringify-safe@5.0.1", "", {}, "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA=="], "json5": ["json5@2.2.3", "", { "bin": "lib/cli.js" }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="], + "jsonc-parser": ["jsonc-parser@3.3.1", "", {}, "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ=="], + "jsonfile": ["jsonfile@6.2.0", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg=="], "keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="], @@ -707,9 +1029,11 @@ "lazystream": ["lazystream@1.0.1", "", { "dependencies": { "readable-stream": "^2.0.5" } }, "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw=="], + "levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="], + "locate-path": ["locate-path@5.0.0", "", { "dependencies": { "p-locate": "^4.1.0" } }, "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g=="], - "lodash": ["lodash@4.17.23", "", {}, "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w=="], + "lodash": ["lodash@4.18.1", "", {}, "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q=="], "lodash.defaults": ["lodash.defaults@4.2.0", "", {}, "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ=="], @@ -725,22 +1049,32 @@ "lodash.union": ["lodash.union@4.6.0", "", {}, "sha512-c4pB2CdGrGdjMKYLA+XiRDO7Y0PRQbm/Gzg8qMj+QH+pFVAoTp5sBpO0odL3FjoPCGjK96p6qsP+yQoiLoOBcw=="], - "log-symbols": ["log-symbols@4.1.0", "", { "dependencies": { "chalk": "^4.1.0", "is-unicode-supported": "^0.1.0" } }, "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg=="], + "log-symbols": ["log-symbols@7.0.1", "", { "dependencies": { "is-unicode-supported": "^2.0.0", "yoctocolors": "^2.1.1" } }, "sha512-ja1E3yCr9i/0hmBVaM0bfwDjnGy8I/s6PP4DFp+yP+a+mrHO4Rm7DtmnqROTUkHIkqffC84YY7AeqX6oFk0WFg=="], "lowercase-keys": ["lowercase-keys@2.0.0", "", {}, "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA=="], "lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], - "lucide-react": ["lucide-react@1.17.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-9FA9evdox/JQL5PT57fdA1x/yg8T7knJ98+zjTL3UfKza6pflQUUh3XtaQIHKvnsJw1lmsEyHVlt5jchYxOQ5w=="], + "lucide-react": ["lucide-react@1.25.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-/mdJTRbiwcLOQ1NZZK1amZF9rIZyvO18D6r9TngE6TG1NmqHgFuT4eE7Xrkm9UsXMbBJD1NlfwHVltCDWHrOTw=="], "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], - "make-fetch-happen": ["make-fetch-happen@14.0.3", "", { "dependencies": { "@npmcli/agent": "^3.0.0", "cacache": "^19.0.1", "http-cache-semantics": "^4.1.1", "minipass": "^7.0.2", "minipass-fetch": "^4.0.0", "minipass-flush": "^1.0.5", "minipass-pipeline": "^1.2.4", "negotiator": "^1.0.0", "proc-log": "^5.0.0", "promise-retry": "^2.0.1", "ssri": "^12.0.0" } }, "sha512-QMjGbFTP0blj97EeidG5hk/QhKQ3T4ICckQGLgz38QF7Vgbk6e6FTARN8KhKxyBbWn8R0HU+bnw8aSoFPD4qtQ=="], + "magicast": ["magicast@0.5.3", "", { "dependencies": { "@babel/parser": "^7.29.3", "@babel/types": "^7.29.0", "source-map-js": "^1.2.1" } }, "sha512-pVKE4UdSQ7DvHzivsCIFx2BJn1mHG6KsyrFcaxFx6tONdneEuThrDx0Cj3AMg58KyN4pzYT+LHOotxDQDjNvkw=="], + + "make-fetch-happen": ["make-fetch-happen@10.2.1", "", { "dependencies": { "agentkeepalive": "^4.2.1", "cacache": "^16.1.0", "http-cache-semantics": "^4.1.0", "http-proxy-agent": "^5.0.0", "https-proxy-agent": "^5.0.0", "is-lambda": "^1.0.1", "lru-cache": "^7.7.1", "minipass": "^3.1.6", "minipass-collect": "^1.0.2", "minipass-fetch": "^2.0.3", "minipass-flush": "^1.0.5", "minipass-pipeline": "^1.2.4", "negotiator": "^0.6.3", "promise-retry": "^2.0.1", "socks-proxy-agent": "^7.0.0", "ssri": "^9.0.0" } }, "sha512-NgOPbRiaQM10DYXvN3/hhGVI2M5MtITFryzBGxHM5p4wnFxsVCbxkrBrDsk+EZ5OB4jEOT7AjDxtdF+KVEFT7w=="], "matcher": ["matcher@3.0.0", "", { "dependencies": { "escape-string-regexp": "^4.0.0" } }, "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng=="], "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], + "merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="], + + "meriyah": ["meriyah@6.1.4", "", {}, "sha512-Sz8FzjzI0kN13GK/6MVEsVzMZEPvOhnmmI1lU5+/1cGOiK3QUahntrNNtdVeihrO7t9JpoH75iMNXg6R6uWflQ=="], + + "meshoptimizer": ["meshoptimizer@1.1.1", "", {}, "sha512-oRFNWJRDA/WTrVj7NWvqa5HqE1t9MYDj2VaWirQCzCCrAd2GHrqR/sQezCxiWATPNlKTcRaPRHPJwIRoPBAp5g=="], + + "micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="], + "mime": ["mime@2.6.0", "", { "bin": "cli.js" }, "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg=="], "mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], @@ -749,17 +1083,19 @@ "mimic-fn": ["mimic-fn@2.1.0", "", {}, "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="], + "mimic-function": ["mimic-function@5.0.1", "", {}, "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA=="], + "mimic-response": ["mimic-response@3.1.0", "", {}, "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ=="], - "minimatch": ["minimatch@10.2.0", "", { "dependencies": { "brace-expansion": "^5.0.2" } }, "sha512-ugkC31VaVg9cF0DFVoADH12k6061zNZkZON+aX8AWsR9GhPcErkcMBceb6znR8wLERM2AkkOxy2nWRLpT9Jq5w=="], + "minimatch": ["minimatch@10.2.5", "", { "dependencies": { "brace-expansion": "^5.0.5" } }, "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg=="], "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], "minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="], - "minipass-collect": ["minipass-collect@2.0.1", "", { "dependencies": { "minipass": "^7.0.3" } }, "sha512-D7V8PO9oaz7PWGLbCACuI1qEOsq7UKfLotx/C0Aet43fCUB/wfQ7DYeq2oR/svFJGYDHPr38SHATeaj/ZoKHKw=="], + "minipass-collect": ["minipass-collect@1.0.2", "", { "dependencies": { "minipass": "^3.0.0" } }, "sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA=="], - "minipass-fetch": ["minipass-fetch@4.0.1", "", { "dependencies": { "minipass": "^7.0.3", "minipass-sized": "^1.0.3", "minizlib": "^3.0.1" }, "optionalDependencies": { "encoding": "^0.1.13" } }, "sha512-j7U11C5HXigVuutxebFadoYBbd7VSdZWggSe64NVdvWNBqGAiXPL2QVCehjmw7lY1oF9gOllYbORh+hiNgfPgQ=="], + "minipass-fetch": ["minipass-fetch@2.1.2", "", { "dependencies": { "minipass": "^3.1.6", "minipass-sized": "^1.0.3", "minizlib": "^2.1.2" }, "optionalDependencies": { "encoding": "^0.1.13" } }, "sha512-LT49Zi2/WMROHYoqGgdlQIZh8mLPZmOrN2NdJjMXxYe4nkN6FUyuPuOAOedNJDrx0IRGg9+4guZewtp8hE6TxA=="], "minipass-flush": ["minipass-flush@1.0.5", "", { "dependencies": { "minipass": "^3.0.0" } }, "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw=="], @@ -771,17 +1107,21 @@ "mkdirp": ["mkdirp@1.0.4", "", { "bin": "bin/cmd.js" }, "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw=="], - "motion": ["motion@12.42.0", "", { "dependencies": { "framer-motion": "^12.42.0", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-Qhwvu9sVl5/URSq5CNzwMCpSKK8Uhnrwb6VO977kZyj/wOCS7mWebJUnBoHx5cZU1Zv8a9BD5CSICWKAlrLJgA=="], + "module-details-from-path": ["module-details-from-path@1.0.4", "", {}, "sha512-EGWKgxALGMgzvxYF1UyGTy0HXX/2vHLkw6+NvDKW2jypWbHpjQuj4UMcqQWXHERJhVGKikolT06G3bcKe4fi7w=="], + + "motion": ["motion@12.42.2", "", { "dependencies": { "framer-motion": "^12.42.2", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-Atvv11yUKIid41cVrRBDVX5m8tF8kNpExRSlbpt6APClhDjtwQssgFHhQzejxw7/7YYbjHSPKBVbHo05BuJT5Q=="], - "motion-dom": ["motion-dom@12.42.0", "", { "dependencies": { "motion-utils": "^12.39.0" } }, "sha512-M63h4n8R+quJdNhBwuLlgxM+OLYa9+I/T2pzDRboB9fLXRdbou+Gw7Zury+SkpaCyACP1JHSjHgZ1EgTkBr30w=="], + "motion-dom": ["motion-dom@12.42.2", "", { "dependencies": { "motion-utils": "^12.39.0" } }, "sha512-5gIMWLp/PycBtJRJWRgjxke5n8dlvkSn2DrYW+tr3XcqAZY1xZh6BJyooJXCM8wdfM7wfMjkBJNLge1CKPUIRA=="], "motion-utils": ["motion-utils@12.39.0", "", {}, "sha512-8nadJAJjTtqRkmRF36FoJTrywK9nnFmnPwnSMyxaOCU7GDjN9RTMJIxx9De8ErM+vpPhMccr/6fo5WciyQLnMQ=="], "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], - "nanoid": ["nanoid@3.3.11", "", { "bin": "bin/nanoid.cjs" }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], + "nanoid": ["nanoid@3.3.16", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q=="], - "negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="], + "natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="], + + "negotiator": ["negotiator@0.6.4", "", {}, "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w=="], "node-abi": ["node-abi@4.28.0", "", { "dependencies": { "semver": "^7.6.3" } }, "sha512-Qfp5XZL1cJDOabOT8H5gnqMTmM4NjvYzHp4I/Kt/Sl76OVkOBBHRFlPspGV0hYvMoqQsypFjT/Yp7Km0beXW9g=="], @@ -791,11 +1131,13 @@ "node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="], - "node-gyp": ["node-gyp@11.5.0", "", { "dependencies": { "env-paths": "^2.2.0", "exponential-backoff": "^3.1.1", "graceful-fs": "^4.2.6", "make-fetch-happen": "^14.0.3", "nopt": "^8.0.0", "proc-log": "^5.0.0", "semver": "^7.3.5", "tar": "^7.4.3", "tinyglobby": "^0.2.12", "which": "^5.0.0" }, "bin": { "node-gyp": "bin/node-gyp.js" } }, "sha512-ra7Kvlhxn5V9Slyus0ygMa2h+UqExPqUIkfk7Pc8QTLT956JLSy51uWFwHtIYy0vI8cB4BDhc/S03+880My/LQ=="], + "node-gyp": ["node-gyp@12.4.0", "", { "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" } }, "sha512-OMcPNvqTCFUnNaBlmdgq+lfNqY7gTiSmNRDjY3uAXRyudeKZEZxu3CLtjMQrx4zZxCX2b/mpNqTtwuCJgXhHkw=="], + + "node-int64": ["node-int64@0.4.0", "", {}, "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw=="], "node-releases": ["node-releases@2.0.27", "", {}, "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="], - "nopt": ["nopt@8.1.0", "", { "dependencies": { "abbrev": "^3.0.0" }, "bin": { "nopt": "bin/nopt.js" } }, "sha512-ieGu42u/Qsa4TFktmaKEwM6MQH0pOWnaB3htzh0JRtx84+Mebc0cbZYN5bC+6WTZ4+77xrL9Pn5m7CV6VIkV7A=="], + "nopt": ["nopt@9.0.0", "", { "dependencies": { "abbrev": "^4.0.0" }, "bin": { "nopt": "bin/nopt.js" } }, "sha512-Zhq3a+yFKrYwSBluL4H9XP3m3y5uvQkB/09CwDruCiRmR/UJYnn9W4R48ry0uGC70aeTPKLynBtscP9efFFcPw=="], "normalize-path": ["normalize-path@3.0.0", "", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="], @@ -807,11 +1149,19 @@ "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], - "onetime": ["onetime@5.1.2", "", { "dependencies": { "mimic-fn": "^2.1.0" } }, "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg=="], + "onetime": ["onetime@7.0.0", "", { "dependencies": { "mimic-function": "^5.0.0" } }, "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ=="], - "ora": ["ora@5.4.1", "", { "dependencies": { "bl": "^4.1.0", "chalk": "^4.1.0", "cli-cursor": "^3.1.0", "cli-spinners": "^2.5.0", "is-interactive": "^1.0.0", "is-unicode-supported": "^0.1.0", "log-symbols": "^4.1.0", "strip-ansi": "^6.0.0", "wcwidth": "^1.0.1" } }, "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ=="], + "optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="], - "oxlint": ["oxlint@1.67.0", "", { "optionalDependencies": { "@oxlint/binding-android-arm-eabi": "1.67.0", "@oxlint/binding-android-arm64": "1.67.0", "@oxlint/binding-darwin-arm64": "1.67.0", "@oxlint/binding-darwin-x64": "1.67.0", "@oxlint/binding-freebsd-x64": "1.67.0", "@oxlint/binding-linux-arm-gnueabihf": "1.67.0", "@oxlint/binding-linux-arm-musleabihf": "1.67.0", "@oxlint/binding-linux-arm64-gnu": "1.67.0", "@oxlint/binding-linux-arm64-musl": "1.67.0", "@oxlint/binding-linux-ppc64-gnu": "1.67.0", "@oxlint/binding-linux-riscv64-gnu": "1.67.0", "@oxlint/binding-linux-riscv64-musl": "1.67.0", "@oxlint/binding-linux-s390x-gnu": "1.67.0", "@oxlint/binding-linux-x64-gnu": "1.67.0", "@oxlint/binding-linux-x64-musl": "1.67.0", "@oxlint/binding-openharmony-arm64": "1.67.0", "@oxlint/binding-win32-arm64-msvc": "1.67.0", "@oxlint/binding-win32-ia32-msvc": "1.67.0", "@oxlint/binding-win32-x64-msvc": "1.67.0" }, "peerDependencies": { "oxlint-tsgolint": ">=0.22.1", "vite-plus": "*" }, "optionalPeers": ["oxlint-tsgolint", "vite-plus"], "bin": { "oxlint": "bin/oxlint" } }, "sha512-blwwaHPdoH8piQ5/z0KHeoHFR7FZgl12WluKJfu4qFLPkZl6mK04PkLE45Fw1NxfBRSlh40Gu7MkxHUw++ociQ=="], + "ora": ["ora@9.4.1", "", { "dependencies": { "chalk": "^5.6.2", "cli-cursor": "^5.0.0", "cli-spinners": "^3.2.0", "is-interactive": "^2.0.0", "is-unicode-supported": "^2.1.0", "log-symbols": "^7.0.1", "stdin-discarder": "^0.3.2", "string-width": "^8.1.0" } }, "sha512-6VlU9MLXbjVQD04AZCMX28hVtA5bUoadvUqO76MUCVA0ilwJbMiHsITRPfyVm6p/BC0Av/BXMujx39WCe1LEqw=="], + + "oxc-parser": ["oxc-parser@0.138.0", "", { "dependencies": { "@oxc-project/types": "^0.138.0" }, "optionalDependencies": { "@oxc-parser/binding-android-arm-eabi": "0.138.0", "@oxc-parser/binding-android-arm64": "0.138.0", "@oxc-parser/binding-darwin-arm64": "0.138.0", "@oxc-parser/binding-darwin-x64": "0.138.0", "@oxc-parser/binding-freebsd-x64": "0.138.0", "@oxc-parser/binding-linux-arm-gnueabihf": "0.138.0", "@oxc-parser/binding-linux-arm-musleabihf": "0.138.0", "@oxc-parser/binding-linux-arm64-gnu": "0.138.0", "@oxc-parser/binding-linux-arm64-musl": "0.138.0", "@oxc-parser/binding-linux-ppc64-gnu": "0.138.0", "@oxc-parser/binding-linux-riscv64-gnu": "0.138.0", "@oxc-parser/binding-linux-riscv64-musl": "0.138.0", "@oxc-parser/binding-linux-s390x-gnu": "0.138.0", "@oxc-parser/binding-linux-x64-gnu": "0.138.0", "@oxc-parser/binding-linux-x64-musl": "0.138.0", "@oxc-parser/binding-openharmony-arm64": "0.138.0", "@oxc-parser/binding-wasm32-wasi": "0.138.0", "@oxc-parser/binding-win32-arm64-msvc": "0.138.0", "@oxc-parser/binding-win32-ia32-msvc": "0.138.0", "@oxc-parser/binding-win32-x64-msvc": "0.138.0" } }, "sha512-c25lvfpZ2+WY1yk6NkP0X0RTQg0ZxgSVaZHDa7lt6fEe1jwZjPWkRWvTyZ1xyaM7roVJMdtRCfbhUj/d4ims3Q=="], + + "oxc-resolver": ["oxc-resolver@11.24.2", "", { "optionalDependencies": { "@oxc-resolver/binding-android-arm-eabi": "11.24.2", "@oxc-resolver/binding-android-arm64": "11.24.2", "@oxc-resolver/binding-darwin-arm64": "11.24.2", "@oxc-resolver/binding-darwin-x64": "11.24.2", "@oxc-resolver/binding-freebsd-x64": "11.24.2", "@oxc-resolver/binding-linux-arm-gnueabihf": "11.24.2", "@oxc-resolver/binding-linux-arm-musleabihf": "11.24.2", "@oxc-resolver/binding-linux-arm64-gnu": "11.24.2", "@oxc-resolver/binding-linux-arm64-musl": "11.24.2", "@oxc-resolver/binding-linux-ppc64-gnu": "11.24.2", "@oxc-resolver/binding-linux-riscv64-gnu": "11.24.2", "@oxc-resolver/binding-linux-riscv64-musl": "11.24.2", "@oxc-resolver/binding-linux-s390x-gnu": "11.24.2", "@oxc-resolver/binding-linux-x64-gnu": "11.24.2", "@oxc-resolver/binding-linux-x64-musl": "11.24.2", "@oxc-resolver/binding-openharmony-arm64": "11.24.2", "@oxc-resolver/binding-wasm32-wasi": "11.24.2", "@oxc-resolver/binding-win32-arm64-msvc": "11.24.2", "@oxc-resolver/binding-win32-x64-msvc": "11.24.2" } }, "sha512-FY91FiDBj7ls5MsFS9jN3tjz2o0/zsdSsymlakySaBwVJZorHhkWyICLZMKxlu1R9vYo+sd3z1jwb4J8x7bNDw=="], + + "oxlint": ["oxlint@1.74.0", "", { "optionalDependencies": { "@oxlint/binding-android-arm-eabi": "1.74.0", "@oxlint/binding-android-arm64": "1.74.0", "@oxlint/binding-darwin-arm64": "1.74.0", "@oxlint/binding-darwin-x64": "1.74.0", "@oxlint/binding-freebsd-x64": "1.74.0", "@oxlint/binding-linux-arm-gnueabihf": "1.74.0", "@oxlint/binding-linux-arm-musleabihf": "1.74.0", "@oxlint/binding-linux-arm64-gnu": "1.74.0", "@oxlint/binding-linux-arm64-musl": "1.74.0", "@oxlint/binding-linux-ppc64-gnu": "1.74.0", "@oxlint/binding-linux-riscv64-gnu": "1.74.0", "@oxlint/binding-linux-riscv64-musl": "1.74.0", "@oxlint/binding-linux-s390x-gnu": "1.74.0", "@oxlint/binding-linux-x64-gnu": "1.74.0", "@oxlint/binding-linux-x64-musl": "1.74.0", "@oxlint/binding-openharmony-arm64": "1.74.0", "@oxlint/binding-win32-arm64-msvc": "1.74.0", "@oxlint/binding-win32-ia32-msvc": "1.74.0", "@oxlint/binding-win32-x64-msvc": "1.74.0" }, "peerDependencies": { "oxlint-tsgolint": ">=0.24.0", "vite-plus": "*" }, "optionalPeers": ["oxlint-tsgolint", "vite-plus"], "bin": { "oxlint": "bin/oxlint" } }, "sha512-odGl2s2x5IOJoj3A0v1k0PGBXVFBZeZ2+AK/+K2MJur7Ghi3bkyX5NuLUWHKqa4js1wjep3hJeuTQJOlr+4+dA=="], + + "oxlint-plugin-react-doctor": ["oxlint-plugin-react-doctor@0.8.1", "", { "dependencies": { "@typescript-eslint/types": "^8.59.3", "eslint-scope": "^9.1.2", "eslint-visitor-keys": "^5.0.1", "oxc-parser": "^0.138.0" } }, "sha512-ETjgoKrY0EYpK51BsbvgpWySkWaLgJ6z7yB/BfOosi+TUY4+GiDmhWfjJCeul+tpDJEFKR5MnpENjlUrmtZ5Dw=="], "p-cancelable": ["p-cancelable@2.1.1", "", {}, "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg=="], @@ -819,12 +1169,14 @@ "p-locate": ["p-locate@4.1.0", "", { "dependencies": { "p-limit": "^2.2.0" } }, "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A=="], - "p-map": ["p-map@7.0.4", "", {}, "sha512-tkAQEw8ysMzmkhgw8k+1U/iPhWNhykKnSk4Rd5zLoPJCuJaGRPo6YposrZgaxHKzDHdDWWZvE/Sk7hsL2X/CpQ=="], + "p-map": ["p-map@4.0.0", "", { "dependencies": { "aggregate-error": "^3.0.0" } }, "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ=="], "p-try": ["p-try@2.2.0", "", {}, "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ=="], "package-json-from-dist": ["package-json-from-dist@1.0.1", "", {}, "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw=="], + "package-manager-detector": ["package-manager-detector@1.7.0", "", {}, "sha512-xg1eHpwYL/D/HEdWw2goFZP6vV0FH7W+PZ5rFkGjdIDLtxq7EkzBUeT3m+lndYCt8wKbmofUu1MUdMCXkCk9ZQ=="], + "path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="], "path-is-absolute": ["path-is-absolute@1.0.1", "", {}, "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg=="], @@ -835,21 +1187,27 @@ "pe-library": ["pe-library@0.4.1", "", {}, "sha512-eRWB5LBz7PpDu4PUlwT0PhnQfTQJlDDdPa35urV4Osrm0t0AqQFGn+UIkU3klZvwJ8KPO3VbBFsXquA6p6kqZw=="], - "pend": ["pend@1.2.0", "", {}, "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg=="], - "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], "picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="], + "pkijs": ["pkijs@3.4.0", "", { "dependencies": { "@noble/hashes": "1.4.0", "asn1js": "^3.0.6", "bytestreamjs": "^2.0.1", "pvtsutils": "^1.3.6", "pvutils": "^1.1.3", "tslib": "^2.8.1" } }, "sha512-emEcLuomt2j03vxD54giVB4SxTjnsqkU692xZOZXHDVoYyypEm+b3jpiTcc+Cf+myooc+/Ly0z01jqeNHVgJGw=="], + "plist": ["plist@3.1.0", "", { "dependencies": { "@xmldom/xmldom": "^0.8.8", "base64-js": "^1.5.1", "xmlbuilder": "^15.1.1" } }, "sha512-uysumyrvkUX0rX/dEVqt8gC3sTBzd4zoWfLeS29nb53imdaXVvLINYXTI2GNqzaMuvacNx4uJQ8+b3zXR0pkgQ=="], "pngjs": ["pngjs@5.0.0", "", {}, "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw=="], - "postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="], + "postcss": ["postcss@8.5.20", "", { "dependencies": { "nanoid": "^3.3.16", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-lW616l85ucIQL+FocMmL7pQFPqBmwejrCMg+iPxyImlrANNJG9NHq/RkyCZopDhd8C3LA03PHRJDjkbGu8vvug=="], + + "posthog-js": ["posthog-js@1.406.2", "", { "dependencies": { "@posthog/browser-common": "^0.2.0", "@posthog/core": "^1.44.0", "@posthog/types": "^1.397.1", "core-js": "^3.49.0", "dompurify": "^3.3.2", "fflate": "^0.4.8", "preact": "^10.29.3", "query-selector-shadow-dom": "^1.0.1", "web-vitals": "^5.3.0" } }, "sha512-HNJO6Llro79s3x/ScxGY7i+Tf/o+ctJkOc79vrnS6R1I3TzucfzfwgAip1JVnQHnkBPV4JE8hI6nUXMJdU113Q=="], + + "posthog-node": ["posthog-node@5.46.0", "", { "dependencies": { "@posthog/core": "^1.44.0" }, "peerDependencies": { "rxjs": "^7.0.0" }, "optionalPeers": ["rxjs"] }, "sha512-Uzkth327Qxho9X55UygGUjVKCF9oaox90HQpa0o9YNjwLjbQmXttgHChzAtjAcsMw/ZKr3NnHC3xcAaS5dXwEQ=="], "preact": ["preact@10.29.1", "", {}, "sha512-gQCLc/vWroE8lIpleXtdJhTFDogTdZG9AjMUpVkDf2iTCNwYNWA+u16dL41TqUDJO4gm2IgrcMv3uTpjd4Pwmg=="], - "proc-log": ["proc-log@5.0.0", "", {}, "sha512-Azwzvl90HaF0aCz1JrDdXQykFakSSNPaPoiZ9fm5qJIMHioDZEi7OAdRwSm6rSoPtY3Qutnm3L7ogmg3dc+wbQ=="], + "prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="], + + "proc-log": ["proc-log@6.1.0", "", {}, "sha512-iG+GYldRf2BQ0UDUAd6JQ/RwzaQy6mXmsk/IzlYyal4A4SNFw54MeH4/tLkF4I5WoWG9SQwuqWzS99jaFQHBuQ=="], "process-nextick-args": ["process-nextick-args@2.0.1", "", {}, "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="], @@ -867,21 +1225,35 @@ "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], + "pvtsutils": ["pvtsutils@1.3.6", "", { "dependencies": { "tslib": "^2.8.1" } }, "sha512-PLgQXQ6H2FWCaeRak8vvk1GW462lMxB5s3Jm673N82zI4vqtVUPuZdffdZbPDFRoU8kAhItWFtPCWiPpp4/EDg=="], + + "pvutils": ["pvutils@1.1.5", "", {}, "sha512-KTqnxsgGiQ6ZAzZCVlJH5eOjSnvlyEgx1m8bkRJfOhmGRqfo5KLvmAlACQkrjEtOQ4B7wF9TdSLIs9O90MX9xA=="], + "qrcode": ["qrcode@1.5.4", "", { "dependencies": { "dijkstrajs": "^1.0.1", "pngjs": "^5.0.0", "yargs": "^15.3.1" }, "bin": { "qrcode": "bin/qrcode" } }, "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg=="], + "query-selector-shadow-dom": ["query-selector-shadow-dom@1.0.1", "", {}, "sha512-lT5yCqEBgfoMYpf3F2xQRK7zEr1rhIIZuceDK6+xRkJQ4NMbHTwXqk4NkwDwQMNqXgG9r9fyHnzwNVs6zV5KRw=="], + + "queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="], + "quick-lru": ["quick-lru@5.1.1", "", {}, "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA=="], - "react": ["react@19.2.6", "", {}, "sha512-sfWGGfavi0xr8Pg0sVsyHMAOziVYKgPLNrS7ig+ivMNb3wbCBw3KxtflsGBAwD3gYQlE/AEZsTLgToRrSCjb0Q=="], + "react": ["react@19.2.7", "", {}, "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ=="], + + "react-doctor": ["react-doctor@0.8.1", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@sentry/node": "^10.54.0", "agent-install": "0.0.5", "conf": "^15.1.0", "confbox": "^0.2.4", "deslop-js": "0.8.1", "eslint-plugin-react-hooks": "^7.1.1", "jiti": "^2.7.0", "magicast": "^0.5.3", "oxlint": ">=1.66.0 <1.67.0", "oxlint-plugin-react-doctor": "0.8.1", "prompts": "^2.4.2", "typescript": ">=5.0.4 <6", "vscode-languageserver": "^9.0.1", "vscode-languageserver-textdocument": "^1.0.12", "vscode-uri": "^3.1.0", "yaml": "^2.9.0" }, "bin": { "react-doctor": "bin/react-doctor.js" } }, "sha512-lP/MaS7dDMeVFpWBjh8qYp6NYcb/1TmsAwSixqkqOI50fea9kfh89fyn+UUAC3vb53FGIqwvDPZIjlJR6BgGAQ=="], + + "react-dom": ["react-dom@19.2.7", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.7" } }, "sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ=="], - "react-dom": ["react-dom@19.2.6", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.6" } }, "sha512-0prMI+hvBbPjsWnxDLxlCGyM8PN6UuWjEUCYmZhO67xIV9Xasa/r/vDnq+Xyq4Lo27g8QSbO5YzARu0D1Sps3g=="], + "react-grab": ["react-grab@0.1.48", "", { "dependencies": { "@react-grab/cli": "0.1.48", "bippy": "^0.5.43" }, "peerDependencies": { "react": ">=17.0.0" }, "optionalPeers": ["react"], "bin": { "react-grab": "bin/cli.js" } }, "sha512-p3WnmK9LLvXE/c4ITPLlXcP1fkXo2VFEQqK94tIfcHIWKNdqdhYYFyNVioO50HR+uyHIwT63Z4txZDqJlVcD/Q=="], "react-refresh": ["react-refresh@0.18.0", "", {}, "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw=="], - "react-scan": ["react-scan@0.5.3", "", { "dependencies": { "@babel/core": "^7.26.0", "@babel/generator": "^7.26.2", "@babel/types": "^7.26.0", "@preact/signals": "^1.3.1", "@rollup/pluginutils": "^5.1.3", "@types/node": "^20.17.9", "bippy": "^0.5.30", "commander": "^14.0.0", "esbuild": "^0.25.0", "estree-walker": "^3.0.3", "picocolors": "^1.1.1", "preact": "^10.25.1", "prompts": "^2.4.2" }, "optionalDependencies": { "unplugin": "2.1.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" }, "bin": { "react-scan": "bin/cli.js" } }, "sha512-qde9PupmUf0L3MU1H6bjmoukZNbCXdMyTEwP4Gh8RQ4rZPd2GGNBgEKWszwLm96E8k+sGtMpc0B9P0KyFDP6Bw=="], + "react-scan": ["react-scan@0.5.7", "", { "dependencies": { "@babel/core": "^7.29.0", "@babel/types": "^7.29.0", "@preact/signals": "^2.9.0", "@rollup/pluginutils": "^5.3.0", "bippy": "^0.5.39", "commander": "^14.0.0", "picocolors": "^1.1.1", "preact": "^10.29.1", "prompts": "^2.4.2", "react-doctor": "latest", "react-grab": "latest" }, "optionalDependencies": { "unplugin": "^3.0.0" }, "peerDependencies": { "esbuild": ">=0.18.0", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["esbuild"], "bin": { "react-scan": "bin/cli.js" } }, "sha512-KRlq734yN6q/f2CZmZi9CWHuiqSzoLhPFLtcJOL6XM4lR54myyFcY81pG9QOwj+eBC1hIHm5n+Ntbtqiilu8Rg=="], + + "react-use-measure": ["react-use-measure@2.1.7", "", { "peerDependencies": { "react": ">=16.13", "react-dom": ">=16.13" }, "optionalPeers": ["react-dom"] }, "sha512-KrvcAo13I/60HpwGO5jpW7E9DfusKyLPLvuHlUyP5zqnmAPhNc6qTRjUQrdTADl0lpPpDVU2/Gg51UlOGHXbdg=="], "read-binary-file-arch": ["read-binary-file-arch@1.0.6", "", { "dependencies": { "debug": "^4.3.4" }, "bin": "cli.js" }, "sha512-BNg9EN3DD3GsDXX7Aa8O4p92sryjkmzYYgmgTAc6CA4uGLEDzFfxOxugu21akOxpcXHiEgsYkC6nPsQvLLLmEg=="], - "readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], + "readable-stream": ["readable-stream@2.3.8", "", { "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" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="], "readdir-glob": ["readdir-glob@1.1.3", "", { "dependencies": { "minimatch": "^5.1.0" } }, "sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA=="], @@ -889,6 +1261,10 @@ "require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="], + "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="], + + "require-in-the-middle": ["require-in-the-middle@8.0.1", "", { "dependencies": { "debug": "^4.3.5", "module-details-from-path": "^1.0.3" } }, "sha512-QT7FVMXfWOYFbeRBF6nu+I6tr2Tf3u0q8RIEjNob/heKY/nh7drD/k7eeMFmSQgnTtCzLDcCu/XEnpW2wk4xCQ=="], + "require-main-filename": ["require-main-filename@2.0.0", "", {}, "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg=="], "resedit": ["resedit@1.7.2", "", { "dependencies": { "pe-library": "^0.4.1" } }, "sha512-vHjcY2MlAITJhC0eRD/Vv8Vlgmu9Sd3LX9zZvtGzU5ZImdTN3+d6e/4mnTyV8vEbyf1sgNIrWxhWlrys52OkEA=="], @@ -897,15 +1273,19 @@ "responselike": ["responselike@2.0.1", "", { "dependencies": { "lowercase-keys": "^2.0.0" } }, "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw=="], - "restore-cursor": ["restore-cursor@3.1.0", "", { "dependencies": { "onetime": "^5.1.0", "signal-exit": "^3.0.2" } }, "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA=="], + "restore-cursor": ["restore-cursor@5.1.0", "", { "dependencies": { "onetime": "^7.0.0", "signal-exit": "^4.1.0" } }, "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA=="], "retry": ["retry@0.12.0", "", {}, "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow=="], + "reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="], + "rimraf": ["rimraf@3.0.2", "", { "dependencies": { "glob": "^7.1.3" }, "bin": "bin.js" }, "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA=="], "roarr": ["roarr@2.15.4", "", { "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" } }, "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A=="], - "rollup": ["rollup@4.57.1", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.57.1", "@rollup/rollup-android-arm64": "4.57.1", "@rollup/rollup-darwin-arm64": "4.57.1", "@rollup/rollup-darwin-x64": "4.57.1", "@rollup/rollup-freebsd-arm64": "4.57.1", "@rollup/rollup-freebsd-x64": "4.57.1", "@rollup/rollup-linux-arm-gnueabihf": "4.57.1", "@rollup/rollup-linux-arm-musleabihf": "4.57.1", "@rollup/rollup-linux-arm64-gnu": "4.57.1", "@rollup/rollup-linux-arm64-musl": "4.57.1", "@rollup/rollup-linux-loong64-gnu": "4.57.1", "@rollup/rollup-linux-loong64-musl": "4.57.1", "@rollup/rollup-linux-ppc64-gnu": "4.57.1", "@rollup/rollup-linux-ppc64-musl": "4.57.1", "@rollup/rollup-linux-riscv64-gnu": "4.57.1", "@rollup/rollup-linux-riscv64-musl": "4.57.1", "@rollup/rollup-linux-s390x-gnu": "4.57.1", "@rollup/rollup-linux-x64-gnu": "4.57.1", "@rollup/rollup-linux-x64-musl": "4.57.1", "@rollup/rollup-openbsd-x64": "4.57.1", "@rollup/rollup-openharmony-arm64": "4.57.1", "@rollup/rollup-win32-arm64-msvc": "4.57.1", "@rollup/rollup-win32-ia32-msvc": "4.57.1", "@rollup/rollup-win32-x64-gnu": "4.57.1", "@rollup/rollup-win32-x64-msvc": "4.57.1", "fsevents": "~2.3.2" }, "bin": "dist/bin/rollup" }, "sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A=="], + "rollup": ["rollup@4.62.2", "", { "dependencies": { "@types/estree": "1.0.9" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.62.2", "@rollup/rollup-android-arm64": "4.62.2", "@rollup/rollup-darwin-arm64": "4.62.2", "@rollup/rollup-darwin-x64": "4.62.2", "@rollup/rollup-freebsd-arm64": "4.62.2", "@rollup/rollup-freebsd-x64": "4.62.2", "@rollup/rollup-linux-arm-gnueabihf": "4.62.2", "@rollup/rollup-linux-arm-musleabihf": "4.62.2", "@rollup/rollup-linux-arm64-gnu": "4.62.2", "@rollup/rollup-linux-arm64-musl": "4.62.2", "@rollup/rollup-linux-loong64-gnu": "4.62.2", "@rollup/rollup-linux-loong64-musl": "4.62.2", "@rollup/rollup-linux-ppc64-gnu": "4.62.2", "@rollup/rollup-linux-ppc64-musl": "4.62.2", "@rollup/rollup-linux-riscv64-gnu": "4.62.2", "@rollup/rollup-linux-riscv64-musl": "4.62.2", "@rollup/rollup-linux-s390x-gnu": "4.62.2", "@rollup/rollup-linux-x64-gnu": "4.62.2", "@rollup/rollup-linux-x64-musl": "4.62.2", "@rollup/rollup-openbsd-x64": "4.62.2", "@rollup/rollup-openharmony-arm64": "4.62.2", "@rollup/rollup-win32-arm64-msvc": "4.62.2", "@rollup/rollup-win32-ia32-msvc": "4.62.2", "@rollup/rollup-win32-x64-gnu": "4.62.2", "@rollup/rollup-win32-x64-msvc": "4.62.2", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA=="], + + "run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="], "safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="], @@ -917,6 +1297,8 @@ "scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="], + "semifies": ["semifies@1.0.0", "", {}, "sha512-xXR3KGeoxTNWPD4aBvL5NUpMTT7WMANr3EWnaS190QVkY52lqqcVRD7Q05UVbBhiWDGWMlJEUam9m7uFFGVScw=="], + "semver": ["semver@7.7.4", "", { "bin": "bin/semver.js" }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], "semver-compare": ["semver-compare@1.0.0", "", {}, "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow=="], @@ -941,7 +1323,7 @@ "socks": ["socks@2.8.7", "", { "dependencies": { "ip-address": "^10.0.1", "smart-buffer": "^4.2.0" } }, "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A=="], - "socks-proxy-agent": ["socks-proxy-agent@8.0.5", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "^4.3.4", "socks": "^2.8.3" } }, "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw=="], + "socks-proxy-agent": ["socks-proxy-agent@7.0.0", "", { "dependencies": { "agent-base": "^6.0.2", "debug": "^4.3.3", "socks": "^2.6.2" } }, "sha512-Fgl0YPZ902wEsAyiQ+idGd1A7rSFx/ayC1CQVMw5P+EQx2V0SgpGtf6OKFhVjPflPUl9YMmEOnmfjCdMUsygww=="], "source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], @@ -951,97 +1333,141 @@ "sprintf-js": ["sprintf-js@1.1.3", "", {}, "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA=="], - "ssri": ["ssri@12.0.0", "", { "dependencies": { "minipass": "^7.0.3" } }, "sha512-S7iGNosepx9RadX82oimUkvr0Ct7IjJbEbs4mJcTxst8um95J3sDYU1RBEOvdu6oL1Wek2ODI5i4MAw+dZ6cAQ=="], + "ssri": ["ssri@9.0.1", "", { "dependencies": { "minipass": "^3.1.1" } }, "sha512-o57Wcn66jMQvfHG1FlYbWeZWW/dHZhJXjpIcTfXldXEk5nz5lStPo3mK0OJQfGR3RbZUlbISexbljkJzuEj/8Q=="], "stat-mode": ["stat-mode@1.0.0", "", {}, "sha512-jH9EhtKIjuXZ2cWxmXS8ZP80XyC3iasQxMDV8jzhNJpfDb7VbQLVW4Wvsxz9QZvzV+G4YoSfBUVKDOyxLzi/sg=="], + "stdin-discarder": ["stdin-discarder@0.3.2", "", {}, "sha512-eCPu1qRxPVkl5605OTWF8Wz40b4Mf45NY5LQmVPQ599knfs5QhASUm9GbJ5BDMDOXgrnh0wyEdvzmL//YMlw0A=="], + "string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], "string-width-cjs": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], - "string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="], + "string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="], "strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], "strip-ansi-cjs": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + "stubborn-fs": ["stubborn-fs@2.0.0", "", { "dependencies": { "stubborn-utils": "^1.0.1" } }, "sha512-Y0AvSwDw8y+nlSNFXMm2g6L51rBGdAQT20J3YSOqxC53Lo3bjWRtr2BKcfYoAf352WYpsZSTURrA0tqhfgudPA=="], + + "stubborn-utils": ["stubborn-utils@1.0.2", "", {}, "sha512-zOh9jPYI+xrNOyisSelgym4tolKTJCQd5GBhK0+0xJvcYDcwlOoxF/rnFKQ2KRZknXSG9jWAp66fwP6AxN9STg=="], + "sumchecker": ["sumchecker@3.0.1", "", { "dependencies": { "debug": "^4.1.0" } }, "sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg=="], "supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], - "tar": ["tar@7.5.13", "", { "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", "minipass": "^7.1.2", "minizlib": "^3.1.0", "yallist": "^5.0.0" } }, "sha512-tOG/7GyXpFevhXVh8jOPJrmtRpOTsYqUIkVdVooZYJS/z8WhfQUX8RJILmeuJNinGAMSu1veBr4asSHFt5/hng=="], + "suspend-react": ["suspend-react@0.1.3", "", { "peerDependencies": { "react": ">=17.0" } }, "sha512-aqldKgX9aZqpoDp3e8/BZ8Dm7x1pJl+qI3ZKxDN0i/IQTWUwBx/ManmlVJ3wowqbno6c2bmiIfs+Um6LbsjJyQ=="], + + "tagged-tag": ["tagged-tag@1.0.0", "", {}, "sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng=="], + + "tar": ["tar@7.5.20", "", { "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", "minipass": "^7.1.2", "minizlib": "^3.1.0", "yallist": "^5.0.0" } }, "sha512-9FcyK4PA6+WbzlTM9WhQm6vB5W7cP7dUiPsv1g7YDwEQnQ1CGpK3MGlKk/ITVWMk05kHZuBhmVhiv8LZoy/PFQ=="], "tar-stream": ["tar-stream@2.2.0", "", { "dependencies": { "bl": "^4.0.3", "end-of-stream": "^1.4.1", "fs-constants": "^1.0.0", "inherits": "^2.0.3", "readable-stream": "^3.1.1" } }, "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ=="], "temp-file": ["temp-file@3.4.0", "", { "dependencies": { "async-exit-hook": "^2.0.1", "fs-extra": "^10.0.0" } }, "sha512-C5tjlC/HCtVUOi3KWVokd4vHVViOmGjtLwIh4MuzPo/nMYTV/p1urt3RnMz2IWXDdKEGJH3k5+KPxtqRsUYGtg=="], + "three": ["three@0.185.1", "", {}, "sha512-5aojFCXKwnjBRZvUnt3WFfEcvUJgkN5LlijRFN95hMy8WVkG4I0QNcJE+OuWvuJ0bOdStrbfXn0pkd6/QyiAlg=="], + "tiny-async-pool": ["tiny-async-pool@1.3.0", "", { "dependencies": { "semver": "^5.5.0" } }, "sha512-01EAw5EDrcVrdgyCLgoSPvqznC0sVxDSVeiOz09FUpjh71G79VCqneOr+xvt7T1r76CF6ZZfPjHorN2+d+3mqA=="], "tiny-typed-emitter": ["tiny-typed-emitter@2.1.0", "", {}, "sha512-qVtvMxeXbVej0cQWKqVSSAHmKZEHAvxdF8HEUBFWts8h+xEo5m/lEiPakuyZ3BnCBjOD8i24kzNOiOLLgsSxhA=="], + "tinyexec": ["tinyexec@1.2.4", "", {}, "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg=="], + "tinyglobby": ["tinyglobby@0.2.16", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg=="], - "tmp": ["tmp@0.2.5", "", {}, "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow=="], + "tmp": ["tmp@0.2.7", "", {}, "sha512-e0votIpp4Uo2AJYSzVHV6xCcawuiez3DzqDAbrTc3YxBkplN6e+dM13ZeIcZnDg/QpSuU2zfZ3rzwY8ukEnaXw=="], "tmp-promise": ["tmp-promise@3.0.3", "", { "dependencies": { "tmp": "^0.2.0" } }, "sha512-RwM7MoPojPxsOBYnyd2hy0bxtIlVrihNs9pj5SUvY8Zz1sQcQG2tG1hSr8PDxfgEB8RNKDhqbIlroIarSNDNsQ=="], + "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], + "tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="], "truncate-utf8-bytes": ["truncate-utf8-bytes@1.0.2", "", { "dependencies": { "utf8-byte-length": "^1.0.1" } }, "sha512-95Pu1QXQvruGEhv62XCMO3Mm90GscOCClvrIUwCM0PYOXK3kaF3l3sIHxx71ThJfcbM2O5Au6SO3AWCSEfW4mQ=="], "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - "tsx": ["tsx@4.22.3", "", { "dependencies": { "esbuild": "~0.28.0" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "bin": { "tsx": "dist/cli.mjs" } }, "sha512-mdoNxBC/cSQObGGVQ5Bpn5i+yv7j68gk3Nfm3wFjcJg3Z0Mix9jzAFfP12prmm5eVGmDKtp0yyArrs0Q+8gZHg=="], + "tsx": ["tsx@4.23.1", "", { "dependencies": { "esbuild": "~0.28.0" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "bin": { "tsx": "dist/cli.mjs" } }, "sha512-GQHnkIfxyx1wYCOS/wonik5MVRZU9hi1TEZmzGZSCJB1y9YgoZ8H6itNE/u4suE+yLmOzuE4E5S4TZ/ZX2wcWQ=="], + + "type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="], - "type-fest": ["type-fest@0.13.1", "", {}, "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg=="], + "type-fest": ["type-fest@5.8.0", "", { "dependencies": { "tagged-tag": "^1.0.0" } }, "sha512-YGYEVz3Fm5iy/AybuA0oyNFq7H4CgQNfRp/qfe8nurE1kuCeNm3/vfm9X4Mtl+qLyaKJUh5xrFZwogr41SMjYA=="], "typescript": ["typescript@6.0.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw=="], - "undici": ["undici@7.27.1", "", {}, "sha512-UDdpiex+mzigiyrXrGbiUaF4HzTNhKbh2vRNFaTMzcqmLIPrZxaCtwo/1TMSuWoM1Xz3WiTo9KdgI3kRqYzJGg=="], + "uint8array-extras": ["uint8array-extras@1.5.0", "", {}, "sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A=="], + + "undici": ["undici@7.28.0", "", {}, "sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA=="], "undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], - "unique-filename": ["unique-filename@4.0.0", "", { "dependencies": { "unique-slug": "^5.0.0" } }, "sha512-XSnEewXmQ+veP7xX2dS5Q4yZAvO40cBN2MWkJ7D/6sW4Dg6wYBNwM1Vrnz1FhH5AdeLIlUXRI9e28z1YZi71NQ=="], + "unique-filename": ["unique-filename@2.0.1", "", { "dependencies": { "unique-slug": "^3.0.0" } }, "sha512-ODWHtkkdx3IAR+veKxFV+VBkUMcN+FaqzUUd7IZzt+0zhDZFPFxhlqwPF3YQvMHx1TD0tdgYl+kuPnJ8E6ql7A=="], - "unique-slug": ["unique-slug@5.0.0", "", { "dependencies": { "imurmurhash": "^0.1.4" } }, "sha512-9OdaqO5kwqR+1kVgHAhsp5vPNU0hnxRa26rBFNfNgM7M6pNtgzeBn3s/xbyCQL3dcjzOatcef6UUHpB/6MaETg=="], + "unique-slug": ["unique-slug@3.0.0", "", { "dependencies": { "imurmurhash": "^0.1.4" } }, "sha512-8EyMynh679x/0gqE9fT9oilG+qEt+ibFyqjuVTsZn1+CMxH+XLlpvr2UZx4nVcCwTpx81nICr2JQFkM+HPLq4w=="], "universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="], - "unplugin": ["unplugin@2.1.0", "", { "dependencies": { "acorn": "^8.14.0", "webpack-virtual-modules": "^0.6.2" } }, "sha512-us4j03/499KhbGP8BU7Hrzrgseo+KdfJYWcbcajCOqsAyb8Gk0Yn2kiUIcZISYCb1JFaZfIuG3b42HmguVOKCQ=="], + "unplugin": ["unplugin@3.3.0", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "picomatch": "^4.0.4", "webpack-virtual-modules": "^0.6.2" }, "peerDependencies": { "@farmfe/core": "*", "@rspack/core": "*", "bun-types-no-globals": "*", "esbuild": "*", "rolldown": "*", "rollup": "*", "unloader": "*", "vite": "*", "webpack": "*" }, "optionalPeers": ["@farmfe/core", "@rspack/core", "bun-types-no-globals", "esbuild", "rolldown", "rollup", "unloader", "vite", "webpack"] }, "sha512-qa66K+crbfyE6JK10GjvbJeRrOsuC/JpbnHctfyp/i4oBTxWOzJfRZyDiOk1PtErMFRu8JhsU/wPvOdBNWe5Rg=="], + + "unzipper": ["unzipper@0.12.5", "", { "dependencies": { "bluebird": "~3.7.2", "duplexer2": "~0.1.4", "fs-extra": "11.3.1", "graceful-fs": "^4.2.2", "node-int64": "^0.4.0" } }, "sha512-tXYOi9R57Uj/2Z25SOs5RRSzq886MBQj2gY8dPL+xl/kv6s6SvByoKfAtvfVeEuhntWDgjd2o9p2lb4TVPAz0A=="], "update-browserslist-db": ["update-browserslist-db@1.2.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": "cli.js" }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="], "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="], + "use-sync-external-store": ["use-sync-external-store@1.6.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w=="], + "utf8-byte-length": ["utf8-byte-length@1.0.5", "", {}, "sha512-Xn0w3MtiQ6zoz2vFyUVruaCL53O/DwUvkEeOvj+uulMm0BkUGYWmBYVyElqZaSLhY6ZD0ulfU3aBra2aVT4xfA=="], "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], "verror": ["verror@1.10.1", "", { "dependencies": { "assert-plus": "^1.0.0", "core-util-is": "1.0.2", "extsprintf": "^1.2.0" } }, "sha512-veufcmxri4e3XSrT0xwfUR7kguIkaxBeosDg00yDWhk49wdwkSUrvvsm7nc75e1PUyvIeZj6nS8VQRYz2/S4Xg=="], - "vite": ["vite@7.3.5", "", { "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-KuOaNhcnGFN2zIPGA7wRmzF+lJA1sea7rHq17aiJ++9lzY1WWG6Jpwqwe1KNbRVPIqHmr8GLYx7jbrQcN/7/ww=="], + "vite": ["vite@7.3.6", "", { "dependencies": { "esbuild": "^0.27.0 || ^0.28.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-4XP60spRGjSZFf1qYH+dJIkK2znL3zQfl9KkOV9MkkRR/3Dls0dxaBsQPTloEc5BLXWPL9vsOxopxyKoMmDueg=="], + + "vscode-jsonrpc": ["vscode-jsonrpc@8.2.0", "", {}, "sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA=="], + + "vscode-languageserver": ["vscode-languageserver@9.0.1", "", { "dependencies": { "vscode-languageserver-protocol": "3.17.5" }, "bin": { "installServerIntoExtension": "bin/installServerIntoExtension" } }, "sha512-woByF3PDpkHFUreUa7Hos7+pUWdeWMXRd26+ZX2A8cFx6v/JPTtd4/uN0/jB6XQHYaOlHbio03NTHCqrgG5n7g=="], + + "vscode-languageserver-protocol": ["vscode-languageserver-protocol@3.17.5", "", { "dependencies": { "vscode-jsonrpc": "8.2.0", "vscode-languageserver-types": "3.17.5" } }, "sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg=="], + + "vscode-languageserver-textdocument": ["vscode-languageserver-textdocument@1.0.12", "", {}, "sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA=="], + + "vscode-languageserver-types": ["vscode-languageserver-types@3.17.5", "", {}, "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg=="], + + "vscode-uri": ["vscode-uri@3.1.0", "", {}, "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ=="], "wcwidth": ["wcwidth@1.0.1", "", { "dependencies": { "defaults": "^1.0.3" } }, "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg=="], + "web-vitals": ["web-vitals@5.3.0", "", {}, "sha512-q6LWsLatGYZp5VGBIOvbTj6JBV2nOmC8KvWztXBmwJcfFAzhwKwbOxhUH306XY3CcaZDUlSmSuNPBsCn0bFu+g=="], + + "webcrypto-core": ["webcrypto-core@1.9.2", "", { "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" } }, "sha512-gsXecm82UQNlTBURJGuqOWy1Ww08S3kZUcr3aOJS02Pk0xLtkfeUAVC0u0xhgdonFme80edSJUIJyuvL/7250Q=="], + "webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="], "webpack-virtual-modules": ["webpack-virtual-modules@0.6.2", "", {}, "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ=="], "whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="], + "when-exit": ["when-exit@2.1.5", "", {}, "sha512-VGkKJ564kzt6Ms1dbgPP/yuIoQCrsFAnRbptpC5wOEsDaNsbCB2bnfnaA8i/vRs5tjUSEOtIuvl9/MyVsvQZCg=="], + "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], "which-module": ["which-module@2.0.1", "", {}, "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ=="], "wide-align": ["wide-align@1.1.5", "", { "dependencies": { "string-width": "^1.0.2 || 2 || 3 || 4" } }, "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg=="], + "word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="], + "wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], "wrap-ansi-cjs": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], - "ws": ["ws@8.21.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g=="], + "ws": ["ws@8.21.1", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-+0NTnW77fFN/DjQi6k/Sq/Yvk4Sgajw7urW8V+asjXnRgDs9gyGkdb7EzgfhA4goXsRIZKE28fzIXBHEzhuiWw=="], "xmlbuilder": ["xmlbuilder@15.1.1", "", {}, "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg=="], @@ -1049,20 +1475,32 @@ "yallist": ["yallist@5.0.0", "", {}, "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw=="], + "yaml": ["yaml@2.9.0", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA=="], + "yargs": ["yargs@17.7.2", "", { "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" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="], "yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="], - "yauzl": ["yauzl@2.10.0", "", { "dependencies": { "buffer-crc32": "~0.2.3", "fd-slicer": "~1.1.0" } }, "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g=="], - "yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], + "yoctocolors": ["yoctocolors@2.1.2", "", {}, "sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug=="], + "zip-stream": ["zip-stream@4.1.1", "", { "dependencies": { "archiver-utils": "^3.0.4", "compress-commons": "^4.1.2", "readable-stream": "^3.6.0" } }, "sha512-9qv4rlDiopXg4E69k+vMHjNN63YFMe9sZMrdlvKnCjlCRWeCBswPPMPUfx+ipsAWq1LXHe70RcbaHdJJpS6hyQ=="], + "zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="], + + "zod-validation-error": ["zod-validation-error@4.0.2", "", { "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" } }, "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ=="], + + "zustand": ["zustand@5.0.14", "", { "peerDependencies": { "@types/react": ">=18.0.0", "immer": ">=9.0.6", "react": ">=18.0.0", "use-sync-external-store": ">=1.2.0" }, "optionalPeers": ["@types/react", "immer", "react", "use-sync-external-store"] }, "sha512-/8tAspM5LMPr28b3fwLYrtdj77ECpfZviaP75CMTnwO8ISyaE4GDIG/9rDDYq/cH9D2Xw2A2RXglLInmVBQB/g=="], + + "@apm-js-collab/code-transformer/@types/estree": ["@types/estree@1.0.9", "", {}, "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg=="], + "@babel/core/semver": ["semver@6.3.1", "", { "bin": "bin/semver.js" }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], "@babel/helper-compilation-targets/semver": ["semver@6.3.1", "", { "bin": "bin/semver.js" }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + "@develar/schema-utils/ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="], + "@electron/asar/commander": ["commander@5.1.0", "", {}, "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg=="], "@electron/asar/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], @@ -1077,11 +1515,15 @@ "@electron/universal/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], + "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], + "@malept/flatpak-bundler/fs-extra": ["fs-extra@9.1.0", "", { "dependencies": { "at-least-node": "^1.0.0", "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ=="], - "@npmcli/agent/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], + "@oxc-resolver/binding-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.11.2", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.2", "tslib": "^2.4.0" } }, "sha512-TC8MkTuZUtcTSiFeuC0ksCh9QIJ5+F21MvZ4Wn4ORfYaFJ/0dsiudv5tVkejgwZlwQ39jL9WWDe2lz8x0WglOA=="], - "@rollup/pluginutils/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="], + "@oxc-resolver/binding-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.11.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-kyOl3X0DuTiT1h2ft8r2fYO8JYtU9a9Xis/zBSiGArNaagCOWx90N1k2wxp18czFDH+OgcWGb5ZP/XMt3dcyPA=="], + + "@react-grab/cli/agent-install": ["agent-install@0.0.6", "", { "dependencies": { "@iarna/toml": "^2.2.5", "commander": "^14.0.0", "jsonc-parser": "^3.3.1", "picocolors": "^1.1.1", "prompts": "^2.4.2", "yaml": "^2.8.3" }, "bin": { "agent-install": "bin/agent-install.mjs" } }, "sha512-7NRMZ/ZDz2vHevQTgJsocBFpakB1/Wx5ip19YSJuj4VOXpraWztTerViNtdSyARKZT9e2yVwUUB5JXXCE7mNrA=="], "@types/cacheable-request/@types/node": ["@types/node@22.19.11", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-BH7YwL6rA93ReqeQS1c4bsPpcfOmJasG+Fkr6Y59q83f9M1WcBRHR2vM+P9eOisYRcN3ujQoiZY8uk5W+1WL8w=="], @@ -1097,7 +1539,7 @@ "@types/ws/@types/node": ["@types/node@22.19.11", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-BH7YwL6rA93ReqeQS1c4bsPpcfOmJasG+Fkr6Y59q83f9M1WcBRHR2vM+P9eOisYRcN3ujQoiZY8uk5W+1WL8w=="], - "@types/yauzl/@types/node": ["@types/node@22.19.11", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-BH7YwL6rA93ReqeQS1c4bsPpcfOmJasG+Fkr6Y59q83f9M1WcBRHR2vM+P9eOisYRcN3ujQoiZY8uk5W+1WL8w=="], + "ajv-keywords/ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="], "app-builder-lib/@electron/get": ["@electron/get@3.1.0", "", { "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" }, "optionalDependencies": { "global-agent": "^3.0.0" } }, "sha512-F+nKc0xW+kVbBRhFzaMgPy3KwmuNTYX1fx6+FxxoSnNgwYX6LD7AKBTWkU0MQ6IBoe7dz069CNkR673sPAgkCQ=="], @@ -1105,23 +1547,39 @@ "app-builder-lib/which": ["which@5.0.0", "", { "dependencies": { "isexe": "^3.1.1" }, "bin": { "node-which": "bin/which.js" } }, "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ=="], - "archiver-utils/readable-stream": ["readable-stream@2.3.8", "", { "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" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="], + "archiver/readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], + + "are-we-there-yet/readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], + + "bl/buffer": ["buffer@5.7.1", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="], + + "bl/readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], + + "cacache/chownr": ["chownr@2.0.0", "", {}, "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ=="], - "cacache/fs-minipass": ["fs-minipass@3.0.3", "", { "dependencies": { "minipass": "^7.0.3" } }, "sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw=="], + "cacache/glob": ["glob@8.1.0", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^5.0.1", "once": "^1.3.0" } }, "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ=="], - "cacache/glob": ["glob@10.5.0", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": "dist/esm/bin.mjs" }, "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg=="], + "cacache/lru-cache": ["lru-cache@7.18.3", "", {}, "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA=="], - "cacache/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], + "cacache/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="], "clone-response/mimic-response": ["mimic-response@1.0.1", "", {}, "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ=="], + "compress-commons/readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], + "config-file-ts/glob": ["glob@10.5.0", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": "dist/esm/bin.mjs" }, "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg=="], "config-file-ts/typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + "crc/buffer": ["buffer@5.7.1", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="], + + "crc32-stream/readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], + + "deslop-js/typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + "dir-compare/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], - "discord-rpc/ws": ["ws@7.5.10", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": "^5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ=="], + "dmg-license/ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="], "electron/@types/node": ["@types/node@24.10.13", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-oH72nZRfDv9lADUBSo104Aq7gPHpQZc4BTx38r9xf9pg5LfP6EzSyH2n7qFmmxRQXh7YlUXODcYsg6PuTDSxGg=="], @@ -1129,6 +1587,20 @@ "electron-builder-squirrel-windows/builder-util": ["builder-util@25.1.7", "", { "dependencies": { "7zip-bin": "~5.2.0", "@types/debug": "^4.1.6", "app-builder-bin": "5.0.0-alpha.10", "bluebird-lst": "^1.0.9", "builder-util-runtime": "9.2.10", "chalk": "^4.1.2", "cross-spawn": "^7.0.3", "debug": "^4.3.4", "fs-extra": "^10.1.0", "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.0", "is-ci": "^3.0.0", "js-yaml": "^4.1.0", "source-map-support": "^0.5.19", "stat-mode": "^1.0.0", "temp-file": "^3.4.0" } }, "sha512-7jPjzBwEGRbwNcep0gGNpLXG9P94VA3CPAZQCzxkFXiV2GMQKlziMbY//rXPI7WKfhsvGgFXjTcXdBEwgXw9ww=="], + "eslint/@types/estree": ["@types/estree@1.0.9", "", {}, "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg=="], + + "eslint/ajv": ["ajv@6.15.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw=="], + + "eslint/find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="], + + "eslint/glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="], + + "eslint/ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], + + "eslint-plugin-react-hooks/@babel/parser": ["@babel/parser@7.29.7", "", { "dependencies": { "@babel/types": "^7.29.7" }, "bin": "./bin/babel-parser.js" }, "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg=="], + + "eslint-scope/@types/estree": ["@types/estree@1.0.9", "", {}, "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg=="], + "filelist/minimatch": ["minimatch@5.1.6", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g=="], "foreground-child/signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], @@ -1141,10 +1613,26 @@ "is-ci/ci-info": ["ci-info@3.9.0", "", {}, "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ=="], - "lazystream/readable-stream": ["readable-stream@2.3.8", "", { "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" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="], - "lru-cache/yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], + "magicast/@babel/parser": ["@babel/parser@7.29.7", "", { "dependencies": { "@babel/types": "^7.29.7" }, "bin": "./bin/babel-parser.js" }, "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg=="], + + "make-fetch-happen/http-proxy-agent": ["http-proxy-agent@5.0.0", "", { "dependencies": { "@tootallnate/once": "2", "agent-base": "6", "debug": "4" } }, "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w=="], + + "make-fetch-happen/https-proxy-agent": ["https-proxy-agent@5.0.1", "", { "dependencies": { "agent-base": "6", "debug": "4" } }, "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA=="], + + "make-fetch-happen/lru-cache": ["lru-cache@7.18.3", "", {}, "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA=="], + + "make-fetch-happen/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="], + + "micromatch/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="], + + "minipass-collect/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="], + + "minipass-fetch/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="], + + "minipass-fetch/minizlib": ["minizlib@2.1.2", "", { "dependencies": { "minipass": "^3.0.0", "yallist": "^4.0.0" } }, "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg=="], + "minipass-flush/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="], "minipass-pipeline/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="], @@ -1153,34 +1641,66 @@ "node-gyp/env-paths": ["env-paths@2.2.1", "", {}, "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A=="], - "node-gyp/which": ["which@5.0.0", "", { "dependencies": { "isexe": "^3.1.1" }, "bin": { "node-which": "bin/which.js" } }, "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ=="], + "node-gyp/which": ["which@6.0.1", "", { "dependencies": { "isexe": "^4.0.0" }, "bin": { "node-which": "bin/which.js" } }, "sha512-oGLe46MIrCRqX7ytPUf66EAYvdeMIZYn3WaocqqKZAxrBpkqHfL/qvTyJ/bTk5+AqHCjXmrv3CEWgy368zhRUg=="], + + "ora/chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="], + + "ora/string-width": ["string-width@8.2.2", "", { "dependencies": { "get-east-asian-width": "^1.5.0", "strip-ansi": "^7.1.2" } }, "sha512-GaPUh5gfdrYzqeVNZvUfT23vYYxXzKYidUcnMtJg/3rxRV63EFZy3k6xfKlmfeJD0176lnUV/Usr3XcwSvFzpg=="], "p-locate/p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="], "path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], + "pkijs/@noble/hashes": ["@noble/hashes@1.4.0", "", {}, "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg=="], + + "posthog-js/fflate": ["fflate@0.4.9", "", {}, "sha512-zdxgIEddhfsyCaWpJ2SdXEP8ZMrKJ6+5jl4OupODcywU0IhRk6gdXuVGcPICyfx2H97hVK7xmJtRLPjkxAX8Vw=="], + + "posthog-js/preact": ["preact@10.29.7", "", { "peerDependencies": { "preact-render-to-string": ">=5" }, "optionalPeers": ["preact-render-to-string"] }, "sha512-DCHYrK/B10yUD3ZjLfhZ3WIE/9Vf9VFUODcRE2dRomTYDpJk6z6L9wecSfhfE6M9ZTHUdyQkoC46arIDhEV84Q=="], + "qrcode/yargs": ["yargs@15.4.1", "", { "dependencies": { "cliui": "^6.0.0", "decamelize": "^1.2.0", "find-up": "^4.1.0", "get-caller-file": "^2.0.1", "require-directory": "^2.1.1", "require-main-filename": "^2.0.0", "set-blocking": "^2.0.0", "string-width": "^4.2.0", "which-module": "^2.0.0", "y18n": "^4.0.0", "yargs-parser": "^18.1.2" } }, "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A=="], - "react-scan/@types/node": ["@types/node@20.19.39", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-orrrD74MBUyK8jOAD/r0+lfa1I2MO6I+vAkmAWzMYbCcgrN4lCrmK52gRFQq/JRxfYPfonkr4b0jcY7Olqdqbw=="], + "react-doctor/oxlint": ["oxlint@1.66.0", "", { "optionalDependencies": { "@oxlint/binding-android-arm-eabi": "1.66.0", "@oxlint/binding-android-arm64": "1.66.0", "@oxlint/binding-darwin-arm64": "1.66.0", "@oxlint/binding-darwin-x64": "1.66.0", "@oxlint/binding-freebsd-x64": "1.66.0", "@oxlint/binding-linux-arm-gnueabihf": "1.66.0", "@oxlint/binding-linux-arm-musleabihf": "1.66.0", "@oxlint/binding-linux-arm64-gnu": "1.66.0", "@oxlint/binding-linux-arm64-musl": "1.66.0", "@oxlint/binding-linux-ppc64-gnu": "1.66.0", "@oxlint/binding-linux-riscv64-gnu": "1.66.0", "@oxlint/binding-linux-riscv64-musl": "1.66.0", "@oxlint/binding-linux-s390x-gnu": "1.66.0", "@oxlint/binding-linux-x64-gnu": "1.66.0", "@oxlint/binding-linux-x64-musl": "1.66.0", "@oxlint/binding-openharmony-arm64": "1.66.0", "@oxlint/binding-win32-arm64-msvc": "1.66.0", "@oxlint/binding-win32-ia32-msvc": "1.66.0", "@oxlint/binding-win32-x64-msvc": "1.66.0" }, "peerDependencies": { "oxlint-tsgolint": ">=0.22.1" }, "optionalPeers": ["oxlint-tsgolint"], "bin": { "oxlint": "bin/oxlint" } }, "sha512-N4LLxYLd94KEBqXDMDM5f+2PUpItTjDLreXe2Gn5KhjhCK4Qp2YUXaBi8Yu325ryOgKwt22m45fpD7nPOn69Yw=="], + + "react-doctor/typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + + "react-grab/bippy": ["bippy@0.5.43", "", { "peerDependencies": { "react": ">=17.0.1" } }, "sha512-Tvu7b1M7+d8b9/YHaCeODEsi2CgbuoBql+dWSBrNnCuqJ1gMUeY3i0r+319hvjjl5GVBP6FFWxrKnq3fhZER0w=="], "readdir-glob/minimatch": ["minimatch@5.1.6", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g=="], - "string_decoder/safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], + "restore-cursor/signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], + + "rollup/@types/estree": ["@types/estree@1.0.9", "", {}, "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg=="], + + "serialize-error/type-fest": ["type-fest@0.13.1", "", {}, "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg=="], + + "socks-proxy-agent/agent-base": ["agent-base@6.0.2", "", { "dependencies": { "debug": "4" } }, "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ=="], + + "ssri/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="], + + "tar-stream/readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], "tiny-async-pool/semver": ["semver@5.7.2", "", { "bin": { "semver": "bin/semver" } }, "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g=="], "tsx/esbuild": ["esbuild@0.28.0", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.28.0", "@esbuild/android-arm": "0.28.0", "@esbuild/android-arm64": "0.28.0", "@esbuild/android-x64": "0.28.0", "@esbuild/darwin-arm64": "0.28.0", "@esbuild/darwin-x64": "0.28.0", "@esbuild/freebsd-arm64": "0.28.0", "@esbuild/freebsd-x64": "0.28.0", "@esbuild/linux-arm": "0.28.0", "@esbuild/linux-arm64": "0.28.0", "@esbuild/linux-ia32": "0.28.0", "@esbuild/linux-loong64": "0.28.0", "@esbuild/linux-mips64el": "0.28.0", "@esbuild/linux-ppc64": "0.28.0", "@esbuild/linux-riscv64": "0.28.0", "@esbuild/linux-s390x": "0.28.0", "@esbuild/linux-x64": "0.28.0", "@esbuild/netbsd-arm64": "0.28.0", "@esbuild/netbsd-x64": "0.28.0", "@esbuild/openbsd-arm64": "0.28.0", "@esbuild/openbsd-x64": "0.28.0", "@esbuild/openharmony-arm64": "0.28.0", "@esbuild/sunos-x64": "0.28.0", "@esbuild/win32-arm64": "0.28.0", "@esbuild/win32-ia32": "0.28.0", "@esbuild/win32-x64": "0.28.0" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw=="], - "vite/esbuild": ["esbuild@0.27.7", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.7", "@esbuild/android-arm": "0.27.7", "@esbuild/android-arm64": "0.27.7", "@esbuild/android-x64": "0.27.7", "@esbuild/darwin-arm64": "0.27.7", "@esbuild/darwin-x64": "0.27.7", "@esbuild/freebsd-arm64": "0.27.7", "@esbuild/freebsd-x64": "0.27.7", "@esbuild/linux-arm": "0.27.7", "@esbuild/linux-arm64": "0.27.7", "@esbuild/linux-ia32": "0.27.7", "@esbuild/linux-loong64": "0.27.7", "@esbuild/linux-mips64el": "0.27.7", "@esbuild/linux-ppc64": "0.27.7", "@esbuild/linux-riscv64": "0.27.7", "@esbuild/linux-s390x": "0.27.7", "@esbuild/linux-x64": "0.27.7", "@esbuild/netbsd-arm64": "0.27.7", "@esbuild/netbsd-x64": "0.27.7", "@esbuild/openbsd-arm64": "0.27.7", "@esbuild/openbsd-x64": "0.27.7", "@esbuild/openharmony-arm64": "0.27.7", "@esbuild/sunos-x64": "0.27.7", "@esbuild/win32-arm64": "0.27.7", "@esbuild/win32-ia32": "0.27.7", "@esbuild/win32-x64": "0.27.7" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w=="], + "unzipper/fs-extra": ["fs-extra@11.3.1", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-eXvGGwZ5CL17ZSwHWd3bbgk7UUpF6IFHtP57NYYakPvHOs8GDgDe5KJI36jIJzDkJ6eJjuzRA8eBQb6SkKue0g=="], + + "vite/esbuild": ["esbuild@0.28.0", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.28.0", "@esbuild/android-arm": "0.28.0", "@esbuild/android-arm64": "0.28.0", "@esbuild/android-x64": "0.28.0", "@esbuild/darwin-arm64": "0.28.0", "@esbuild/darwin-x64": "0.28.0", "@esbuild/freebsd-arm64": "0.28.0", "@esbuild/freebsd-x64": "0.28.0", "@esbuild/linux-arm": "0.28.0", "@esbuild/linux-arm64": "0.28.0", "@esbuild/linux-ia32": "0.28.0", "@esbuild/linux-loong64": "0.28.0", "@esbuild/linux-mips64el": "0.28.0", "@esbuild/linux-ppc64": "0.28.0", "@esbuild/linux-riscv64": "0.28.0", "@esbuild/linux-s390x": "0.28.0", "@esbuild/linux-x64": "0.28.0", "@esbuild/netbsd-arm64": "0.28.0", "@esbuild/netbsd-x64": "0.28.0", "@esbuild/openbsd-arm64": "0.28.0", "@esbuild/openbsd-x64": "0.28.0", "@esbuild/openharmony-arm64": "0.28.0", "@esbuild/sunos-x64": "0.28.0", "@esbuild/win32-arm64": "0.28.0", "@esbuild/win32-ia32": "0.28.0", "@esbuild/win32-x64": "0.28.0" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw=="], "zip-stream/archiver-utils": ["archiver-utils@3.0.4", "", { "dependencies": { "glob": "^7.2.3", "graceful-fs": "^4.2.0", "lazystream": "^1.0.0", "lodash.defaults": "^4.2.0", "lodash.difference": "^4.5.0", "lodash.flatten": "^4.4.0", "lodash.isplainobject": "^4.0.6", "lodash.union": "^4.6.0", "normalize-path": "^3.0.0", "readable-stream": "^3.6.0" } }, "sha512-KVgf4XQVrTjhyWmx6cte4RxonPLR9onExufI1jhvw/MQ4BB6IsZD5gT8Lq+u/+pRkWna/6JoHpiQioaqFP5Rzw=="], + "zip-stream/readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], + + "@develar/schema-utils/ajv/json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], + "@electron/asar/minimatch/brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="], "@electron/universal/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], "@types/qrcode/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], + "ajv-keywords/ajv/json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], + "app-builder-lib/@electron/get/env-paths": ["env-paths@2.2.1", "", {}, "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A=="], "app-builder-lib/@electron/get/fs-extra": ["fs-extra@8.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^4.0.0", "universalify": "^0.1.0" } }, "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g=="], @@ -1189,18 +1709,28 @@ "app-builder-lib/which/isexe": ["isexe@3.1.5", "", {}, "sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w=="], - "archiver-utils/readable-stream/string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="], + "archiver/readable-stream/string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="], + + "are-we-there-yet/readable-stream/string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="], + + "bl/readable-stream/string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="], - "cacache/glob/jackspeak": ["jackspeak@3.4.3", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw=="], + "cacache/glob/minimatch": ["minimatch@5.1.6", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g=="], - "cacache/glob/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], + "cacache/minipass/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="], + + "compress-commons/readable-stream/string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="], "config-file-ts/glob/jackspeak": ["jackspeak@3.4.3", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw=="], "config-file-ts/glob/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], + "crc32-stream/readable-stream/string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="], + "dir-compare/minimatch/brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="], + "dmg-license/ajv/json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], + "electron-builder-squirrel-windows/app-builder-lib/@electron/osx-sign": ["@electron/osx-sign@1.3.1", "", { "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" } }, "sha512-BAfviURMHpmb1Yb50YbCxnOY0wfwaLXH5KJ4+80zS0gUkzDX3ec23naTlEqKsN+PwYn+a1cCzM7BJ4Wcd3sGzw=="], "electron-builder-squirrel-windows/app-builder-lib/@electron/rebuild": ["@electron/rebuild@3.6.1", "", { "dependencies": { "@malept/cross-spawn-promise": "^2.0.0", "chalk": "^4.0.0", "debug": "^4.1.1", "detect-libc": "^2.0.1", "fs-extra": "^10.0.0", "got": "^11.7.0", "node-abi": "^3.45.0", "node-api-version": "^0.2.0", "node-gyp": "^9.0.0", "ora": "^5.1.0", "read-binary-file-arch": "^1.0.6", "semver": "^7.3.5", "tar": "^6.0.5", "yargs": "^17.0.1" }, "bin": { "electron-rebuild": "lib/cli.js" } }, "sha512-f6596ZHpEq/YskUd8emYvOUne89ij8mQgjYFA5ru25QwbrRO+t1SImofdDv7kKOuWCmVOuU5tvfkbgGxIl3E/w=="], @@ -1213,14 +1743,18 @@ "electron-builder-squirrel-windows/app-builder-lib/electron-publish": ["electron-publish@25.1.7", "", { "dependencies": { "@types/fs-extra": "^9.0.11", "builder-util": "25.1.7", "builder-util-runtime": "9.2.10", "chalk": "^4.1.2", "fs-extra": "^10.1.0", "lazy-val": "^1.0.5", "mime": "^2.5.2" } }, "sha512-+jbTkR9m39eDBMP4gfbqglDd6UvBC7RLh5Y0MhFSsc6UkGHj9Vj9TWobxevHYMMqmoujL11ZLjfPpMX+Pt6YEg=="], - "electron-builder-squirrel-windows/app-builder-lib/tar": ["tar@6.2.1", "", { "dependencies": { "chownr": "^2.0.0", "fs-minipass": "^2.0.0", "minipass": "^5.0.0", "minizlib": "^2.1.1", "mkdirp": "^1.0.3", "yallist": "^4.0.0" } }, "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A=="], - - "electron-builder-squirrel-windows/builder-util/app-builder-bin": ["app-builder-bin@5.0.0-alpha.10", "", {}, "sha512-Ev4jj3D7Bo+O0GPD2NMvJl+PGiBAfS7pUGawntBNpCbxtpncfUixqFj9z9Jme7V7s3LBGqsWZZP54fxBX3JKJw=="], + "electron-builder-squirrel-windows/app-builder-lib/minimatch": ["minimatch@10.2.0", "", { "dependencies": { "brace-expansion": "^5.0.2" } }, "sha512-ugkC31VaVg9cF0DFVoADH12k6061zNZkZON+aX8AWsR9GhPcErkcMBceb6znR8wLERM2AkkOxy2nWRLpT9Jq5w=="], "electron-builder-squirrel-windows/builder-util/builder-util-runtime": ["builder-util-runtime@9.2.10", "", { "dependencies": { "debug": "^4.3.4", "sax": "^1.2.4" } }, "sha512-6p/gfG1RJSQeIbz8TK5aPNkoztgY1q5TgmGFMAXcY8itsGW6Y2ld1ALsZ5UJn8rog7hKF3zHx5iQbNQ8uLcRlw=="], "electron/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], + "eslint-plugin-react-hooks/@babel/parser/@babel/types": ["@babel/types@7.29.7", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA=="], + + "eslint/ajv/json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], + + "eslint/find-up/locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="], + "filelist/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], "fs-minipass/minipass/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="], @@ -1229,7 +1763,19 @@ "hosted-git-info/lru-cache/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="], - "lazystream/readable-stream/string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="], + "magicast/@babel/parser/@babel/types": ["@babel/types@7.29.7", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA=="], + + "make-fetch-happen/http-proxy-agent/agent-base": ["agent-base@6.0.2", "", { "dependencies": { "debug": "4" } }, "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ=="], + + "make-fetch-happen/https-proxy-agent/agent-base": ["agent-base@6.0.2", "", { "dependencies": { "debug": "4" } }, "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ=="], + + "make-fetch-happen/minipass/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="], + + "minipass-collect/minipass/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="], + + "minipass-fetch/minipass/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="], + + "minipass-fetch/minizlib/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="], "minipass-flush/minipass/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="], @@ -1237,7 +1783,9 @@ "minipass-sized/minipass/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="], - "node-gyp/which/isexe": ["isexe@3.1.5", "", {}, "sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w=="], + "node-gyp/which/isexe": ["isexe@4.0.0", "", {}, "sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw=="], + + "ora/string-width/strip-ansi": ["strip-ansi@7.1.2", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA=="], "qrcode/yargs/cliui": ["cliui@6.0.0", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.0", "wrap-ansi": "^6.2.0" } }, "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ=="], @@ -1245,8 +1793,50 @@ "qrcode/yargs/yargs-parser": ["yargs-parser@18.1.3", "", { "dependencies": { "camelcase": "^5.0.0", "decamelize": "^1.2.0" } }, "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ=="], + "react-doctor/oxlint/@oxlint/binding-android-arm-eabi": ["@oxlint/binding-android-arm-eabi@1.66.0", "", { "os": "android", "cpu": "arm" }, "sha512-f7kq8N51T4phpzqfBpA2qaVTI/KrkCmNwaj3t/97I/WLTDI+UhlP5GL9eER+zVxBhtlx5rKXWByJU1/zDAvyaw=="], + + "react-doctor/oxlint/@oxlint/binding-android-arm64": ["@oxlint/binding-android-arm64@1.66.0", "", { "os": "android", "cpu": "arm64" }, "sha512-xu6QO71tdDS9mjmLZ3AqhtaVHBvdmsOKkYnReNNDgh+XiwnsipeQOIxbiYOOO0iAXycJ+GK0wdMSZP/2j/AmSg=="], + + "react-doctor/oxlint/@oxlint/binding-darwin-arm64": ["@oxlint/binding-darwin-arm64@1.66.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-HZ24VimSOC7mxuEA99e0H2FS0C1yO3+iW13jPRAk+e2njsUs3QeAXsafCDyaIrV/MirdOVez+etQNQsJE43zNQ=="], + + "react-doctor/oxlint/@oxlint/binding-darwin-x64": ["@oxlint/binding-darwin-x64@1.66.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-awhj8ZvJrrRSnXj7V++rpZvTmnl99L6mi0B7gg7Cp7BN6cKpzuI481bHNLvXGA9GB1/oEgA3ponuyoAc6Md12A=="], + + "react-doctor/oxlint/@oxlint/binding-freebsd-x64": ["@oxlint/binding-freebsd-x64@1.66.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-KQF0oVV21/FjIqkRuL8Q1vh8ECsE5+ocdH5tcqTQ4ZnYuDVoYibQUNfqBjQaUsP6UIIda5Y75Wpm5p4RgQWiWw=="], + + "react-doctor/oxlint/@oxlint/binding-linux-arm-gnueabihf": ["@oxlint/binding-linux-arm-gnueabihf@1.66.0", "", { "os": "linux", "cpu": "arm" }, "sha512-9u1rgwZSEXWb30vbFZzQ78HVXBo0WCKNwJ3a2InRUTNMRng+PUDIoSFmA+m4HdUfBaIqftShq8J8qHc+eE/Vig=="], + + "react-doctor/oxlint/@oxlint/binding-linux-arm-musleabihf": ["@oxlint/binding-linux-arm-musleabihf@1.66.0", "", { "os": "linux", "cpu": "arm" }, "sha512-Ynot2HR1bHxUaNWoC280MVTDfZuaWuP3XfSMRDhyuZrVjhzoaBCVFlw8h8qeZjWKVUBhPWFIxB7AQTlK8Z2WWg=="], + + "react-doctor/oxlint/@oxlint/binding-linux-arm64-gnu": ["@oxlint/binding-linux-arm64-gnu@1.66.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-xCbgzciGgo+A4aQZEknsNrNiIwY7sU5SfRuMmRjPIvZAgdF34cIHiKvwOsS5XRLjlTVSFwitmq6YclTtHTfU+g=="], + + "react-doctor/oxlint/@oxlint/binding-linux-arm64-musl": ["@oxlint/binding-linux-arm64-musl@1.66.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-hmo+ZB/lHkR1HdDmnziNpzSLmulnUSu10VEqX2Yex7OwvoBAbjJQLvy4gIBRV3AAwWnCvAxKp5Nv1GE6LU1QMg=="], + + "react-doctor/oxlint/@oxlint/binding-linux-ppc64-gnu": ["@oxlint/binding-linux-ppc64-gnu@1.66.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-2Invd4Uyy81mVooQC5FBtfxSNrvcX1OxbMlVQ6M2erRrNI2awFYF26YNW2yFxdVFZ4ffNOWKghtMjhnUPsXsVA=="], + + "react-doctor/oxlint/@oxlint/binding-linux-riscv64-gnu": ["@oxlint/binding-linux-riscv64-gnu@1.66.0", "", { "os": "linux", "cpu": "none" }, "sha512-s0iXPDQVdgayE3RGa/N2DZF7tjgg0TwEtD1sGoDxqPDGrIXgo45H0yHknT0f9A0yteASsweYZtDyTuVlM4aSag=="], + + "react-doctor/oxlint/@oxlint/binding-linux-riscv64-musl": ["@oxlint/binding-linux-riscv64-musl@1.66.0", "", { "os": "linux", "cpu": "none" }, "sha512-OekL4XFiu7RPK0JIZi8VeHgtIXPREf42t8Cy/rKEsC+P3gcqDgNAAGiyuUOpdbG4wwbfue1q4CHcCO7spSve6w=="], + + "react-doctor/oxlint/@oxlint/binding-linux-s390x-gnu": ["@oxlint/binding-linux-s390x-gnu@1.66.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-Ga1D0kj1SFslm34ThA/BdkUlyAYEnTsXyRC4pF0C5agZSwtGdHYWMTQWemUfBGp4RCG4QWXgdO+HmmmKqOtlBg=="], + + "react-doctor/oxlint/@oxlint/binding-linux-x64-gnu": ["@oxlint/binding-linux-x64-gnu@1.66.0", "", { "os": "linux", "cpu": "x64" }, "sha512-p5jfP1wUZe/IC3qpQO84n9DRnf9g3lKRtLBlQq23ykyrDglHcVx7sWmVTlPuU6SBw8mNnPzyOn022G3XZHnlww=="], + + "react-doctor/oxlint/@oxlint/binding-linux-x64-musl": ["@oxlint/binding-linux-x64-musl@1.66.0", "", { "os": "linux", "cpu": "x64" }, "sha512-vUB/sYlYZorDL1ZD+o9mRv7zbsykrrFRtmgS6R8musZqLtrPRQn1gc1eGpuX+sfdccz42STl/AqldY6XRb2upQ=="], + + "react-doctor/oxlint/@oxlint/binding-openharmony-arm64": ["@oxlint/binding-openharmony-arm64@1.66.0", "", { "os": "none", "cpu": "arm64" }, "sha512-yde+6p/F59xRkGR9H1HfngWRif1QRJjynZK349l+UI0H6w9hL3G8/AVaTHFyTtLVQ56qtNbX2/5Dc77n1ovnOg=="], + + "react-doctor/oxlint/@oxlint/binding-win32-arm64-msvc": ["@oxlint/binding-win32-arm64-msvc@1.66.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-O9GLucgoTdmOrbBX+EjzNe7o/Ze5TFOvXcib6bzUOtBOmj6cV+zw18NgB+cGKAkDw1Pdqs8vGkfHbbsLuDtXWg=="], + + "react-doctor/oxlint/@oxlint/binding-win32-ia32-msvc": ["@oxlint/binding-win32-ia32-msvc@1.66.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-m3Pjwc2MfTcom4E4gOv7DyuGyt7OfGNCbmqDHd+N7EzXmP+ppHuudm2NjcA3AjV5TSeGxaguVF4SbTKHe1USYA=="], + + "react-doctor/oxlint/@oxlint/binding-win32-x64-msvc": ["@oxlint/binding-win32-x64-msvc@1.66.0", "", { "os": "win32", "cpu": "x64" }, "sha512-/DbBvw8UFBhja6PqudUjV4UtfsJr0Oa7jUjWVKB0g86lj/VwnPrkngn0sFql3c9RDA0O16dh7ozsXb6GjNAzBQ=="], + "readdir-glob/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], + "ssri/minipass/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="], + + "tar-stream/readable-stream/string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="], + "tsx/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.28.0", "", { "os": "aix", "cpu": "ppc64" }, "sha512-lhRUCeuOyJQURhTxl4WkpFTjIsbDayJHih5kZC1giwE+MhIzAb7mEsQMqMf18rHLsrb5qI1tafG20mLxEWcWlA=="], "tsx/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.28.0", "", { "os": "android", "cpu": "arm" }, "sha512-wqh0ByljabXLKHeWXYLqoJ5jKC4XBaw6Hk08OfMrCRd2nP2ZQ5eleDZC41XHyCNgktBGYMbqnrJKq/K/lzPMSQ=="], @@ -1299,57 +1889,59 @@ "tsx/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.28.0", "", { "os": "win32", "cpu": "x64" }, "sha512-pEl1bO9mfAmIC+tW5btTmrKaujg3zGtUmWNdCw/xs70FBjwAL3o9OEKNHvNmnyylD6ubxUERiEhdsL0xBQ9efw=="], - "vite/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.7", "", { "os": "aix", "cpu": "ppc64" }, "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg=="], + "vite/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.28.0", "", { "os": "aix", "cpu": "ppc64" }, "sha512-lhRUCeuOyJQURhTxl4WkpFTjIsbDayJHih5kZC1giwE+MhIzAb7mEsQMqMf18rHLsrb5qI1tafG20mLxEWcWlA=="], + + "vite/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.28.0", "", { "os": "android", "cpu": "arm" }, "sha512-wqh0ByljabXLKHeWXYLqoJ5jKC4XBaw6Hk08OfMrCRd2nP2ZQ5eleDZC41XHyCNgktBGYMbqnrJKq/K/lzPMSQ=="], - "vite/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.27.7", "", { "os": "android", "cpu": "arm" }, "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ=="], + "vite/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.28.0", "", { "os": "android", "cpu": "arm64" }, "sha512-+WzIXQOSaGs33tLEgYPYe/yQHf0WTU0X42Jca3y8NWMbUVhp7rUnw+vAsRC/QiDrdD31IszMrZy+qwPOPjd+rw=="], - "vite/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.27.7", "", { "os": "android", "cpu": "arm64" }, "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ=="], + "vite/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.28.0", "", { "os": "android", "cpu": "x64" }, "sha512-+VJggoaKhk2VNNqVL7f6S189UzShHC/mR9EE8rDdSkdpN0KflSwWY/gWjDrNxxisg8Fp1ZCD9jLMo4m0OUfeUA=="], - "vite/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.27.7", "", { "os": "android", "cpu": "x64" }, "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg=="], + "vite/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.28.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-0T+A9WZm+bZ84nZBtk1ckYsOvyA3x7e2Acj1KdVfV4/2tdG4fzUp91YHx+GArWLtwqp77pBXVCPn2We7Letr0Q=="], - "vite/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.27.7", "", { "os": "darwin", "cpu": "arm64" }, "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw=="], + "vite/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.28.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-fyzLm/DLDl/84OCfp2f/XQ4flmORsjU7VKt8HLjvIXChJoFFOIL6pLJPH4Yhd1n1gGFF9mPwtlN5Wf82DZs+LQ=="], - "vite/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.27.7", "", { "os": "darwin", "cpu": "x64" }, "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ=="], + "vite/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.28.0", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-l9GeW5UZBT9k9brBYI+0WDffcRxgHQD8ShN2Ur4xWq/NFzUKm3k5lsH4PdaRgb2w7mI9u61nr2gI2mLI27Nh3Q=="], - "vite/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.27.7", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w=="], + "vite/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.28.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-BXoQai/A0wPO6Es3yFJ7APCiKGc1tdAEOgeTNy3SsB491S3aHn4S4r3e976eUnPdU+NbdtmBuLncYir2tMU9Nw=="], - "vite/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.27.7", "", { "os": "freebsd", "cpu": "x64" }, "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ=="], + "vite/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.28.0", "", { "os": "linux", "cpu": "arm" }, "sha512-CjaaREJagqJp7iTaNQjjidaNbCKYcd4IDkzbwwxtSvjI7NZm79qiHc8HqciMddQ6CKvJT6aBd8lO9kN/ZudLlw=="], - "vite/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.27.7", "", { "os": "linux", "cpu": "arm" }, "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA=="], + "vite/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.28.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-RVyzfb3FWsGA55n6WY0MEIEPURL1FcbhFE6BffZEMEekfCzCIMtB5yyDcFnVbTnwk+CLAgTujmV/Lgvih56W+A=="], - "vite/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.27.7", "", { "os": "linux", "cpu": "arm64" }, "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A=="], + "vite/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.28.0", "", { "os": "linux", "cpu": "ia32" }, "sha512-KBnSTt1kxl9x70q+ydterVdl+Cn0H18ngRMRCEQfrbqdUuntQQ0LoMZv47uB97NljZFzY6HcfqEZ2SAyIUTQBQ=="], - "vite/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.27.7", "", { "os": "linux", "cpu": "ia32" }, "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg=="], + "vite/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.28.0", "", { "os": "linux", "cpu": "none" }, "sha512-zpSlUce1mnxzgBADvxKXX5sl8aYQHo2ezvMNI8I0lbblJtp8V4odlm3Yzlj7gPyt3T8ReksE6bK+pT3WD+aJRg=="], - "vite/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.27.7", "", { "os": "linux", "cpu": "none" }, "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q=="], + "vite/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.28.0", "", { "os": "linux", "cpu": "none" }, "sha512-2jIfP6mmjkdmeTlsX/9vmdmhBmKADrWqN7zcdtHIeNSCH1SqIoNI63cYsjQR8J+wGa4Y5izRcSHSm8K3QWmk3w=="], - "vite/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.27.7", "", { "os": "linux", "cpu": "none" }, "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw=="], + "vite/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.28.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-bc0FE9wWeC0WBm49IQMPSPILRocGTQt3j5KPCA8os6VprfuJ7KD+5PzESSrJ6GmPIPJK965ZJHTUlSA6GNYEhg=="], - "vite/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.27.7", "", { "os": "linux", "cpu": "ppc64" }, "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ=="], + "vite/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.28.0", "", { "os": "linux", "cpu": "none" }, "sha512-SQPZOwoTTT/HXFXQJG/vBX8sOFagGqvZyXcgLA3NhIqcBv1BJU1d46c0rGcrij2B56Z2rNiSLaZOYW5cUk7yLQ=="], - "vite/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.27.7", "", { "os": "linux", "cpu": "none" }, "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ=="], + "vite/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.28.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-SCfR0HN8CEEjnYnySJTd2cw0k9OHB/YFzt5zgJEwa+wL/T/raGWYMBqwDNAC6dqFKmJYZoQBRfHjgwLHGSrn3Q=="], - "vite/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.27.7", "", { "os": "linux", "cpu": "s390x" }, "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw=="], + "vite/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.28.0", "", { "os": "linux", "cpu": "x64" }, "sha512-us0dSb9iFxIi8srnpl931Nvs65it/Jd2a2K3qs7fz2WfGPHqzfzZTfec7oxZJRNPXPnNYZtanmRc4AL/JwVzHQ=="], - "vite/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.27.7", "", { "os": "linux", "cpu": "x64" }, "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA=="], + "vite/esbuild/@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.28.0", "", { "os": "none", "cpu": "arm64" }, "sha512-CR/RYotgtCKwtftMwJlUU7xCVNg3lMYZ0RzTmAHSfLCXw3NtZtNpswLEj/Kkf6kEL3Gw+BpOekRX0BYCtklhUw=="], - "vite/esbuild/@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.27.7", "", { "os": "none", "cpu": "arm64" }, "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w=="], + "vite/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.28.0", "", { "os": "none", "cpu": "x64" }, "sha512-nU1yhmYutL+fQ71Kxnhg8uEOdC0pwEW9entHykTgEbna2pw2dkbFSMeqjjyHZoCmt8SBkOSvV+yNmm94aUrrqw=="], - "vite/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.27.7", "", { "os": "none", "cpu": "x64" }, "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw=="], + "vite/esbuild/@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.28.0", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-cXb5vApOsRsxsEl4mcZ1XY3D4DzcoMxR/nnc4IyqYs0rTI8ZKmW6kyyg+11Z8yvgMfAEldKzP7AdP64HnSC/6g=="], - "vite/esbuild/@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.27.7", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A=="], + "vite/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.28.0", "", { "os": "openbsd", "cpu": "x64" }, "sha512-8wZM2qqtv9UP3mzy7HiGYNH/zjTA355mpeuA+859TyR+e+Tc08IHYpLJuMsfpDJwoLo1ikIJI8jC3GFjnRClzA=="], - "vite/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.27.7", "", { "os": "openbsd", "cpu": "x64" }, "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg=="], + "vite/esbuild/@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.28.0", "", { "os": "none", "cpu": "arm64" }, "sha512-FLGfyizszcef5C3YtoyQDACyg95+dndv79i2EekILBofh5wpCa1KuBqOWKrEHZg3zrL3t5ouE5jgr94vA+Wb2w=="], - "vite/esbuild/@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.27.7", "", { "os": "none", "cpu": "arm64" }, "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw=="], + "vite/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.28.0", "", { "os": "sunos", "cpu": "x64" }, "sha512-1ZgjUoEdHZZl/YlV76TSCz9Hqj9h9YmMGAgAPYd+q4SicWNX3G5GCyx9uhQWSLcbvPW8Ni7lj4gDa1T40akdlw=="], - "vite/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.27.7", "", { "os": "sunos", "cpu": "x64" }, "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA=="], + "vite/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.28.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-Q9StnDmQ/enxnpxCCLSg0oo4+34B9TdXpuyPeTedN/6+iXBJ4J+zwfQI28u/Jl40nOYAxGoNi7mFP40RUtkmUA=="], - "vite/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.27.7", "", { "os": "win32", "cpu": "arm64" }, "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA=="], + "vite/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.28.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-zF3ag/gfiCe6U2iczcRzSYJKH1DCI+ByzSENHlM2FcDbEeo5Zd2C86Aq0tKUYAJJ1obRP84ymxIAksZUcdztHA=="], - "vite/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.27.7", "", { "os": "win32", "cpu": "ia32" }, "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw=="], + "vite/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.28.0", "", { "os": "win32", "cpu": "x64" }, "sha512-pEl1bO9mfAmIC+tW5btTmrKaujg3zGtUmWNdCw/xs70FBjwAL3o9OEKNHvNmnyylD6ubxUERiEhdsL0xBQ9efw=="], - "vite/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.7", "", { "os": "win32", "cpu": "x64" }, "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg=="], + "zip-stream/readable-stream/string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="], "@electron/asar/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], @@ -1359,14 +1951,22 @@ "app-builder-lib/@electron/get/fs-extra/universalify": ["universalify@0.1.2", "", {}, "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg=="], - "cacache/glob/jackspeak/@isaacs/cliui": ["@isaacs/cliui@8.0.2", "", { "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", "strip-ansi": "^7.0.1", "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", "wrap-ansi": "^8.1.0", "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" } }, "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA=="], + "archiver/readable-stream/string_decoder/safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], + + "are-we-there-yet/readable-stream/string_decoder/safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], + + "bl/readable-stream/string_decoder/safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], "cacache/glob/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], + "compress-commons/readable-stream/string_decoder/safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], + "config-file-ts/glob/jackspeak/@isaacs/cliui": ["@isaacs/cliui@8.0.2", "", { "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", "strip-ansi": "^7.0.1", "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", "wrap-ansi": "^8.1.0", "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" } }, "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA=="], "config-file-ts/glob/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], + "crc32-stream/readable-stream/string_decoder/safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], + "dir-compare/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], "electron-builder-squirrel-windows/app-builder-lib/@electron/osx-sign/isbinaryfile": ["isbinaryfile@4.0.10", "", {}, "sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw=="], @@ -1375,31 +1975,37 @@ "electron-builder-squirrel-windows/app-builder-lib/@electron/rebuild/node-gyp": ["node-gyp@9.4.1", "", { "dependencies": { "env-paths": "^2.2.0", "exponential-backoff": "^3.1.1", "glob": "^7.1.4", "graceful-fs": "^4.2.6", "make-fetch-happen": "^10.0.3", "nopt": "^6.0.0", "npmlog": "^6.0.0", "rimraf": "^3.0.2", "semver": "^7.3.5", "tar": "^6.1.2", "which": "^2.0.2" }, "bin": "bin/node-gyp.js" }, "sha512-OQkWKbjQKbGkMf/xqI1jjy3oCTgMKJac58G2+bjZb3fza6gW2YrCSdMQYaoTb70crvE//Gngr4f0AgVHmqHvBQ=="], + "electron-builder-squirrel-windows/app-builder-lib/@electron/rebuild/ora": ["ora@5.4.1", "", { "dependencies": { "bl": "^4.1.0", "chalk": "^4.1.0", "cli-cursor": "^3.1.0", "cli-spinners": "^2.5.0", "is-interactive": "^1.0.0", "is-unicode-supported": "^0.1.0", "log-symbols": "^4.1.0", "strip-ansi": "^6.0.0", "wcwidth": "^1.0.1" } }, "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ=="], + "electron-builder-squirrel-windows/app-builder-lib/@electron/universal/fs-extra": ["fs-extra@11.3.3", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-VWSRii4t0AFm6ixFFmLLx1t7wS1gh+ckoa84aOeapGum0h+EZd1EhEumSB+ZdDLnEPuucsVB9oB7cxJHap6Afg=="], "electron-builder-squirrel-windows/app-builder-lib/@electron/universal/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], - "electron-builder-squirrel-windows/app-builder-lib/tar/chownr": ["chownr@2.0.0", "", {}, "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ=="], + "electron-builder-squirrel-windows/app-builder-lib/minimatch/brace-expansion": ["brace-expansion@5.0.2", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-Pdk8c9poy+YhOgVWw1JNN22/HcivgKWwpxKq04M/jTmHyCZn12WPJebZxdjSa5TmBqISrUSgNYU3eRORljfCCw=="], - "electron-builder-squirrel-windows/app-builder-lib/tar/minipass": ["minipass@5.0.0", "", {}, "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ=="], + "eslint-plugin-react-hooks/@babel/parser/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.29.7", "", {}, "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw=="], - "electron-builder-squirrel-windows/app-builder-lib/tar/minizlib": ["minizlib@2.1.2", "", { "dependencies": { "minipass": "^3.0.0", "yallist": "^4.0.0" } }, "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg=="], + "eslint-plugin-react-hooks/@babel/parser/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="], - "electron-builder-squirrel-windows/app-builder-lib/tar/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="], + "eslint/find-up/locate-path/p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="], "filelist/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], "glob/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + "magicast/@babel/parser/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.29.7", "", {}, "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw=="], + + "magicast/@babel/parser/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="], + + "ora/string-width/strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="], + "qrcode/yargs/cliui/wrap-ansi": ["wrap-ansi@6.2.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA=="], "readdir-glob/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], - "cacache/glob/jackspeak/@isaacs/cliui/string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="], + "tar-stream/readable-stream/string_decoder/safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], - "cacache/glob/jackspeak/@isaacs/cliui/strip-ansi": ["strip-ansi@7.1.2", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA=="], - - "cacache/glob/jackspeak/@isaacs/cliui/wrap-ansi": ["wrap-ansi@8.1.0", "", { "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", "strip-ansi": "^7.0.1" } }, "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ=="], + "zip-stream/readable-stream/string_decoder/safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], "cacache/glob/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], @@ -1413,19 +2019,19 @@ "electron-builder-squirrel-windows/app-builder-lib/@electron/rebuild/node-gyp/env-paths": ["env-paths@2.2.1", "", {}, "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A=="], - "electron-builder-squirrel-windows/app-builder-lib/@electron/rebuild/node-gyp/make-fetch-happen": ["make-fetch-happen@10.2.1", "", { "dependencies": { "agentkeepalive": "^4.2.1", "cacache": "^16.1.0", "http-cache-semantics": "^4.1.0", "http-proxy-agent": "^5.0.0", "https-proxy-agent": "^5.0.0", "is-lambda": "^1.0.1", "lru-cache": "^7.7.1", "minipass": "^3.1.6", "minipass-collect": "^1.0.2", "minipass-fetch": "^2.0.3", "minipass-flush": "^1.0.5", "minipass-pipeline": "^1.2.4", "negotiator": "^0.6.3", "promise-retry": "^2.0.1", "socks-proxy-agent": "^7.0.0", "ssri": "^9.0.0" } }, "sha512-NgOPbRiaQM10DYXvN3/hhGVI2M5MtITFryzBGxHM5p4wnFxsVCbxkrBrDsk+EZ5OB4jEOT7AjDxtdF+KVEFT7w=="], - "electron-builder-squirrel-windows/app-builder-lib/@electron/rebuild/node-gyp/nopt": ["nopt@6.0.0", "", { "dependencies": { "abbrev": "^1.0.0" }, "bin": "bin/nopt.js" }, "sha512-ZwLpbTgdhuZUnZzjd7nb1ZV+4DoiC6/sfiVKok72ym/4Tlf+DFdlHYmT2JPmcNNWV6Pi3SDf1kT+A4r9RTuT9g=="], - "electron-builder-squirrel-windows/app-builder-lib/@electron/universal/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], + "electron-builder-squirrel-windows/app-builder-lib/@electron/rebuild/ora/cli-cursor": ["cli-cursor@3.1.0", "", { "dependencies": { "restore-cursor": "^3.1.0" } }, "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw=="], - "electron-builder-squirrel-windows/app-builder-lib/tar/minizlib/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="], + "electron-builder-squirrel-windows/app-builder-lib/@electron/rebuild/ora/cli-spinners": ["cli-spinners@2.9.2", "", {}, "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg=="], - "cacache/glob/jackspeak/@isaacs/cliui/string-width/emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="], + "electron-builder-squirrel-windows/app-builder-lib/@electron/rebuild/ora/is-interactive": ["is-interactive@1.0.0", "", {}, "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w=="], - "cacache/glob/jackspeak/@isaacs/cliui/strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="], + "electron-builder-squirrel-windows/app-builder-lib/@electron/rebuild/ora/is-unicode-supported": ["is-unicode-supported@0.1.0", "", {}, "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw=="], - "cacache/glob/jackspeak/@isaacs/cliui/wrap-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="], + "electron-builder-squirrel-windows/app-builder-lib/@electron/rebuild/ora/log-symbols": ["log-symbols@4.1.0", "", { "dependencies": { "chalk": "^4.1.0", "is-unicode-supported": "^0.1.0" } }, "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg=="], + + "electron-builder-squirrel-windows/app-builder-lib/@electron/universal/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], "config-file-ts/glob/jackspeak/@isaacs/cliui/string-width/emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="], @@ -1433,58 +2039,12 @@ "config-file-ts/glob/jackspeak/@isaacs/cliui/wrap-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="], - "electron-builder-squirrel-windows/app-builder-lib/@electron/rebuild/node-gyp/make-fetch-happen/cacache": ["cacache@16.1.3", "", { "dependencies": { "@npmcli/fs": "^2.1.0", "@npmcli/move-file": "^2.0.0", "chownr": "^2.0.0", "fs-minipass": "^2.1.0", "glob": "^8.0.1", "infer-owner": "^1.0.4", "lru-cache": "^7.7.1", "minipass": "^3.1.6", "minipass-collect": "^1.0.2", "minipass-flush": "^1.0.5", "minipass-pipeline": "^1.2.4", "mkdirp": "^1.0.4", "p-map": "^4.0.0", "promise-inflight": "^1.0.1", "rimraf": "^3.0.2", "ssri": "^9.0.0", "tar": "^6.1.11", "unique-filename": "^2.0.0" } }, "sha512-/+Emcj9DAXxX4cwlLmRI9c166RuL3w30zp4R7Joiv2cQTtTtA+jeuCAjH3ZlGnYS3tKENSrKhAzVVP9GVyzeYQ=="], - - "electron-builder-squirrel-windows/app-builder-lib/@electron/rebuild/node-gyp/make-fetch-happen/http-proxy-agent": ["http-proxy-agent@5.0.0", "", { "dependencies": { "@tootallnate/once": "2", "agent-base": "6", "debug": "4" } }, "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w=="], - - "electron-builder-squirrel-windows/app-builder-lib/@electron/rebuild/node-gyp/make-fetch-happen/https-proxy-agent": ["https-proxy-agent@5.0.1", "", { "dependencies": { "agent-base": "6", "debug": "4" } }, "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA=="], - - "electron-builder-squirrel-windows/app-builder-lib/@electron/rebuild/node-gyp/make-fetch-happen/lru-cache": ["lru-cache@7.18.3", "", {}, "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA=="], - - "electron-builder-squirrel-windows/app-builder-lib/@electron/rebuild/node-gyp/make-fetch-happen/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="], - - "electron-builder-squirrel-windows/app-builder-lib/@electron/rebuild/node-gyp/make-fetch-happen/minipass-collect": ["minipass-collect@1.0.2", "", { "dependencies": { "minipass": "^3.0.0" } }, "sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA=="], - - "electron-builder-squirrel-windows/app-builder-lib/@electron/rebuild/node-gyp/make-fetch-happen/minipass-fetch": ["minipass-fetch@2.1.2", "", { "dependencies": { "minipass": "^3.1.6", "minipass-sized": "^1.0.3", "minizlib": "^2.1.2" }, "optionalDependencies": { "encoding": "^0.1.13" } }, "sha512-LT49Zi2/WMROHYoqGgdlQIZh8mLPZmOrN2NdJjMXxYe4nkN6FUyuPuOAOedNJDrx0IRGg9+4guZewtp8hE6TxA=="], - - "electron-builder-squirrel-windows/app-builder-lib/@electron/rebuild/node-gyp/make-fetch-happen/negotiator": ["negotiator@0.6.4", "", {}, "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w=="], - - "electron-builder-squirrel-windows/app-builder-lib/@electron/rebuild/node-gyp/make-fetch-happen/socks-proxy-agent": ["socks-proxy-agent@7.0.0", "", { "dependencies": { "agent-base": "^6.0.2", "debug": "^4.3.3", "socks": "^2.6.2" } }, "sha512-Fgl0YPZ902wEsAyiQ+idGd1A7rSFx/ayC1CQVMw5P+EQx2V0SgpGtf6OKFhVjPflPUl9YMmEOnmfjCdMUsygww=="], - - "electron-builder-squirrel-windows/app-builder-lib/@electron/rebuild/node-gyp/make-fetch-happen/ssri": ["ssri@9.0.1", "", { "dependencies": { "minipass": "^3.1.1" } }, "sha512-o57Wcn66jMQvfHG1FlYbWeZWW/dHZhJXjpIcTfXldXEk5nz5lStPo3mK0OJQfGR3RbZUlbISexbljkJzuEj/8Q=="], - "electron-builder-squirrel-windows/app-builder-lib/@electron/rebuild/node-gyp/nopt/abbrev": ["abbrev@1.1.1", "", {}, "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q=="], - "electron-builder-squirrel-windows/app-builder-lib/@electron/universal/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], - - "electron-builder-squirrel-windows/app-builder-lib/@electron/rebuild/node-gyp/make-fetch-happen/cacache/@npmcli/fs": ["@npmcli/fs@2.1.2", "", { "dependencies": { "@gar/promisify": "^1.1.3", "semver": "^7.3.5" } }, "sha512-yOJKRvohFOaLqipNtwYB9WugyZKhC/DZC4VYPmpaCzDBrA8YpK3qHZ8/HGscMnE4GqbkLNuVcCnxkeQEdGt6LQ=="], - - "electron-builder-squirrel-windows/app-builder-lib/@electron/rebuild/node-gyp/make-fetch-happen/cacache/chownr": ["chownr@2.0.0", "", {}, "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ=="], - - "electron-builder-squirrel-windows/app-builder-lib/@electron/rebuild/node-gyp/make-fetch-happen/cacache/glob": ["glob@8.1.0", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^5.0.1", "once": "^1.3.0" } }, "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ=="], - - "electron-builder-squirrel-windows/app-builder-lib/@electron/rebuild/node-gyp/make-fetch-happen/cacache/p-map": ["p-map@4.0.0", "", { "dependencies": { "aggregate-error": "^3.0.0" } }, "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ=="], + "electron-builder-squirrel-windows/app-builder-lib/@electron/rebuild/ora/cli-cursor/restore-cursor": ["restore-cursor@3.1.0", "", { "dependencies": { "onetime": "^5.1.0", "signal-exit": "^3.0.2" } }, "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA=="], - "electron-builder-squirrel-windows/app-builder-lib/@electron/rebuild/node-gyp/make-fetch-happen/cacache/unique-filename": ["unique-filename@2.0.1", "", { "dependencies": { "unique-slug": "^3.0.0" } }, "sha512-ODWHtkkdx3IAR+veKxFV+VBkUMcN+FaqzUUd7IZzt+0zhDZFPFxhlqwPF3YQvMHx1TD0tdgYl+kuPnJ8E6ql7A=="], - - "electron-builder-squirrel-windows/app-builder-lib/@electron/rebuild/node-gyp/make-fetch-happen/http-proxy-agent/agent-base": ["agent-base@6.0.2", "", { "dependencies": { "debug": "4" } }, "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ=="], - - "electron-builder-squirrel-windows/app-builder-lib/@electron/rebuild/node-gyp/make-fetch-happen/https-proxy-agent/agent-base": ["agent-base@6.0.2", "", { "dependencies": { "debug": "4" } }, "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ=="], - - "electron-builder-squirrel-windows/app-builder-lib/@electron/rebuild/node-gyp/make-fetch-happen/minipass/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="], - - "electron-builder-squirrel-windows/app-builder-lib/@electron/rebuild/node-gyp/make-fetch-happen/minipass-fetch/minizlib": ["minizlib@2.1.2", "", { "dependencies": { "minipass": "^3.0.0", "yallist": "^4.0.0" } }, "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg=="], - - "electron-builder-squirrel-windows/app-builder-lib/@electron/rebuild/node-gyp/make-fetch-happen/socks-proxy-agent/agent-base": ["agent-base@6.0.2", "", { "dependencies": { "debug": "4" } }, "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ=="], - - "electron-builder-squirrel-windows/app-builder-lib/@electron/rebuild/node-gyp/make-fetch-happen/cacache/glob/minimatch": ["minimatch@5.1.6", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g=="], - - "electron-builder-squirrel-windows/app-builder-lib/@electron/rebuild/node-gyp/make-fetch-happen/cacache/unique-filename/unique-slug": ["unique-slug@3.0.0", "", { "dependencies": { "imurmurhash": "^0.1.4" } }, "sha512-8EyMynh679x/0gqE9fT9oilG+qEt+ibFyqjuVTsZn1+CMxH+XLlpvr2UZx4nVcCwTpx81nICr2JQFkM+HPLq4w=="], - - "electron-builder-squirrel-windows/app-builder-lib/@electron/rebuild/node-gyp/make-fetch-happen/minipass-fetch/minizlib/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="], - - "electron-builder-squirrel-windows/app-builder-lib/@electron/rebuild/node-gyp/make-fetch-happen/cacache/glob/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], + "electron-builder-squirrel-windows/app-builder-lib/@electron/universal/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], - "electron-builder-squirrel-windows/app-builder-lib/@electron/rebuild/node-gyp/make-fetch-happen/cacache/glob/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + "electron-builder-squirrel-windows/app-builder-lib/@electron/rebuild/ora/cli-cursor/restore-cursor/onetime": ["onetime@5.1.2", "", { "dependencies": { "mimic-fn": "^2.1.0" } }, "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg=="], } } diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/concrt140.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/concrt140.dll new file mode 100644 index 000000000..830dfaeaf Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/concrt140.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gio-2.0-0.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gio-2.0-0.dll new file mode 100644 index 000000000..56235522e Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gio-2.0-0.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/glib-2.0-0.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/glib-2.0-0.dll new file mode 100644 index 000000000..a9e79cd53 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/glib-2.0-0.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gmodule-2.0-0.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gmodule-2.0-0.dll new file mode 100644 index 000000000..f9712b7cd Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gmodule-2.0-0.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gobject-2.0-0.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gobject-2.0-0.dll new file mode 100644 index 000000000..33f81a469 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gobject-2.0-0.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer-1.0-0.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer-1.0-0.dll new file mode 100644 index 000000000..66afb1858 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer-1.0-0.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/OPENNOW-GSTREAMER-RUNTIME.txt b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/OPENNOW-GSTREAMER-RUNTIME.txt new file mode 100644 index 000000000..3135dca61 --- /dev/null +++ b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/OPENNOW-GSTREAMER-RUNTIME.txt @@ -0,0 +1,7 @@ +OpenNOW private GStreamer runtime bundle +Source: C:\Program Files\gstreamer\1.0\msvc_x86_64 +Generated: 2026-06-30T13:34:35.902Z +Platform: win32 +Scope: native streamer child process only + +This directory is loaded only for the native streamer child process. Keep the private layout intact. diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/FLAC-8.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/FLAC-8.dll new file mode 100644 index 000000000..95227040b Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/FLAC-8.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/SvtAv1Enc.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/SvtAv1Enc.dll new file mode 100644 index 000000000..9dd3d0cfd Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/SvtAv1Enc.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/SvtJpegxs.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/SvtJpegxs.dll new file mode 100644 index 000000000..bea4c0fee Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/SvtJpegxs.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/accesskit-c-0.17.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/accesskit-c-0.17.dll new file mode 100644 index 000000000..bed220e35 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/accesskit-c-0.17.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/ass-9.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/ass-9.dll new file mode 100644 index 000000000..0fd9a5ceb Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/ass-9.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/avcodec-61.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/avcodec-61.dll new file mode 100644 index 000000000..c949370bf Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/avcodec-61.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/avfilter-10.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/avfilter-10.dll new file mode 100644 index 000000000..f98956b98 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/avfilter-10.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/avformat-61.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/avformat-61.dll new file mode 100644 index 000000000..95cf742b2 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/avformat-61.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/avutil-59.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/avutil-59.dll new file mode 100644 index 000000000..c113530a2 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/avutil-59.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/bz2.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/bz2.dll new file mode 100644 index 000000000..d16161f30 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/bz2.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/cairo-2.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/cairo-2.dll new file mode 100644 index 000000000..044112338 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/cairo-2.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/cairo-gobject-2.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/cairo-gobject-2.dll new file mode 100644 index 000000000..600f8f053 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/cairo-gobject-2.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/cairo-script-interpreter-2.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/cairo-script-interpreter-2.dll new file mode 100644 index 000000000..b850224af Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/cairo-script-interpreter-2.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/concrt140.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/concrt140.dll new file mode 100644 index 000000000..830dfaeaf Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/concrt140.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/dav1d.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/dav1d.dll new file mode 100644 index 000000000..90618b088 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/dav1d.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/dca-0.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/dca-0.dll new file mode 100644 index 000000000..bffffe9fa Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/dca-0.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/dv-4.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/dv-4.dll new file mode 100644 index 000000000..95c0114d4 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/dv-4.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/dvdnav-4.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/dvdnav-4.dll new file mode 100644 index 000000000..ccecadc00 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/dvdnav-4.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/dvdread-8.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/dvdread-8.dll new file mode 100644 index 000000000..935a9c546 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/dvdread-8.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/epoxy-0.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/epoxy-0.dll new file mode 100644 index 000000000..7d4b5aab6 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/epoxy-0.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/ffi-7.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/ffi-7.dll new file mode 100644 index 000000000..c443a4ac3 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/ffi-7.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/fmt.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/fmt.dll new file mode 100644 index 000000000..eb77ff122 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/fmt.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/fontconfig-1.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/fontconfig-1.dll new file mode 100644 index 000000000..614b988fb Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/fontconfig-1.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/freetype-6.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/freetype-6.dll new file mode 100644 index 000000000..8323309db Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/freetype-6.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/fribidi-0.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/fribidi-0.dll new file mode 100644 index 000000000..a831cbaeb Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/fribidi-0.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/g-ir-annotation-tool b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/g-ir-annotation-tool new file mode 100644 index 000000000..e6b73a389 --- /dev/null +++ b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/g-ir-annotation-tool @@ -0,0 +1,134 @@ +#!/usr/bin/env C:/Users/nirbheek/AppData/Local/Python/pythoncore-3.9-64/python.exe +# -*- Mode: Python -*- +# GObject-Introspection - a framework for introspecting GObject libraries +# Copyright (C) 2008 Johan Dahlin +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +# 02110-1301, USA. +# + +import os +import sys +import sysconfig +import builtins + + +debug = os.getenv('GI_SCANNER_DEBUG') +if debug: + if 'pydevd' in debug.split(','): + # http://pydev.org/manual_adv_remote_debugger.html + pydevdpath = os.getenv('PYDEVDPATH', None) + if pydevdpath is not None and os.path.isdir(pydevdpath): + sys.path.insert(0, pydevdpath) + import pydevd + pydevd.settrace() + else: + def on_exception(exctype, value, tb): + print("Caught exception: %r %r" % (exctype, value)) + import pdb + pdb.pm() + sys.excepthook = on_exception + +# Detect and set datadir, pylibdir, etc as applicable +# Similar to the method used in gdbus-codegen +filedir = os.path.dirname(__file__) + +# Try using relative paths first so that the installation prefix is relocatable +datadir = os.path.abspath(os.path.join(filedir, '..', 'share')) +# Fallback to hard-coded paths if the relocatable paths are wrong +if not os.path.isdir(os.path.join(datadir, 'gir-1.0')): + datadir = "C:/projects/repos/cerbero.git/1.28/build/dist/msvc_x86_64/share" + +builtins.__dict__['DATADIR'] = datadir + +gir_dir = os.path.abspath(os.path.join(filedir, '..', 'share', 'gir-1.0')) +# Fallback to hard-coded paths if the relocatable paths are wrong +if not os.path.isdir(gir_dir): + gir_dir = "C:/projects/repos/cerbero.git/1.28/build/dist/msvc_x86_64/share/gir-1.0" + +builtins.__dict__['GIR_DIR'] = gir_dir + +# Again, relative paths first so that the installation prefix is relocatable +pylibdir = os.path.abspath(os.path.join(filedir, '..', 'lib', 'gobject-introspection')) + +# EXT_SUFFIX for py3 SO for py2 +py_mod_suffix = sysconfig.get_config_var('EXT_SUFFIX') or sysconfig.get_config_var('SO') + +if not os.path.isfile(os.path.join(pylibdir, 'giscanner', '_giscanner' + py_mod_suffix)): + # Running uninstalled? + builddir = os.getenv('UNINSTALLED_INTROSPECTION_BUILDDIR', None) + if builddir is not None: + # Autotools, most likely + builddir = os.path.abspath(builddir) + # For _giscanner.so + sys.path.insert(0, os.path.join(builddir, '.libs')) + srcdir = os.getenv('UNINSTALLED_INTROSPECTION_SRCDIR', None) + if srcdir: + # For the giscanner python files + pylibdir = srcdir + elif os.path.isdir(os.path.join(filedir, '..', 'giscanner')): + # We're running uninstalled inside meson + builddir = os.path.abspath(os.path.join(filedir, '..')) + pylibdir = builddir + + if 'GI_GIR_PATH' not in os.environ: + os.environ['GI_GIR_PATH'] = os.path.join(filedir, os.pardir, 'gir') + + gdump_path = os.path.join(builddir, 'giscanner', 'gdump.c') + if os.path.isfile(gdump_path): + builtins.__dict__['GDUMP_PATH'] = gdump_path + else: + # Okay, we're not running uninstalled and the prefix is not + # relocatable. Use hard-coded libdir. + pylibdir = os.path.join('C:/projects/repos/cerbero.git/1.28/build/dist/msvc_x86_64/lib', 'gobject-introspection') + +sys.path.insert(0, pylibdir) + +from giscanner.utils import dll_dirs +dll_dirs = dll_dirs() +dll_dirs.add_dll_dirs(['gio-2.0']) + +def get_rspfile_args(rspfile): + ''' + Response files are useful on Windows where there is a command-line character + limit of 8191 because when passing sources as arguments to glib-mkenums this + limit can be exceeded in large codebases. + + There is no specification for response files and each tool that supports it + generally writes them out in slightly different ways, but some sources are: + https://docs.microsoft.com/en-us/visualstudio/msbuild/msbuild-response-files + https://docs.microsoft.com/en-us/windows/desktop/midl/the-response-file-command + ''' + import shlex + if not os.path.isfile(rspfile): + sys.exit('Response file {!r} does not exist'.format(rspfile)) + try: + with open(rspfile, 'r') as f: + cmdline = f.read() + except OSError as e: + sys.exit('Response file {!r} could not be read: {}' + .format(rspfile, e.strerror)) + return shlex.split(cmdline) + + +# Support reading an rspfile of the form @filename which contains the args +# to be parsed +if sys.argv[-1].startswith('@'): + args = sys.argv[0:-1] + get_rspfile_args(sys.argv[-1][1:]) +else: + args = sys.argv + +from giscanner.annotationmain import annotation_main +sys.exit(annotation_main(args)) diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/g-ir-compiler.exe b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/g-ir-compiler.exe new file mode 100644 index 000000000..39bbc175e Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/g-ir-compiler.exe differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/g-ir-generate.exe b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/g-ir-generate.exe new file mode 100644 index 000000000..5e046b3c9 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/g-ir-generate.exe differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/g-ir-inspect.exe b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/g-ir-inspect.exe new file mode 100644 index 000000000..718fcab38 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/g-ir-inspect.exe differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/g-ir-scanner b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/g-ir-scanner new file mode 100644 index 000000000..ed58d9e1c --- /dev/null +++ b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/g-ir-scanner @@ -0,0 +1,134 @@ +#!/usr/bin/env C:/Users/nirbheek/AppData/Local/Python/pythoncore-3.9-64/python.exe +# -*- Mode: Python -*- +# GObject-Introspection - a framework for introspecting GObject libraries +# Copyright (C) 2008 Johan Dahlin +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +# 02110-1301, USA. +# + +import os +import sys +import sysconfig +import builtins + + +debug = os.getenv('GI_SCANNER_DEBUG') +if debug: + if 'pydevd' in debug.split(','): + # http://pydev.org/manual_adv_remote_debugger.html + pydevdpath = os.getenv('PYDEVDPATH', None) + if pydevdpath is not None and os.path.isdir(pydevdpath): + sys.path.insert(0, pydevdpath) + import pydevd + pydevd.settrace() + else: + def on_exception(exctype, value, tb): + print("Caught exception: %r %r" % (exctype, value)) + import pdb + pdb.pm() + sys.excepthook = on_exception + +# Detect and set datadir, pylibdir, etc as applicable +# Similar to the method used in gdbus-codegen +filedir = os.path.dirname(__file__) + +# Try using relative paths first so that the installation prefix is relocatable +datadir = os.path.abspath(os.path.join(filedir, '..', 'share')) +# Fallback to hard-coded paths if the relocatable paths are wrong +if not os.path.isdir(os.path.join(datadir, 'gir-1.0')): + datadir = "C:/projects/repos/cerbero.git/1.28/build/dist/msvc_x86_64/share" + +builtins.__dict__['DATADIR'] = datadir + +gir_dir = os.path.abspath(os.path.join(filedir, '..', 'share', 'gir-1.0')) +# Fallback to hard-coded paths if the relocatable paths are wrong +if not os.path.isdir(gir_dir): + gir_dir = "C:/projects/repos/cerbero.git/1.28/build/dist/msvc_x86_64/share/gir-1.0" + +builtins.__dict__['GIR_DIR'] = gir_dir + +# Again, relative paths first so that the installation prefix is relocatable +pylibdir = os.path.abspath(os.path.join(filedir, '..', 'lib', 'gobject-introspection')) + +# EXT_SUFFIX for py3 SO for py2 +py_mod_suffix = sysconfig.get_config_var('EXT_SUFFIX') or sysconfig.get_config_var('SO') + +if not os.path.isfile(os.path.join(pylibdir, 'giscanner', '_giscanner' + py_mod_suffix)): + # Running uninstalled? + builddir = os.getenv('UNINSTALLED_INTROSPECTION_BUILDDIR', None) + if builddir is not None: + # Autotools, most likely + builddir = os.path.abspath(builddir) + # For _giscanner.so + sys.path.insert(0, os.path.join(builddir, '.libs')) + srcdir = os.getenv('UNINSTALLED_INTROSPECTION_SRCDIR', None) + if srcdir: + # For the giscanner python files + pylibdir = srcdir + elif os.path.isdir(os.path.join(filedir, '..', 'giscanner')): + # We're running uninstalled inside meson + builddir = os.path.abspath(os.path.join(filedir, '..')) + pylibdir = builddir + + if 'GI_GIR_PATH' not in os.environ: + os.environ['GI_GIR_PATH'] = os.path.join(filedir, os.pardir, 'gir') + + gdump_path = os.path.join(builddir, 'giscanner', 'gdump.c') + if os.path.isfile(gdump_path): + builtins.__dict__['GDUMP_PATH'] = gdump_path + else: + # Okay, we're not running uninstalled and the prefix is not + # relocatable. Use hard-coded libdir. + pylibdir = os.path.join('C:/projects/repos/cerbero.git/1.28/build/dist/msvc_x86_64/lib', 'gobject-introspection') + +sys.path.insert(0, pylibdir) + +from giscanner.utils import dll_dirs +dll_dirs = dll_dirs() +dll_dirs.add_dll_dirs(['gio-2.0']) + +def get_rspfile_args(rspfile): + ''' + Response files are useful on Windows where there is a command-line character + limit of 8191 because when passing sources as arguments to glib-mkenums this + limit can be exceeded in large codebases. + + There is no specification for response files and each tool that supports it + generally writes them out in slightly different ways, but some sources are: + https://docs.microsoft.com/en-us/visualstudio/msbuild/msbuild-response-files + https://docs.microsoft.com/en-us/windows/desktop/midl/the-response-file-command + ''' + import shlex + if not os.path.isfile(rspfile): + sys.exit('Response file {!r} does not exist'.format(rspfile)) + try: + with open(rspfile, 'r') as f: + cmdline = f.read() + except OSError as e: + sys.exit('Response file {!r} could not be read: {}' + .format(rspfile, e.strerror)) + return shlex.split(cmdline) + + +# Support reading an rspfile of the form @filename which contains the args +# to be parsed +if sys.argv[-1].startswith('@'): + args = sys.argv[0:-1] + get_rspfile_args(sys.argv[-1][1:]) +else: + args = sys.argv + +from giscanner.scannermain import scanner_main +sys.exit(scanner_main(args)) diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gdbus-codegen b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gdbus-codegen new file mode 100644 index 000000000..b41ed6b37 --- /dev/null +++ b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gdbus-codegen @@ -0,0 +1,57 @@ +#!C:\projects\repos\cerbero.git\1.28\build\build-tools\bin\python.exe + +# GDBus - GLib D-Bus Library +# +# Copyright (C) 2008-2011 Red Hat, Inc. +# +# SPDX-License-Identifier: LGPL-2.1-or-later +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General +# Public License along with this library; if not, see . +# +# Author: David Zeuthen + + +import os +import sys + +srcdir = os.getenv('UNINSTALLED_GLIB_SRCDIR', None) +filedir = os.path.dirname(__file__) + +if srcdir is not None: + path = os.path.join(srcdir, 'gio', 'gdbus-2.0') +elif os.path.basename(filedir) == 'bin': + # Make the prefix containing gdbus-codegen 'relocatable' at runtime by + # adding /some/prefix/bin/../share/glib-2.0 to the python path + path = os.path.join(filedir, '..', 'share', 'glib-2.0') +else: + # Assume that the modules we need are in the current directory and add the + # parent directory to the python path. + path = os.path.join(filedir, '..') + +# Canonicalize, then do further testing +path = os.path.abspath(path) + +# If the above path detection failed, use the hard-coded datadir. This can +# happen when, for instance, bindir and datadir are not in the same prefix or +# on Windows where we cannot make any guarantees about the directory structure. +# +# In these cases our installation cannot be relocatable, but at least we should +# be able to find the codegen module. +if not os.path.isfile(os.path.join(path, 'codegen', 'codegen_main.py')): + path = os.path.join('C:/projects/repos/cerbero.git/1.28/build/dist/msvc_x86_64/share', 'glib-2.0') + +sys.path.insert(0, path) +from codegen import codegen_main + +sys.exit(codegen_main.codegen_main()) diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gdbus.exe b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gdbus.exe new file mode 100644 index 000000000..30163f69b Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gdbus.exe differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gdk-pixbuf-csource.exe b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gdk-pixbuf-csource.exe new file mode 100644 index 000000000..b87483411 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gdk-pixbuf-csource.exe differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gdk-pixbuf-query-loaders.exe b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gdk-pixbuf-query-loaders.exe new file mode 100644 index 000000000..1407281d8 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gdk-pixbuf-query-loaders.exe differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gdk_pixbuf-2.0-0.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gdk_pixbuf-2.0-0.dll new file mode 100644 index 000000000..fa787363c Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gdk_pixbuf-2.0-0.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/ges-1.0-0.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/ges-1.0-0.dll new file mode 100644 index 000000000..7a3f44e87 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/ges-1.0-0.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/ges-launch-1.0.exe b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/ges-launch-1.0.exe new file mode 100644 index 000000000..ee2b8e9a8 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/ges-launch-1.0.exe differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gio-2.0-0.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gio-2.0-0.dll new file mode 100644 index 000000000..56235522e Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gio-2.0-0.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gio-querymodules.exe b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gio-querymodules.exe new file mode 100644 index 000000000..ebc9a14cf Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gio-querymodules.exe differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/girepository-1.0-1.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/girepository-1.0-1.dll new file mode 100644 index 000000000..ada1ff718 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/girepository-1.0-1.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/glib-2.0-0.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/glib-2.0-0.dll new file mode 100644 index 000000000..a9e79cd53 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/glib-2.0-0.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/glib-compile-resources.exe b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/glib-compile-resources.exe new file mode 100644 index 000000000..003d72a31 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/glib-compile-resources.exe differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/glib-compile-schemas.exe b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/glib-compile-schemas.exe new file mode 100644 index 000000000..5bfbd6396 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/glib-compile-schemas.exe differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/glib-genmarshal b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/glib-genmarshal new file mode 100644 index 000000000..bc3fe55ac --- /dev/null +++ b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/glib-genmarshal @@ -0,0 +1,1080 @@ +#!C:\projects\repos\cerbero.git\1.28\build\build-tools\bin\python.exe + +# pylint: disable=too-many-lines, missing-docstring, invalid-name + +# This file is part of GLib +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, see . + +import argparse +import os +import re +import sys + +VERSION_STR = '''glib-genmarshal version 2.82.4 +glib-genmarshal comes with ABSOLUTELY NO WARRANTY. +You may redistribute copies of glib-genmarshal under the terms of +the GNU General Public License which can be found in the +GLib source package. Sources, examples and contact +information are available at http://www.gtk.org''' + +GETTERS_STR = '''#ifdef G_ENABLE_DEBUG +#define g_marshal_value_peek_boolean(v) g_value_get_boolean (v) +#define g_marshal_value_peek_char(v) g_value_get_schar (v) +#define g_marshal_value_peek_uchar(v) g_value_get_uchar (v) +#define g_marshal_value_peek_int(v) g_value_get_int (v) +#define g_marshal_value_peek_uint(v) g_value_get_uint (v) +#define g_marshal_value_peek_long(v) g_value_get_long (v) +#define g_marshal_value_peek_ulong(v) g_value_get_ulong (v) +#define g_marshal_value_peek_int64(v) g_value_get_int64 (v) +#define g_marshal_value_peek_uint64(v) g_value_get_uint64 (v) +#define g_marshal_value_peek_enum(v) g_value_get_enum (v) +#define g_marshal_value_peek_flags(v) g_value_get_flags (v) +#define g_marshal_value_peek_float(v) g_value_get_float (v) +#define g_marshal_value_peek_double(v) g_value_get_double (v) +#define g_marshal_value_peek_string(v) (char*) g_value_get_string (v) +#define g_marshal_value_peek_param(v) g_value_get_param (v) +#define g_marshal_value_peek_boxed(v) g_value_get_boxed (v) +#define g_marshal_value_peek_pointer(v) g_value_get_pointer (v) +#define g_marshal_value_peek_object(v) g_value_get_object (v) +#define g_marshal_value_peek_variant(v) g_value_get_variant (v) +#else /* !G_ENABLE_DEBUG */ +/* WARNING: This code accesses GValues directly, which is UNSUPPORTED API. + * Do not access GValues directly in your code. Instead, use the + * g_value_get_*() functions + */ +#define g_marshal_value_peek_boolean(v) (v)->data[0].v_int +#define g_marshal_value_peek_char(v) (v)->data[0].v_int +#define g_marshal_value_peek_uchar(v) (v)->data[0].v_uint +#define g_marshal_value_peek_int(v) (v)->data[0].v_int +#define g_marshal_value_peek_uint(v) (v)->data[0].v_uint +#define g_marshal_value_peek_long(v) (v)->data[0].v_long +#define g_marshal_value_peek_ulong(v) (v)->data[0].v_ulong +#define g_marshal_value_peek_int64(v) (v)->data[0].v_int64 +#define g_marshal_value_peek_uint64(v) (v)->data[0].v_uint64 +#define g_marshal_value_peek_enum(v) (v)->data[0].v_long +#define g_marshal_value_peek_flags(v) (v)->data[0].v_ulong +#define g_marshal_value_peek_float(v) (v)->data[0].v_float +#define g_marshal_value_peek_double(v) (v)->data[0].v_double +#define g_marshal_value_peek_string(v) (v)->data[0].v_pointer +#define g_marshal_value_peek_param(v) (v)->data[0].v_pointer +#define g_marshal_value_peek_boxed(v) (v)->data[0].v_pointer +#define g_marshal_value_peek_pointer(v) (v)->data[0].v_pointer +#define g_marshal_value_peek_object(v) (v)->data[0].v_pointer +#define g_marshal_value_peek_variant(v) (v)->data[0].v_pointer +#endif /* !G_ENABLE_DEBUG */''' + +DEPRECATED_MSG_STR = 'The token "{}" is deprecated; use "{}" instead' + +VA_ARG_STR = \ + ' arg{:d} = ({:s}) va_arg (args_copy, {:s});' +STATIC_CHECK_STR = \ + '(param_types[{:d}] & G_SIGNAL_TYPE_STATIC_SCOPE) == 0 && ' +BOX_TYPED_STR = \ + ' arg{idx:d} = {box_func} (param_types[{idx:d}] & ~G_SIGNAL_TYPE_STATIC_SCOPE, arg{idx:d});' +BOX_UNTYPED_STR = \ + ' arg{idx:d} = {box_func} (arg{idx:d});' +UNBOX_TYPED_STR = \ + ' {unbox_func} (param_types[{idx:d}] & ~G_SIGNAL_TYPE_STATIC_SCOPE, arg{idx:d});' +UNBOX_UNTYPED_STR = \ + ' {unbox_func} (arg{idx:d});' + +STD_PREFIX = 'g_cclosure_marshal' + +# These are part of our ABI; keep this in sync with gmarshal.h +GOBJECT_MARSHALLERS = { + 'g_cclosure_marshal_VOID__VOID', + 'g_cclosure_marshal_VOID__BOOLEAN', + 'g_cclosure_marshal_VOID__CHAR', + 'g_cclosure_marshal_VOID__UCHAR', + 'g_cclosure_marshal_VOID__INT', + 'g_cclosure_marshal_VOID__UINT', + 'g_cclosure_marshal_VOID__LONG', + 'g_cclosure_marshal_VOID__ULONG', + 'g_cclosure_marshal_VOID__ENUM', + 'g_cclosure_marshal_VOID__FLAGS', + 'g_cclosure_marshal_VOID__FLOAT', + 'g_cclosure_marshal_VOID__DOUBLE', + 'g_cclosure_marshal_VOID__STRING', + 'g_cclosure_marshal_VOID__PARAM', + 'g_cclosure_marshal_VOID__BOXED', + 'g_cclosure_marshal_VOID__POINTER', + 'g_cclosure_marshal_VOID__OBJECT', + 'g_cclosure_marshal_VOID__VARIANT', + 'g_cclosure_marshal_VOID__UINT_POINTER', + 'g_cclosure_marshal_BOOLEAN__FLAGS', + 'g_cclosure_marshal_STRING__OBJECT_POINTER', + 'g_cclosure_marshal_BOOLEAN__BOXED_BOXED', +} + + +# pylint: disable=too-few-public-methods +class Color: + '''ANSI Terminal colors''' + GREEN = '\033[1;32m' + BLUE = '\033[1;34m' + YELLOW = '\033[1;33m' + RED = '\033[1;31m' + END = '\033[0m' + + +def print_color(msg, color=Color.END, prefix='MESSAGE'): + '''Print a string with a color prefix''' + if os.isatty(sys.stderr.fileno()): + real_prefix = '{start}{prefix}{end}'.format(start=color, prefix=prefix, end=Color.END) + else: + real_prefix = prefix + sys.stderr.write('{prefix}: {msg}\n'.format(prefix=real_prefix, msg=msg)) + + +def print_error(msg): + '''Print an error, and terminate''' + print_color(msg, color=Color.RED, prefix='ERROR') + sys.exit(1) + + +def print_warning(msg, fatal=False): + '''Print a warning, and optionally terminate''' + if fatal: + color = Color.RED + prefix = 'ERROR' + else: + color = Color.YELLOW + prefix = 'WARNING' + print_color(msg, color, prefix) + if fatal: + sys.exit(1) + + +def print_info(msg): + '''Print a message''' + print_color(msg, color=Color.GREEN, prefix='INFO') + + +def generate_licensing_comment(outfile): + outfile.write('/* This file is generated by glib-genmarshal, do not ' + 'modify it. This code is licensed under the same license as ' + 'the containing project. Note that it links to GLib, so ' + 'must comply with the LGPL linking clauses. */\n') + + +def generate_header_preamble(outfile, prefix='', std_includes=True, use_pragma=False): + '''Generate the preamble for the marshallers header file''' + generate_licensing_comment(outfile) + + if use_pragma: + outfile.write('#pragma once\n') + outfile.write('\n') + else: + outfile.write('#ifndef __{}_MARSHAL_H__\n'.format(prefix.upper())) + outfile.write('#define __{}_MARSHAL_H__\n'.format(prefix.upper())) + outfile.write('\n') + # Maintain compatibility with the old C-based tool + if std_includes: + outfile.write('#include \n') + outfile.write('\n') + + outfile.write('G_BEGIN_DECLS\n') + outfile.write('\n') + + +def generate_header_postamble(outfile, prefix='', use_pragma=False): + '''Generate the postamble for the marshallers header file''' + outfile.write('\n') + outfile.write('G_END_DECLS\n') + + if not use_pragma: + outfile.write('\n') + outfile.write('#endif /* __{}_MARSHAL_H__ */\n'.format(prefix.upper())) + + +def generate_body_preamble(outfile, std_includes=True, include_headers=None, cpp_defines=None, cpp_undefines=None): + '''Generate the preamble for the marshallers source file''' + generate_licensing_comment(outfile) + + for header in (include_headers or []): + outfile.write('#include "{}"\n'.format(header)) + if include_headers: + outfile.write('\n') + + for define in (cpp_defines or []): + s = define.split('=') + symbol = s[0] + value = s[1] if len(s) > 1 else '1' + outfile.write('#define {} {}\n'.format(symbol, value)) + if cpp_defines: + outfile.write('\n') + + for undefine in (cpp_undefines or []): + outfile.write('#undef {}\n'.format(undefine)) + if cpp_undefines: + outfile.write('\n') + + if std_includes: + outfile.write('#include \n') + outfile.write('\n') + + outfile.write(GETTERS_STR) + outfile.write('\n\n') + + +# Marshaller arguments, as a dictionary where the key is the token used in +# the source file, and the value is another dictionary with the following +# keys: +# +# - signal: the token used in the marshaller prototype (mandatory) +# - ctype: the C type for the marshaller argument (mandatory) +# - getter: the function used to retrieve the argument from the GValue +# array when invoking the callback (optional) +# - promoted: the C type used by va_arg() to retrieve the argument from +# the va_list when invoking the callback (optional, only used when +# generating va_list marshallers) +# - box: an array of two elements, containing the boxing and unboxing +# functions for the given type (optional, only used when generating +# va_list marshallers) +# - static-check: a boolean value, if the given type should perform +# a static type check before boxing or unboxing the argument (optional, +# only used when generating va_list marshallers) +# - takes-type: a boolean value, if the boxing and unboxing functions +# for the given type require the type (optional, only used when +# generating va_list marshallers) +# - deprecated: whether the token has been deprecated (optional) +# - replaced-by: the token used to replace a deprecated token (optional, +# only used if deprecated is True) +IN_ARGS = { + 'VOID': { + 'signal': 'VOID', + 'ctype': 'void', + }, + 'BOOLEAN': { + 'signal': 'BOOLEAN', + 'ctype': 'gboolean', + 'getter': 'g_marshal_value_peek_boolean', + }, + 'CHAR': { + 'signal': 'CHAR', + 'ctype': 'gchar', + 'promoted': 'gint', + 'getter': 'g_marshal_value_peek_char', + }, + 'UCHAR': { + 'signal': 'UCHAR', + 'ctype': 'guchar', + 'promoted': 'guint', + 'getter': 'g_marshal_value_peek_uchar', + }, + 'INT': { + 'signal': 'INT', + 'ctype': 'gint', + 'getter': 'g_marshal_value_peek_int', + }, + 'UINT': { + 'signal': 'UINT', + 'ctype': 'guint', + 'getter': 'g_marshal_value_peek_uint', + }, + 'LONG': { + 'signal': 'LONG', + 'ctype': 'glong', + 'getter': 'g_marshal_value_peek_long', + }, + 'ULONG': { + 'signal': 'ULONG', + 'ctype': 'gulong', + 'getter': 'g_marshal_value_peek_ulong', + }, + 'INT64': { + 'signal': 'INT64', + 'ctype': 'gint64', + 'getter': 'g_marshal_value_peek_int64', + }, + 'UINT64': { + 'signal': 'UINT64', + 'ctype': 'guint64', + 'getter': 'g_marshal_value_peek_uint64', + }, + 'ENUM': { + 'signal': 'ENUM', + 'ctype': 'gint', + 'getter': 'g_marshal_value_peek_enum', + }, + 'FLAGS': { + 'signal': 'FLAGS', + 'ctype': 'guint', + 'getter': 'g_marshal_value_peek_flags', + }, + 'FLOAT': { + 'signal': 'FLOAT', + 'ctype': 'gfloat', + 'promoted': 'gdouble', + 'getter': 'g_marshal_value_peek_float', + }, + 'DOUBLE': { + 'signal': 'DOUBLE', + 'ctype': 'gdouble', + 'getter': 'g_marshal_value_peek_double', + }, + 'STRING': { + 'signal': 'STRING', + 'ctype': 'gpointer', + 'getter': 'g_marshal_value_peek_string', + 'box': ['g_strdup', 'g_free'], + 'static-check': True, + }, + 'PARAM': { + 'signal': 'PARAM', + 'ctype': 'gpointer', + 'getter': 'g_marshal_value_peek_param', + 'box': ['g_param_spec_ref', 'g_param_spec_unref'], + 'static-check': True, + }, + 'BOXED': { + 'signal': 'BOXED', + 'ctype': 'gpointer', + 'getter': 'g_marshal_value_peek_boxed', + 'box': ['g_boxed_copy', 'g_boxed_free'], + 'static-check': True, + 'takes-type': True, + }, + 'POINTER': { + 'signal': 'POINTER', + 'ctype': 'gpointer', + 'getter': 'g_marshal_value_peek_pointer', + }, + 'OBJECT': { + 'signal': 'OBJECT', + 'ctype': 'gpointer', + 'getter': 'g_marshal_value_peek_object', + 'box': ['g_object_ref', 'g_object_unref'], + }, + 'VARIANT': { + 'signal': 'VARIANT', + 'ctype': 'gpointer', + 'getter': 'g_marshal_value_peek_variant', + 'box': ['g_variant_ref_sink', 'g_variant_unref'], + 'static-check': True, + 'takes-type': False, + }, + + # Deprecated tokens + 'NONE': { + 'signal': 'VOID', + 'ctype': 'void', + 'deprecated': True, + 'replaced_by': 'VOID' + }, + 'BOOL': { + 'signal': 'BOOLEAN', + 'ctype': 'gboolean', + 'getter': 'g_marshal_value_peek_boolean', + 'deprecated': True, + 'replaced_by': 'BOOLEAN' + } +} + + +# Marshaller return values, as a dictionary where the key is the token used +# in the source file, and the value is another dictionary with the following +# keys: +# +# - signal: the token used in the marshaller prototype (mandatory) +# - ctype: the C type for the marshaller argument (mandatory) +# - setter: the function used to set the return value of the callback +# into a GValue (optional) +# - deprecated: whether the token has been deprecated (optional) +# - replaced-by: the token used to replace a deprecated token (optional, +# only used if deprecated is True) +OUT_ARGS = { + 'VOID': { + 'signal': 'VOID', + 'ctype': 'void', + }, + 'BOOLEAN': { + 'signal': 'BOOLEAN', + 'ctype': 'gboolean', + 'setter': 'g_value_set_boolean', + }, + 'CHAR': { + 'signal': 'CHAR', + 'ctype': 'gchar', + 'setter': 'g_value_set_char', + }, + 'UCHAR': { + 'signal': 'UCHAR', + 'ctype': 'guchar', + 'setter': 'g_value_set_uchar', + }, + 'INT': { + 'signal': 'INT', + 'ctype': 'gint', + 'setter': 'g_value_set_int', + }, + 'UINT': { + 'signal': 'UINT', + 'ctype': 'guint', + 'setter': 'g_value_set_uint', + }, + 'LONG': { + 'signal': 'LONG', + 'ctype': 'glong', + 'setter': 'g_value_set_long', + }, + 'ULONG': { + 'signal': 'ULONG', + 'ctype': 'gulong', + 'setter': 'g_value_set_ulong', + }, + 'INT64': { + 'signal': 'INT64', + 'ctype': 'gint64', + 'setter': 'g_value_set_int64', + }, + 'UINT64': { + 'signal': 'UINT64', + 'ctype': 'guint64', + 'setter': 'g_value_set_uint64', + }, + 'ENUM': { + 'signal': 'ENUM', + 'ctype': 'gint', + 'setter': 'g_value_set_enum', + }, + 'FLAGS': { + 'signal': 'FLAGS', + 'ctype': 'guint', + 'setter': 'g_value_set_flags', + }, + 'FLOAT': { + 'signal': 'FLOAT', + 'ctype': 'gfloat', + 'setter': 'g_value_set_float', + }, + 'DOUBLE': { + 'signal': 'DOUBLE', + 'ctype': 'gdouble', + 'setter': 'g_value_set_double', + }, + 'STRING': { + 'signal': 'STRING', + 'ctype': 'gchar*', + 'setter': 'g_value_take_string', + }, + 'PARAM': { + 'signal': 'PARAM', + 'ctype': 'GParamSpec*', + 'setter': 'g_value_take_param', + }, + 'BOXED': { + 'signal': 'BOXED', + 'ctype': 'gpointer', + 'setter': 'g_value_take_boxed', + }, + 'POINTER': { + 'signal': 'POINTER', + 'ctype': 'gpointer', + 'setter': 'g_value_set_pointer', + }, + 'OBJECT': { + 'signal': 'OBJECT', + 'ctype': 'GObject*', + 'setter': 'g_value_take_object', + }, + 'VARIANT': { + 'signal': 'VARIANT', + 'ctype': 'GVariant*', + 'setter': 'g_value_take_variant', + }, + + # Deprecated tokens + 'NONE': { + 'signal': 'VOID', + 'ctype': 'void', + 'setter': None, + 'deprecated': True, + 'replaced_by': 'VOID', + }, + 'BOOL': { + 'signal': 'BOOLEAN', + 'ctype': 'gboolean', + 'setter': 'g_value_set_boolean', + 'deprecated': True, + 'replaced_by': 'BOOLEAN', + }, +} + + +def check_args(retval, params, fatal_warnings=False): + '''Check the @retval and @params tokens for invalid and deprecated symbols.''' + if retval not in OUT_ARGS: + print_error('Unknown return value type "{}"'.format(retval)) + + if OUT_ARGS[retval].get('deprecated', False): + replaced_by = OUT_ARGS[retval]['replaced_by'] + print_warning(DEPRECATED_MSG_STR.format(retval, replaced_by), fatal_warnings) + + for param in params: + if param not in IN_ARGS: + print_error('Unknown parameter type "{}"'.format(param)) + else: + if IN_ARGS[param].get('deprecated', False): + replaced_by = IN_ARGS[param]['replaced_by'] + print_warning(DEPRECATED_MSG_STR.format(param, replaced_by), fatal_warnings) + + +def indent(text, level=0, fill=' '): + '''Indent @text by @level columns, using the @fill character''' + return ''.join([fill for x in range(level)]) + text + + +# pylint: disable=too-few-public-methods +class Visibility: + '''Symbol visibility options''' + NONE = 0 + INTERNAL = 1 + EXTERN = 2 + + +def generate_marshaller_name(prefix, retval, params, replace_deprecated=True): + '''Generate a marshaller name for the given @prefix, @retval, and @params. + If @replace_deprecated is True, the generated name will replace deprecated + tokens.''' + if replace_deprecated: + real_retval = OUT_ARGS[retval]['signal'] + real_params = [] + for param in params: + real_params.append(IN_ARGS[param]['signal']) + else: + real_retval = retval + real_params = params + return '{prefix}_{retval}__{args}'.format(prefix=prefix, + retval=real_retval, + args='_'.join(real_params)) + + +def generate_prototype(retval, params, + prefix='g_cclosure_user_marshal', + visibility=Visibility.NONE, + va_marshal=False): + '''Generate a marshaller declaration with the given @visibility. If @va_marshal + is True, the marshaller will use variadic arguments in place of a GValue array.''' + signature = [] + + if visibility == Visibility.INTERNAL: + signature += ['G_GNUC_INTERNAL'] + elif visibility == Visibility.EXTERN: + signature += ['extern'] + + function_name = generate_marshaller_name(prefix, retval, params) + + if not va_marshal: + signature += ['void ' + function_name + ' (GClosure *closure,'] + width = len('void ') + len(function_name) + 2 + + signature += [indent('GValue *return_value,', level=width, fill=' ')] + signature += [indent('guint n_param_values,', level=width, fill=' ')] + signature += [indent('const GValue *param_values,', level=width, fill=' ')] + signature += [indent('gpointer invocation_hint,', level=width, fill=' ')] + signature += [indent('gpointer marshal_data);', level=width, fill=' ')] + else: + signature += ['void ' + function_name + 'v (GClosure *closure,'] + width = len('void ') + len(function_name) + 3 + + signature += [indent('GValue *return_value,', level=width, fill=' ')] + signature += [indent('gpointer instance,', level=width, fill=' ')] + signature += [indent('va_list args,', level=width, fill=' ')] + signature += [indent('gpointer marshal_data,', level=width, fill=' ')] + signature += [indent('int n_params,', level=width, fill=' ')] + signature += [indent('GType *param_types);', level=width, fill=' ')] + + return signature + + +# pylint: disable=too-many-statements, too-many-locals, too-many-branches +def generate_body(retval, params, prefix, va_marshal=False): + '''Generate a marshaller definition. If @va_marshal is True, the marshaller + will use va_list and variadic arguments in place of a GValue array.''' + retval_setter = OUT_ARGS[retval].get('setter', None) + # If there's no return value then we can mark the retval argument as unused + # and get a minor optimisation, as well as avoid a compiler warning + if not retval_setter: + unused = ' G_GNUC_UNUSED' + else: + unused = '' + + body = ['void'] + + function_name = generate_marshaller_name(prefix, retval, params) + + if not va_marshal: + body += [function_name + ' (GClosure *closure,'] + width = len(function_name) + 2 + + body += [indent('GValue *return_value{},'.format(unused), level=width, fill=' ')] + body += [indent('guint n_param_values,', level=width, fill=' ')] + body += [indent('const GValue *param_values,', level=width, fill=' ')] + body += [indent('gpointer invocation_hint G_GNUC_UNUSED,', level=width, fill=' ')] + body += [indent('gpointer marshal_data)', level=width, fill=' ')] + else: + body += [function_name + 'v (GClosure *closure,'] + width = len(function_name) + 3 + + body += [indent('GValue *return_value{},'.format(unused), level=width, fill=' ')] + body += [indent('gpointer instance,', level=width, fill=' ')] + body += [indent('va_list args,', level=width, fill=' ')] + body += [indent('gpointer marshal_data,', level=width, fill=' ')] + body += [indent('int n_params,', level=width, fill=' ')] + body += [indent('GType *param_types)', level=width, fill=' ')] + + # Filter the arguments that have a getter + get_args = [x for x in params if IN_ARGS[x].get('getter', None) is not None] + + body += ['{'] + + # Generate the type of the marshaller function + typedef_marshal = generate_marshaller_name('GMarshalFunc', retval, params) + + typedef = ' typedef {ctype} (*{func_name}) ('.format(ctype=OUT_ARGS[retval]['ctype'], + func_name=typedef_marshal) + pad = len(typedef) + typedef += 'gpointer data1,' + body += [typedef] + + for idx, in_arg in enumerate(get_args): + body += [indent('{} arg{:d},'.format(IN_ARGS[in_arg]['ctype'], idx + 1), level=pad)] + + body += [indent('gpointer data2);', level=pad)] + + # Variable declarations + body += [' GCClosure *cc = (GCClosure *) closure;'] + body += [' gpointer data1, data2;'] + body += [' {} callback;'.format(typedef_marshal)] + + if retval_setter: + body += [' {} v_return;'.format(OUT_ARGS[retval]['ctype'])] + + if va_marshal: + for idx, arg in enumerate(get_args): + body += [' {} arg{:d};'.format(IN_ARGS[arg]['ctype'], idx)] + + if get_args: + body += [' va_list args_copy;'] + body += [''] + + body += [' va_copy (args_copy, args);'] + + for idx, arg in enumerate(get_args): + ctype = IN_ARGS[arg]['ctype'] + promoted_ctype = IN_ARGS[arg].get('promoted', ctype) + body += [VA_ARG_STR.format(idx, ctype, promoted_ctype)] + if IN_ARGS[arg].get('box', None): + box_func = IN_ARGS[arg]['box'][0] + if IN_ARGS[arg].get('static-check', False): + static_check = STATIC_CHECK_STR.format(idx) + else: + static_check = '' + arg_check = 'arg{:d} != NULL'.format(idx) + body += [' if ({}{})'.format(static_check, arg_check)] + if IN_ARGS[arg].get('takes-type', False): + body += [BOX_TYPED_STR.format(idx=idx, box_func=box_func)] + else: + body += [BOX_UNTYPED_STR.format(idx=idx, box_func=box_func)] + + body += [' va_end (args_copy);'] + + body += [''] + + # Preconditions check + if retval_setter: + body += [' g_return_if_fail (return_value != NULL);'] + + if not va_marshal: + body += [' g_return_if_fail (n_param_values == {:d});'.format(len(get_args) + 1)] + + body += [''] + + # Marshal instance, data, and callback set up + body += [' if (G_CCLOSURE_SWAP_DATA (closure))'] + body += [' {'] + body += [' data1 = closure->data;'] + if va_marshal: + body += [' data2 = instance;'] + else: + body += [' data2 = g_value_peek_pointer (param_values + 0);'] + body += [' }'] + body += [' else'] + body += [' {'] + if va_marshal: + body += [' data1 = instance;'] + else: + body += [' data1 = g_value_peek_pointer (param_values + 0);'] + body += [' data2 = closure->data;'] + body += [' }'] + # pylint: disable=line-too-long + body += [' callback = ({}) (marshal_data ? marshal_data : cc->callback);'.format(typedef_marshal)] + body += [''] + + # Marshal callback action + if retval_setter: + callback = ' {} callback ('.format(' v_return =') + else: + callback = ' callback (' + + pad = len(callback) + body += [callback + 'data1,'] + + if va_marshal: + for idx, arg in enumerate(get_args): + body += [indent('arg{:d},'.format(idx), level=pad)] + else: + for idx, arg in enumerate(get_args): + arg_getter = IN_ARGS[arg]['getter'] + body += [indent('{} (param_values + {:d}),'.format(arg_getter, idx + 1), level=pad)] + + body += [indent('data2);', level=pad)] + + if va_marshal: + boxed_args = [x for x in get_args if IN_ARGS[x].get('box', None) is not None] + if not boxed_args: + body += [''] + else: + for idx, arg in enumerate(get_args): + if not IN_ARGS[arg].get('box', None): + continue + unbox_func = IN_ARGS[arg]['box'][1] + if IN_ARGS[arg].get('static-check', False): + static_check = STATIC_CHECK_STR.format(idx) + else: + static_check = '' + arg_check = 'arg{:d} != NULL'.format(idx) + body += [' if ({}{})'.format(static_check, arg_check)] + if IN_ARGS[arg].get('takes-type', False): + body += [UNBOX_TYPED_STR.format(idx=idx, unbox_func=unbox_func)] + else: + body += [UNBOX_UNTYPED_STR.format(idx=idx, unbox_func=unbox_func)] + + if retval_setter: + body += [''] + body += [' {} (return_value, v_return);'.format(retval_setter)] + + body += ['}'] + + return body + + +def generate_marshaller_alias(outfile, marshaller, real_marshaller, + include_va=False, + source_location=None): + '''Generate an alias between @marshaller and @real_marshaller, including + an optional alias for va_list marshallers''' + if source_location: + outfile.write('/* {} */\n'.format(source_location)) + + outfile.write('#define {}\t{}\n'.format(marshaller, real_marshaller)) + + if include_va: + outfile.write('#define {}v\t{}v\n'.format(marshaller, real_marshaller)) + + outfile.write('\n') + + +def generate_marshallers_header(outfile, retval, params, + prefix='g_cclosure_user_marshal', + internal=False, + include_va=False, source_location=None): + '''Generate a declaration for a marshaller function, to be used in the header, + with the given @retval, @params, and @prefix. An optional va_list marshaller + for the same arguments is also generated. The generated buffer is written to + the @outfile stream object.''' + if source_location: + outfile.write('/* {} */\n'.format(source_location)) + + if internal: + visibility = Visibility.INTERNAL + else: + visibility = Visibility.EXTERN + + signature = generate_prototype(retval, params, prefix, visibility, False) + if include_va: + signature += generate_prototype(retval, params, prefix, visibility, True) + signature += [''] + + outfile.write('\n'.join(signature)) + outfile.write('\n') + + +def generate_marshallers_body(outfile, retval, params, + prefix='g_cclosure_user_marshal', + include_prototype=True, + internal=False, + include_va=False, source_location=None): + '''Generate a definition for a marshaller function, to be used in the source, + with the given @retval, @params, and @prefix. An optional va_list marshaller + for the same arguments is also generated. The generated buffer is written to + the @outfile stream object.''' + if source_location: + outfile.write('/* {} */\n'.format(source_location)) + + if include_prototype: + # Declaration visibility + if internal: + decl_visibility = Visibility.INTERNAL + else: + decl_visibility = Visibility.EXTERN + proto = ['/* Prototype for -Wmissing-prototypes */'] + # Add C++ guards in case somebody compiles the generated code + # with a C++ compiler + proto += ['G_BEGIN_DECLS'] + proto += generate_prototype(retval, params, prefix, decl_visibility, False) + proto += ['G_END_DECLS'] + outfile.write('\n'.join(proto)) + outfile.write('\n') + + body = generate_body(retval, params, prefix, False) + outfile.write('\n'.join(body)) + outfile.write('\n\n') + + if include_va: + if include_prototype: + # Declaration visibility + if internal: + decl_visibility = Visibility.INTERNAL + else: + decl_visibility = Visibility.EXTERN + proto = ['/* Prototype for -Wmissing-prototypes */'] + # Add C++ guards here as well + proto += ['G_BEGIN_DECLS'] + proto += generate_prototype(retval, params, prefix, decl_visibility, True) + proto += ['G_END_DECLS'] + outfile.write('\n'.join(proto)) + outfile.write('\n') + + body = generate_body(retval, params, prefix, True) + outfile.write('\n'.join(body)) + outfile.write('\n\n') + + +def parse_args(): + arg_parser = argparse.ArgumentParser(description='Generate signal marshallers for GObject') + arg_parser.add_argument('--prefix', metavar='STRING', + default='g_cclosure_user_marshal', + help='Specify marshaller prefix') + arg_parser.add_argument('--output', metavar='FILE', + type=argparse.FileType('w'), + default=sys.stdout, + help='Write output into the specified file') + arg_parser.add_argument('--skip-source', + action='store_true', + help='Skip source location comments') + arg_parser.add_argument('--internal', + action='store_true', + help='Mark generated functions as internal') + arg_parser.add_argument('--valist-marshallers', + action='store_true', + help='Generate va_list marshallers') + arg_parser.add_argument('-v', '--version', + action='store_true', + dest='show_version', + help='Print version information, and exit') + arg_parser.add_argument('--g-fatal-warnings', + action='store_true', + dest='fatal_warnings', + help='Make warnings fatal') + arg_parser.add_argument('--include-header', metavar='HEADER', nargs='?', + action='append', + dest='include_headers', + help='Include the specified header in the body') + arg_parser.add_argument('--pragma-once', + action='store_true', + help='Use "pragma once" as the inclusion guard') + arg_parser.add_argument('-D', + action='append', + dest='cpp_defines', + default=[], + help='Pre-processor define') + arg_parser.add_argument('-U', + action='append', + dest='cpp_undefines', + default=[], + help='Pre-processor undefine') + arg_parser.add_argument('files', metavar='FILE', nargs='*', + type=argparse.FileType('r'), + help='Files with lists of marshallers to generate, ' + + 'or "-" for standard input') + arg_parser.add_argument('--prototypes', + action='store_true', + help='Generate the marshallers prototype in the C code') + arg_parser.add_argument('--header', + action='store_true', + help='Generate C headers') + arg_parser.add_argument('--body', + action='store_true', + help='Generate C code') + + group = arg_parser.add_mutually_exclusive_group() + group.add_argument('--stdinc', + action='store_true', + dest='stdinc', default=True, + help='Include standard marshallers') + group.add_argument('--nostdinc', + action='store_false', + dest='stdinc', default=True, + help='Use standard marshallers') + + group = arg_parser.add_mutually_exclusive_group() + group.add_argument('--quiet', + action='store_true', + help='Only print warnings and errors') + group.add_argument('--verbose', + action='store_true', + help='Be verbose, and include debugging information') + + args = arg_parser.parse_args() + + if args.show_version: + print(VERSION_STR) + sys.exit(0) + + return args + + +def generate(args): + # Backward compatibility hack; some projects use both arguments to + # generate the marshallers prototype in the C source, even though + # it's not really a supported use case. We keep this behaviour by + # forcing the --prototypes and --body arguments instead. We make this + # warning non-fatal even with --g-fatal-warnings, as it's a deprecation + compatibility_mode = False + if args.header and args.body: + print_warning('Using --header and --body at the same time is deprecated; ' + + 'use --body --prototypes instead', False) + args.prototypes = True + args.header = False + compatibility_mode = True + + if args.header: + generate_header_preamble(args.output, + prefix=args.prefix, + std_includes=args.stdinc, + use_pragma=args.pragma_once) + elif args.body: + generate_body_preamble(args.output, + std_includes=args.stdinc, + include_headers=args.include_headers, + cpp_defines=args.cpp_defines, + cpp_undefines=args.cpp_undefines) + + seen_marshallers = set() + + for infile in args.files: + if not args.quiet: + print_info('Reading {}...'.format(infile.name)) + + line_count = 0 + for line in infile: + line_count += 1 + + if line == '\n' or line.startswith('#'): + continue + + matches = re.match(r'^([A-Z0-9]+)\s?:\s?([A-Z0-9,\s]+)$', line.strip()) + if not matches or len(matches.groups()) != 2: + print_warning('Invalid entry: "{}"'.format(line.strip()), args.fatal_warnings) + continue + + if not args.skip_source: + location = '{} ({}:{:d})'.format(line.strip(), infile.name, line_count) + else: + location = None + + retval = matches.group(1).strip() + params = [x.strip() for x in matches.group(2).split(',')] + check_args(retval, params, args.fatal_warnings) + + raw_marshaller = generate_marshaller_name(args.prefix, retval, params, False) + if raw_marshaller in seen_marshallers: + if args.verbose: + print_info('Skipping repeated marshaller {}'.format(line.strip())) + continue + + if args.header: + if args.verbose: + print_info('Generating declaration for {}'.format(line.strip())) + generate_std_alias = False + if args.stdinc: + std_marshaller = generate_marshaller_name(STD_PREFIX, retval, params) + if std_marshaller in GOBJECT_MARSHALLERS: + if args.verbose: + print_info('Skipping default marshaller {}'.format(line.strip())) + generate_std_alias = True + + marshaller = generate_marshaller_name(args.prefix, retval, params) + if generate_std_alias: + generate_marshaller_alias(args.output, marshaller, std_marshaller, + source_location=location, + include_va=args.valist_marshallers) + else: + generate_marshallers_header(args.output, retval, params, + prefix=args.prefix, + internal=args.internal, + include_va=args.valist_marshallers, + source_location=location) + # If the marshaller is defined using a deprecated token, we want to maintain + # compatibility and generate an alias for the old name pointing to the new + # one + if marshaller != raw_marshaller: + if args.verbose: + print_info('Generating alias for deprecated tokens') + generate_marshaller_alias(args.output, raw_marshaller, marshaller, + include_va=args.valist_marshallers) + elif args.body: + if args.verbose: + print_info('Generating definition for {}'.format(line.strip())) + generate_std_alias = False + if args.stdinc: + std_marshaller = generate_marshaller_name(STD_PREFIX, retval, params) + if std_marshaller in GOBJECT_MARSHALLERS: + if args.verbose: + print_info('Skipping default marshaller {}'.format(line.strip())) + generate_std_alias = True + marshaller = generate_marshaller_name(args.prefix, retval, params) + if generate_std_alias: + # We need to generate the alias if we are in compatibility mode + if compatibility_mode: + generate_marshaller_alias(args.output, marshaller, std_marshaller, + source_location=location, + include_va=args.valist_marshallers) + else: + generate_marshallers_body(args.output, retval, params, + prefix=args.prefix, + internal=args.internal, + include_prototype=args.prototypes, + include_va=args.valist_marshallers, + source_location=location) + if compatibility_mode and marshaller != raw_marshaller: + if args.verbose: + print_info('Generating alias for deprecated tokens') + generate_marshaller_alias(args.output, raw_marshaller, marshaller, + include_va=args.valist_marshallers) + + seen_marshallers.add(raw_marshaller) + + if args.header: + generate_header_postamble(args.output, prefix=args.prefix, use_pragma=args.pragma_once) + + +if __name__ == '__main__': + args = parse_args() + + with args.output: + generate(args) diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/glib-gettextize b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/glib-gettextize new file mode 100644 index 000000000..9dc6f5d9b --- /dev/null +++ b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/glib-gettextize @@ -0,0 +1,189 @@ +#! /bin/sh +# +# Copyright (C) 1995-1998, 2000, 2001 Free Software Foundation, Inc. +# +# SPDX-License-Identifier: GPL-2.0-or-later +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2, or (at your option) +# any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, see . +# + +# - Modified in October 2001 by jacob berkman to +# work with glib's Makefile.in.in and po2tbl.sed.in, to not copy in +# intl/, and to not add ChangeLog entries to po/ChangeLog + +# This file is meant for authors or maintainers which want to +# internationalize their package with the help of GNU gettext. For +# further information how to use it consult the GNU gettext manual. + +echo=echo +progname=$0 +force=0 +configstatus=0 +origdir=`pwd` +usage="\ +Usage: glib-gettextize [OPTION]... [package-dir] + --help print this help and exit + --version print version information and exit + -c, --copy copy files instead of making symlinks + -f, --force force writing of new files even if old exist +Report bugs to https://gitlab.gnome.org/GNOME/glib/issues/new." +package=glib +version=2.82.4 +try_ln_s=: + +# Directory where the sources are stored. +prefix=C:/projects/repos/cerbero.git/1.28/build/dist/msvc_x86_64 +case `uname` in +MINGW32*) + prefix="`dirname $0`/.." + ;; +esac + +datarootdir=C:/projects/repos/cerbero.git/1.28/build/dist/msvc_x86_64/share +datadir=C:/projects/repos/cerbero.git/1.28/build/dist/msvc_x86_64/share + +gettext_dir=$datadir/glib-2.0/gettext + +while test $# -gt 0; do + case "$1" in + -c | --copy | --c* ) + shift + try_ln_s=false ;; + -f | --force | --f* ) + shift + force=1 ;; + -r | --run | --r* ) + shift + configstatus=1 ;; + --help | --h* ) + $echo "$usage"; exit 0 ;; + --version | --v* ) + echo "$progname (GNU $package) $version" + $echo "Copyright (C) 1995-1998, 2000, 2001 Free Software Foundation, Inc. +This is free software; see the source for copying conditions. There is NO +warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE." + $echo "Written by" "Ulrich Drepper" + exit 0 ;; + -- ) # Stop option processing + shift; break ;; + -* ) + $echo "glib-gettextize: unknown option $1" + $echo "Try \`glib-gettextize --help' for more information."; exit 1 ;; + * ) + break ;; + esac +done + +if test $# -gt 1; then + $echo "$usage" + exit 1 +fi + +# Fill in the command line options value. +if test $# -eq 1; then + srcdir=$1 + if cd "$srcdir"; then + srcdir=`pwd` + else + $echo "Cannot change directory to \`$srcdir'" + exit 1 + fi +else + srcdir=$origdir +fi + +test -f configure.in || test -f configure.ac || { + $echo "Missing configure.in or configure.ac, please cd to your package first." + exit 1 +} + +configure_in=NONE +if test -f configure.in; then + configure_in=configure.in +else + if test -f configure.ac; then + configure_in=configure.ac + fi +fi +# Check in which directory config.rpath, mkinstalldirs etc. belong. +auxdir=`cat "$configure_in" | grep '^AC_CONFIG_AUX_DIR' | sed -n -e 's/AC_CONFIG_AUX_DIR(\([^()]*\))/\1/p' | sed -e 's/^\[\(.*\)\]$/\1/' | sed -e 1q` +if test -n "$auxdir"; then + auxdir="$auxdir/" +fi + +if test -f po/Makefile.in.in && test $force -eq 0; then + $echo "\ +po/Makefile.in.in exists: use option -f if you really want to delete it." + exit 1 +fi + +test -d po || { + $echo "Creating po/ subdirectory" + mkdir po || { + $echo "failed to create po/ subdirectory" + exit 1 + } +} + +# For simplicity we changed to the gettext source directory. +cd $gettext_dir || { + $echo "gettext source directory '${gettext_dir}' doesn't exist" + exit 1 +} + +# Now copy all files. Take care for the destination directories. +for file in *; do + case $file in + intl | po) + ;; + mkinstalldirs) + rm -f "$srcdir/$auxdir$file" + ($try_ln_s && ln -s $gettext_dir/$file "$srcdir/$auxdir$file" && $echo "Symlinking file $file") 2>/dev/null || + { $echo "Copying file $file"; cp $file "$srcdir/$auxdir$file"; } + ;; + *) + rm -f "$srcdir/$file" + ($try_ln_s && ln -s $gettext_dir/$file "$srcdir/$file" && $echo "Symlinking file $file") 2>/dev/null || + { $echo "Copying file $file"; cp $file "$srcdir/$file"; } + ;; + esac +done + +# Copy files to po/ subdirectory. +cd po +for file in *; do + rm -f "$srcdir/po/$file" + ($try_ln_s && ln -s $gettext_dir/po/$file "$srcdir/po/$file" && $echo "Symlinking file po/$file") 2>/dev/null || + { $echo "Copying file po/$file"; cp $file "$srcdir/po/$file"; } +done +if test -f "$srcdir/po/cat-id-tbl.c"; then + $echo "Removing po/cat-id-tbl.c" + rm -f "$srcdir/po/cat-id-tbl.c" +fi +if test -f "$srcdir/po/stamp-cat-id"; then + $echo "Removing po/stamp-cat-id" + rm -f "$srcdir/po/stamp-cat-id" +fi + +echo +echo "Please add the files" +echo " codeset.m4 gettext.m4 glibc21.m4 iconv.m4 isc-posix.m4 lcmessage.m4" +echo " progtest.m4" +echo "from the $datadir/aclocal directory to your autoconf macro directory" +echo "or directly to your aclocal.m4 file." +echo "You will also need config.guess and config.sub, which you can get from" +echo "ftp://ftp.gnu.org/pub/gnu/config/." +echo + +exit 0 diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/glib-mkenums b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/glib-mkenums new file mode 100644 index 000000000..825003f1d --- /dev/null +++ b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/glib-mkenums @@ -0,0 +1,816 @@ +#!C:\projects\repos\cerbero.git\1.28\build\build-tools\bin\python.exe + +# If the code below looks horrible and unpythonic, do not panic. +# +# It is. +# +# This is a manual conversion from the original Perl script to +# Python. Improvements are welcome. +# +from __future__ import print_function, unicode_literals + +import argparse +import os +import re +import sys +import tempfile +import io +import errno +import codecs +import locale + +# Non-english locale systems might complain to unrecognized character +sys.stdout = io.TextIOWrapper(sys.stdout.detach(), encoding='utf-8') + +VERSION_STR = '''glib-mkenums version 2.82.4 +glib-mkenums comes with ABSOLUTELY NO WARRANTY. +You may redistribute copies of glib-mkenums under the terms of +the GNU General Public License which can be found in the +GLib source package. Sources, examples and contact +information are available at http://www.gtk.org''' + +# pylint: disable=too-few-public-methods +class Color: + '''ANSI Terminal colors''' + GREEN = '\033[1;32m' + BLUE = '\033[1;34m' + YELLOW = '\033[1;33m' + RED = '\033[1;31m' + END = '\033[0m' + + +def print_color(msg, color=Color.END, prefix='MESSAGE'): + '''Print a string with a color prefix''' + if os.isatty(sys.stderr.fileno()): + real_prefix = '{start}{prefix}{end}'.format(start=color, prefix=prefix, end=Color.END) + else: + real_prefix = prefix + print('{prefix}: {msg}'.format(prefix=real_prefix, msg=msg), file=sys.stderr) + + +def print_error(msg): + '''Print an error, and terminate''' + print_color(msg, color=Color.RED, prefix='ERROR') + sys.exit(1) + + +def print_warning(msg, fatal=False): + '''Print a warning, and optionally terminate''' + if fatal: + color = Color.RED + prefix = 'ERROR' + else: + color = Color.YELLOW + prefix = 'WARNING' + print_color(msg, color, prefix) + if fatal: + sys.exit(1) + + +def print_info(msg): + '''Print a message''' + print_color(msg, color=Color.GREEN, prefix='INFO') + + +def get_rspfile_args(rspfile): + ''' + Response files are useful on Windows where there is a command-line character + limit of 8191 because when passing sources as arguments to glib-mkenums this + limit can be exceeded in large codebases. + + There is no specification for response files and each tool that supports it + generally writes them out in slightly different ways, but some sources are: + https://docs.microsoft.com/en-us/visualstudio/msbuild/msbuild-response-files + https://docs.microsoft.com/en-us/windows/desktop/midl/the-response-file-command + ''' + import shlex + if not os.path.isfile(rspfile): + sys.exit('Response file {!r} does not exist'.format(rspfile)) + try: + with open(rspfile, 'r') as f: + cmdline = f.read() + except OSError as e: + sys.exit('Response file {!r} could not be read: {}' + .format(rspfile, e.strerror)) + return shlex.split(cmdline) + + +def write_output(output): + global output_stream + print(output, file=output_stream) + + +# Python 2 defaults to ASCII in case stdout is redirected. +# This should make it match Python 3, which uses the locale encoding. +if sys.stdout.encoding is None: + output_stream = codecs.getwriter( + locale.getpreferredencoding())(sys.stdout) +else: + output_stream = sys.stdout + + +# Some source files aren't UTF-8 and the old perl version didn't care. +# Replace invalid data with a replacement character to keep things working. +# https://bugzilla.gnome.org/show_bug.cgi?id=785113#c20 +def replace_and_warn(err): + # 7 characters of context either side of the offending character + print_warning('UnicodeWarning: {} at {} ({})'.format( + err.reason, err.start, + err.object[err.start - 7:err.end + 7])) + return ('?', err.end) + +codecs.register_error('replace_and_warn', replace_and_warn) + + +# glib-mkenums.py +# Information about the current enumeration +flags = None # Is enumeration a bitmask? +option_underscore_name = '' # Overridden underscore variant of the enum name + # for example to fix the cases we don't get the + # mixed-case -> underscorized transform right. +option_lowercase_name = '' # DEPRECATED. A lower case name to use as part + # of the *_get_type() function, instead of the + # one that we guess. For instance, when an enum + # uses abnormal capitalization and we can not + # guess where to put the underscores. +option_since = '' # User provided version info for the enum. +seenbitshift = 0 # Have we seen bitshift operators? +seenprivate = False # Have we seen a private option? +enum_prefix = None # Prefix for this enumeration +enumname = '' # Name for this enumeration +enumshort = '' # $enumname without prefix +enumname_prefix = '' # prefix of $enumname +enumindex = 0 # Global enum counter +firstenum = 1 # Is this the first enumeration per file? +entries = [] # [ name, val ] for each entry +c_namespace = {} # C symbols namespace. + +output = '' # Filename to write result into + +def parse_trigraph(opts): + result = {} + for opt in re.findall(r'(?:[^\s,"]|"(?:\\.|[^"])*")+', opts): + opt = re.sub(r'^\s*', '', opt) + opt = re.sub(r'\s*$', '', opt) + m = re.search(r'(\w+)(?:=(.+))?', opt) + assert m is not None + groups = m.groups() + key = groups[0] + if len(groups) > 1: + val = groups[1] + else: + val = 1 + result[key] = val.strip('"') if val is not None else None + return result + +def parse_entries(file, file_name): + global entries, enumindex, enumname, seenbitshift, seenprivate, flags + looking_for_name = False + + while True: + line = file.readline() + if not line: + break + + line = line.strip() + + # read lines until we have no open comments + while re.search(r'/\*([^*]|\*(?!/))*$', line): + line += file.readline() + + # strip comments w/o options + line = re.sub(r'''/\*(?!<) + ([^*]+|\*(?!/))* + \*/''', '', line, flags=re.X) + + line = line.rstrip() + + # skip empty lines + if len(line.strip()) == 0: + continue + + if looking_for_name: + m = re.match(r'\s*(\w+)', line) + if m: + enumname = m.group(1) + return True + + # Handle include files + m = re.match(r'\#include\s*<([^>]*)>', line) + if m: + newfilename = os.path.join("..", m.group(1)) + newfile = io.open(newfilename, encoding="utf-8", + errors="replace_and_warn") + + if not parse_entries(newfile, newfilename): + return False + else: + continue + + m = re.match(r'\s*\}\s*(\w+)', line) + if m: + enumname = m.group(1) + enumindex += 1 + return 1 + + m = re.match(r'\s*\}', line) + if m: + enumindex += 1 + looking_for_name = True + continue + + m = re.match(r'''\s* + (\w+)\s* # name + (\s+[A-Z]+_(?:AVAILABLE|DEPRECATED)_ENUMERATOR_IN_[0-9_]+(?:_FOR\s*\(\s*\w+\s*\))?\s*)? # availability + (?:=( # value + \s*'[^']*'\s* # char + | # OR + \s*\w+\s*\(.*\)\s* # macro with multiple args + | # OR + (?:[^,/]|/(?!\*))* # anything but a comma or comment + ))?,?\s* + (?:/\*< # options + (([^*]|\*(?!/))*) + >\s*\*/)?,? + \s*$''', line, flags=re.X) + if m: + groups = m.groups() + name = groups[0] + availability = None + value = None + options = None + if len(groups) > 1: + availability = groups[1] + if len(groups) > 2: + value = groups[2] + if len(groups) > 3: + options = groups[3] + if flags is None and value is not None and '<<' in value: + seenbitshift = 1 + + if options is not None: + options = parse_trigraph(options) + if 'skip' not in options: + entries.append((name, value, seenprivate, options.get('nick'))) + else: + entries.append((name, value, seenprivate)) + else: + m = re.match(r'''\s* + /\*< (([^*]|\*(?!/))*) >\s*\*/ + \s*$''', line, flags=re.X) + if m: + options = m.groups()[0] + if options is not None: + options = parse_trigraph(options) + if 'private' in options: + seenprivate = True + continue + if 'public' in options: + seenprivate = False + continue + if re.match(r's*\#', line): + pass + else: + print_warning('Failed to parse "{}" in {}'.format(line, file_name)) + return False + +help_epilog = '''Production text substitutions: + \u0040EnumName\u0040 PrefixTheXEnum + \u0040enum_name\u0040 prefix_the_xenum + \u0040ENUMNAME\u0040 PREFIX_THE_XENUM + \u0040ENUMSHORT\u0040 THE_XENUM + \u0040ENUMPREFIX\u0040 PREFIX + \u0040enumsince\u0040 the user-provided since value given + \u0040VALUENAME\u0040 PREFIX_THE_XVALUE + \u0040valuenick\u0040 the-xvalue + \u0040valuenum\u0040 the integer value (limited support, Since: 2.26) + \u0040type\u0040 either enum or flags + \u0040Type\u0040 either Enum or Flags + \u0040TYPE\u0040 either ENUM or FLAGS + \u0040filename\u0040 name of current input file + \u0040basename\u0040 base name of the current input file (Since: 2.22) +''' + + +# production variables: +idprefix = "" # "G", "Gtk", etc +symprefix = "" # "g", "gtk", etc, if not just lc($idprefix) +fhead = "" # output file header +fprod = "" # per input file production +ftail = "" # output file trailer +eprod = "" # per enum text (produced prior to value itarations) +vhead = "" # value header, produced before iterating over enum values +vprod = "" # value text, produced for each enum value +vtail = "" # value tail, produced after iterating over enum values +comment_tmpl = "" # comment template + +def read_template_file(file): + global idprefix, symprefix, fhead, fprod, ftail, eprod, vhead, vprod, vtail, comment_tmpl + tmpl = {'file-header': fhead, + 'file-production': fprod, + 'file-tail': ftail, + 'enumeration-production': eprod, + 'value-header': vhead, + 'value-production': vprod, + 'value-tail': vtail, + 'comment': comment_tmpl, + } + in_ = 'junk' + + ifile = io.open(file, encoding="utf-8", errors="replace_and_warn") + for line in ifile: + m = re.match(r'\/\*\*\*\s+(BEGIN|END)\s+([\w-]+)\s+\*\*\*\/', line) + if m: + if in_ == 'junk' and m.group(1) == 'BEGIN' and m.group(2) in tmpl: + in_ = m.group(2) + continue + elif in_ == m.group(2) and m.group(1) == 'END' and m.group(2) in tmpl: + in_ = 'junk' + continue + else: + sys.exit("Malformed template file " + file) + + if in_ != 'junk': + tmpl[in_] += line + + if in_ != 'junk': + sys.exit("Malformed template file " + file) + + fhead = tmpl['file-header'] + fprod = tmpl['file-production'] + ftail = tmpl['file-tail'] + eprod = tmpl['enumeration-production'] + vhead = tmpl['value-header'] + vprod = tmpl['value-production'] + vtail = tmpl['value-tail'] + comment_tmpl = tmpl['comment'] + +parser = argparse.ArgumentParser(epilog=help_epilog, + formatter_class=argparse.RawDescriptionHelpFormatter) + +parser.add_argument('--identifier-prefix', default='', dest='idprefix', + help='Identifier prefix') +parser.add_argument('--symbol-prefix', default='', dest='symprefix', + help='Symbol prefix') +parser.add_argument('--fhead', default=[], dest='fhead', action='append', + help='Output file header') +parser.add_argument('--ftail', default=[], dest='ftail', action='append', + help='Output file footer') +parser.add_argument('--fprod', default=[], dest='fprod', action='append', + help='Put out TEXT every time a new input file is being processed.') +parser.add_argument('--eprod', default=[], dest='eprod', action='append', + help='Per enum text, produced prior to value iterations') +parser.add_argument('--vhead', default=[], dest='vhead', action='append', + help='Value header, produced before iterating over enum values') +parser.add_argument('--vprod', default=[], dest='vprod', action='append', + help='Value text, produced for each enum value.') +parser.add_argument('--vtail', default=[], dest='vtail', action='append', + help='Value tail, produced after iterating over enum values') +parser.add_argument('--comments', default='', dest='comment_tmpl', + help='Comment structure') +parser.add_argument('--template', default='', dest='template', + help='Template file') +parser.add_argument('--output', default=None, dest='output') +parser.add_argument('--version', '-v', default=False, action='store_true', dest='version', + help='Print version information') +parser.add_argument('args', nargs='*', + help='One or more input files, or a single argument @rspfile_path ' + 'pointing to a file that contains the actual arguments') + +# Support reading an rspfile of the form @filename which contains the args +# to be parsed +if len(sys.argv) == 2 and sys.argv[1].startswith('@'): + args = get_rspfile_args(sys.argv[1][1:]) +else: + args = sys.argv[1:] + +options = parser.parse_args(args) + +if options.version: + print(VERSION_STR) + sys.exit(0) + +def unescape_cmdline_args(arg): + arg = arg.replace('\\n', '\n') + arg = arg.replace('\\r', '\r') + return arg.replace('\\t', '\t') + +if options.template != '': + read_template_file(options.template) + +idprefix += options.idprefix +symprefix += options.symprefix + +# This is a hack to maintain some semblance of backward compatibility with +# the old, Perl-based glib-mkenums. The old tool had an implicit ordering +# on the arguments and templates; each argument was parsed in order, and +# all the strings appended. This allowed developers to write: +# +# glib-mkenums \ +# --fhead ... \ +# --template a-template-file.c.in \ +# --ftail ... +# +# And have the fhead be prepended to the file-head stanza in the template, +# as well as the ftail be appended to the file-tail stanza in the template. +# Short of throwing away ArgumentParser and going over sys.argv[] element +# by element, we can simulate that behaviour by ensuring some ordering in +# how we build the template strings: +# +# - the head stanzas are always prepended to the template +# - the prod stanzas are always appended to the template +# - the tail stanzas are always appended to the template +# +# Within each instance of the command line argument, we append each value +# to the array in the order in which it appears on the command line. +fhead = ''.join([unescape_cmdline_args(x) for x in options.fhead]) + fhead +vhead = ''.join([unescape_cmdline_args(x) for x in options.vhead]) + vhead + +fprod += ''.join([unescape_cmdline_args(x) for x in options.fprod]) +eprod += ''.join([unescape_cmdline_args(x) for x in options.eprod]) +vprod += ''.join([unescape_cmdline_args(x) for x in options.vprod]) + +ftail = ftail + ''.join([unescape_cmdline_args(x) for x in options.ftail]) +vtail = vtail + ''.join([unescape_cmdline_args(x) for x in options.vtail]) + +if options.comment_tmpl != '': + comment_tmpl = unescape_cmdline_args(options.comment_tmpl) +elif comment_tmpl == "": + # default to C-style comments + comment_tmpl = "/* \u0040comment\u0040 */" + +output = options.output + +if output is not None: + (out_dir, out_fn) = os.path.split(options.output) + out_suffix = '_' + os.path.splitext(out_fn)[1] + if out_dir == '': + out_dir = '.' + fd, filename = tempfile.mkstemp(dir=out_dir) + os.close(fd) + tmpfile = io.open(filename, "w", encoding="utf-8") + output_stream = tmpfile +else: + tmpfile = None + +# put auto-generation comment +comment = comment_tmpl.replace('\u0040comment\u0040', + 'This file is generated by glib-mkenums, do ' + 'not modify it. This code is licensed under ' + 'the same license as the containing project. ' + 'Note that it links to GLib, so must comply ' + 'with the LGPL linking clauses.') +write_output("\n" + comment + '\n') + +def replace_specials(prod): + prod = prod.replace(r'\\a', r'\a') + prod = prod.replace(r'\\b', r'\b') + prod = prod.replace(r'\\t', r'\t') + prod = prod.replace(r'\\n', r'\n') + prod = prod.replace(r'\\f', r'\f') + prod = prod.replace(r'\\r', r'\r') + prod = prod.rstrip() + return prod + + +def warn_if_filename_basename_used(section, prod): + for substitution in ('\u0040filename\u0040', + '\u0040basename\u0040'): + if substitution in prod: + print_warning('{} used in {} section.'.format(substitution, + section)) + +if len(fhead) > 0: + prod = fhead + warn_if_filename_basename_used('file-header', prod) + prod = replace_specials(prod) + write_output(prod) + +def process_file(curfilename): + global entries, flags, seenbitshift, seenprivate, enum_prefix, c_namespace + firstenum = True + + try: + curfile = io.open(curfilename, encoding="utf-8", + errors="replace_and_warn") + except IOError as e: + if e.errno == errno.ENOENT: + print_warning('No file "{}" found.'.format(curfilename)) + return + raise + + while True: + line = curfile.readline() + if not line: + break + + line = line.strip() + + # read lines until we have no open comments + while re.search(r'/\*([^*]|\*(?!/))*$', line): + line += curfile.readline() + + # strip comments w/o options + line = re.sub(r'''/\*(?!<) + ([^*]+|\*(?!/))* + \*/''', '', line) + + # ignore forward declarations + if re.match(r'\s*typedef\s+enum.*;', line): + continue + + m = re.match(r'''\s*typedef\s+enum\s*[_A-Za-z]*[_A-Za-z0-9]*\s* + ({)?\s* + (?:/\*< + (([^*]|\*(?!/))*) + >\s*\*/)? + \s*({)?''', line, flags=re.X) + if m: + groups = m.groups() + if len(groups) >= 2 and groups[1] is not None: + options = parse_trigraph(groups[1]) + if 'skip' in options: + continue + enum_prefix = options.get('prefix', None) + flags = options.get('flags', None) + if 'flags' in options: + if flags is None: + flags = 1 + else: + flags = int(flags) + option_lowercase_name = options.get('lowercase_name', None) + option_underscore_name = options.get('underscore_name', None) + option_since = options.get('since', None) + else: + enum_prefix = None + flags = None + option_lowercase_name = None + option_underscore_name = None + option_since = None + + if option_lowercase_name is not None: + if option_underscore_name is not None: + print_warning("lowercase_name overridden with underscore_name") + option_lowercase_name = None + else: + print_warning("lowercase_name is deprecated, use underscore_name") + + # Didn't have trailing '{' look on next lines + if groups[0] is None and (len(groups) < 4 or groups[3] is None): + while True: + line = curfile.readline() + if not line: + print_error("Syntax error when looking for opening { in enum") + if re.match(r'\s*\{', line): + break + + seenbitshift = 0 + seenprivate = False + entries = [] + + # Now parse the entries + parse_entries(curfile, curfilename) + + # figure out if this was a flags or enums enumeration + if flags is None: + flags = seenbitshift + + # Autogenerate a prefix + if enum_prefix is None: + for entry in entries: + if not entry[2] and (len(entry) < 4 or entry[3] is None): + name = entry[0] + if enum_prefix is not None: + enum_prefix = os.path.commonprefix([name, enum_prefix]) + else: + enum_prefix = name + if enum_prefix is None: + enum_prefix = "" + else: + # Trim so that it ends in an underscore + enum_prefix = re.sub(r'_[^_]*$', '_', enum_prefix) + else: + # canonicalize user defined prefixes + enum_prefix = enum_prefix.upper() + enum_prefix = enum_prefix.replace('-', '_') + enum_prefix = re.sub(r'(.*)([^_])$', r'\1\2_', enum_prefix) + + fixed_entries = [] + for e in entries: + name = e[0] + num = e[1] + private = e[2] + if len(e) < 4 or e[3] is None: + nick = re.sub(r'^' + enum_prefix, '', name) + nick = nick.replace('_', '-').lower() + e = (name, num, private, nick) + fixed_entries.append(e) + entries = fixed_entries + + # Spit out the output + if option_underscore_name is not None: + enumlong = option_underscore_name.upper() + enumsym = option_underscore_name.lower() + enumshort = re.sub(r'^[A-Z][A-Z0-9]*_', '', enumlong) + + enumname_prefix = re.sub('_' + enumshort + '$', '', enumlong) + elif symprefix == '' and idprefix == '': + # enumname is e.g. GMatchType + enspace = re.sub(r'^([A-Z][a-z]*).*$', r'\1', enumname) + + enumshort = re.sub(r'^[A-Z][a-z]*', '', enumname) + enumshort = re.sub(r'([^A-Z])([A-Z])', r'\1_\2', enumshort) + enumshort = re.sub(r'([A-Z][A-Z])([A-Z][0-9a-z])', r'\1_\2', enumshort) + enumshort = enumshort.upper() + + enumname_prefix = re.sub(r'^([A-Z][a-z]*).*$', r'\1', enumname).upper() + + enumlong = enspace.upper() + "_" + enumshort + enumsym = enspace.lower() + "_" + enumshort.lower() + + if option_lowercase_name is not None: + enumsym = option_lowercase_name + else: + enumshort = enumname + if idprefix: + enumshort = re.sub(r'^' + idprefix, '', enumshort) + else: + enumshort = re.sub(r'/^[A-Z][a-z]*', '', enumshort) + + enumshort = re.sub(r'([^A-Z])([A-Z])', r'\1_\2', enumshort) + enumshort = re.sub(r'([A-Z][A-Z])([A-Z][0-9a-z])', r'\1_\2', enumshort) + enumshort = enumshort.upper() + + if symprefix: + enumname_prefix = symprefix.upper() + else: + enumname_prefix = idprefix.upper() + + enumlong = enumname_prefix + "_" + enumshort + enumsym = enumlong.lower() + + if option_since is not None: + enumsince = option_since + else: + enumsince = "" + + if firstenum: + firstenum = False + + if len(fprod) > 0: + prod = fprod + base = os.path.basename(curfilename) + + prod = prod.replace('\u0040filename\u0040', curfilename) + prod = prod.replace('\u0040basename\u0040', base) + prod = replace_specials(prod) + + write_output(prod) + + if len(eprod) > 0: + prod = eprod + + prod = prod.replace('\u0040enum_name\u0040', enumsym) + prod = prod.replace('\u0040EnumName\u0040', enumname) + prod = prod.replace('\u0040ENUMSHORT\u0040', enumshort) + prod = prod.replace('\u0040ENUMNAME\u0040', enumlong) + prod = prod.replace('\u0040ENUMPREFIX\u0040', enumname_prefix) + prod = prod.replace('\u0040enumsince\u0040', enumsince) + if flags: + prod = prod.replace('\u0040type\u0040', 'flags') + else: + prod = prod.replace('\u0040type\u0040', 'enum') + if flags: + prod = prod.replace('\u0040Type\u0040', 'Flags') + else: + prod = prod.replace('\u0040Type\u0040', 'Enum') + if flags: + prod = prod.replace('\u0040TYPE\u0040', 'FLAGS') + else: + prod = prod.replace('\u0040TYPE\u0040', 'ENUM') + prod = replace_specials(prod) + write_output(prod) + + if len(vhead) > 0: + prod = vhead + prod = prod.replace('\u0040enum_name\u0040', enumsym) + prod = prod.replace('\u0040EnumName\u0040', enumname) + prod = prod.replace('\u0040ENUMSHORT\u0040', enumshort) + prod = prod.replace('\u0040ENUMNAME\u0040', enumlong) + prod = prod.replace('\u0040ENUMPREFIX\u0040', enumname_prefix) + prod = prod.replace('\u0040enumsince\u0040', enumsince) + if flags: + prod = prod.replace('\u0040type\u0040', 'flags') + else: + prod = prod.replace('\u0040type\u0040', 'enum') + if flags: + prod = prod.replace('\u0040Type\u0040', 'Flags') + else: + prod = prod.replace('\u0040Type\u0040', 'Enum') + if flags: + prod = prod.replace('\u0040TYPE\u0040', 'FLAGS') + else: + prod = prod.replace('\u0040TYPE\u0040', 'ENUM') + prod = replace_specials(prod) + write_output(prod) + + if len(vprod) > 0: + prod = vprod + next_num = 0 + + prod = replace_specials(prod) + for name, num, private, nick in entries: + tmp_prod = prod + + if '\u0040valuenum\u0040' in prod: + # only attempt to eval the value if it is requested + # this prevents us from throwing errors otherwise + if num is not None: + # use sandboxed evaluation as a reasonable + # approximation to C constant folding + inum = eval(num, {}, c_namespace) + + # Support character literals + if isinstance(inum, str) and len(inum) == 1: + inum = ord(inum) + + # make sure it parsed to an integer + if not isinstance(inum, int): + sys.exit("Unable to parse enum value '%s'" % num) + num = inum + else: + num = next_num + + c_namespace[name] = num + tmp_prod = tmp_prod.replace('\u0040valuenum\u0040', str(num)) + next_num = int(num) + 1 + + if private: + continue + + tmp_prod = tmp_prod.replace('\u0040VALUENAME\u0040', name) + tmp_prod = tmp_prod.replace('\u0040valuenick\u0040', nick) + if flags: + tmp_prod = tmp_prod.replace('\u0040type\u0040', 'flags') + else: + tmp_prod = tmp_prod.replace('\u0040type\u0040', 'enum') + if flags: + tmp_prod = tmp_prod.replace('\u0040Type\u0040', 'Flags') + else: + tmp_prod = tmp_prod.replace('\u0040Type\u0040', 'Enum') + if flags: + tmp_prod = tmp_prod.replace('\u0040TYPE\u0040', 'FLAGS') + else: + tmp_prod = tmp_prod.replace('\u0040TYPE\u0040', 'ENUM') + tmp_prod = tmp_prod.rstrip() + + write_output(tmp_prod) + + if len(vtail) > 0: + prod = vtail + prod = prod.replace('\u0040enum_name\u0040', enumsym) + prod = prod.replace('\u0040EnumName\u0040', enumname) + prod = prod.replace('\u0040ENUMSHORT\u0040', enumshort) + prod = prod.replace('\u0040ENUMNAME\u0040', enumlong) + prod = prod.replace('\u0040ENUMPREFIX\u0040', enumname_prefix) + prod = prod.replace('\u0040enumsince\u0040', enumsince) + if flags: + prod = prod.replace('\u0040type\u0040', 'flags') + else: + prod = prod.replace('\u0040type\u0040', 'enum') + if flags: + prod = prod.replace('\u0040Type\u0040', 'Flags') + else: + prod = prod.replace('\u0040Type\u0040', 'Enum') + if flags: + prod = prod.replace('\u0040TYPE\u0040', 'FLAGS') + else: + prod = prod.replace('\u0040TYPE\u0040', 'ENUM') + prod = replace_specials(prod) + write_output(prod) + +for fname in sorted(options.args): + process_file(fname) + +if len(ftail) > 0: + prod = ftail + warn_if_filename_basename_used('file-tail', prod) + prod = replace_specials(prod) + write_output(prod) + +# put auto-generation comment +comment = comment_tmpl +comment = comment.replace('\u0040comment\u0040', 'Generated data ends here') +write_output("\n" + comment + "\n") + +if tmpfile is not None: + tmpfilename = tmpfile.name + tmpfile.close() + + try: + os.unlink(options.output) + except OSError as error: + if error.errno != errno.ENOENT: + raise error + + os.rename(tmpfilename, options.output) diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gmodule-2.0-0.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gmodule-2.0-0.dll new file mode 100644 index 000000000..f9712b7cd Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gmodule-2.0-0.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gobject-2.0-0.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gobject-2.0-0.dll new file mode 100644 index 000000000..33f81a469 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gobject-2.0-0.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/graphene-1.0-0.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/graphene-1.0-0.dll new file mode 100644 index 000000000..88b49fbda Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/graphene-1.0-0.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gresource.exe b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gresource.exe new file mode 100644 index 000000000..fa5efb09f Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gresource.exe differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gsettings.exe b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gsettings.exe new file mode 100644 index 000000000..fe48af7da Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gsettings.exe differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gspawn-win64-helper-console.exe b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gspawn-win64-helper-console.exe new file mode 100644 index 000000000..b8ae06f03 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gspawn-win64-helper-console.exe differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gspawn-win64-helper.exe b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gspawn-win64-helper.exe new file mode 100644 index 000000000..b872dcebf Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gspawn-win64-helper.exe differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gst-device-monitor-1.0.exe b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gst-device-monitor-1.0.exe new file mode 100644 index 000000000..7351b80ea Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gst-device-monitor-1.0.exe differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gst-discoverer-1.0.exe b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gst-discoverer-1.0.exe new file mode 100644 index 000000000..6edd577d5 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gst-discoverer-1.0.exe differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gst-dots-viewer.exe b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gst-dots-viewer.exe new file mode 100644 index 000000000..9dedde707 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gst-dots-viewer.exe differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gst-inspect-1.0.exe b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gst-inspect-1.0.exe new file mode 100644 index 000000000..ecf5887be Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gst-inspect-1.0.exe differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gst-launch-1.0.exe b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gst-launch-1.0.exe new file mode 100644 index 000000000..a2cb81edc Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gst-launch-1.0.exe differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gst-play-1.0.exe b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gst-play-1.0.exe new file mode 100644 index 000000000..7a18dd03f Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gst-play-1.0.exe differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gst-shell b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gst-shell new file mode 100644 index 000000000..347bc9fa7 --- /dev/null +++ b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gst-shell @@ -0,0 +1,22 @@ +#!/bin/bash + +export GSTREAMER_ROOT="C:/projects/repos/cerbero.git/1.28/build/dist/msvc_x86_64" +export PATH="${GSTREAMER_ROOT}/bin${PATH:+:$PATH}" +export LD_LIBRARY_PATH="${GSTREAMER_ROOT}/lib${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}" +export PKG_CONFIG_PATH="${GSTREAMER_ROOT}/lib/pkgconfig:${GSTREAMER_ROOT}/share/pkgconfig${PKG_CONFIG_PATH:+:$PKG_CONFIG_PATH}" +export XDG_DATA_DIRS="${GSTREAMER_ROOT}/share${XDG_DATA_DIRS:+:$XDG_DATA_DIRS}" +export XDG_CONFIG_DIRS="${GSTREAMER_ROOT}/etc/xdg${XDG_CONFIG_DIRS:+:$XDG_CONFIG_DIRS}" +export GST_REGISTRY_1_0="${HOME}/.cache/gstreamer-1.0/gstreamer-cerbero-registry" +export GST_PLUGIN_SCANNER_1_0="${GSTREAMER_ROOT}/libexec/gstreamer-1.0/gst-plugin-scanner" +export GST_PLUGIN_PATH_1_0="${GSTREAMER_ROOT}/lib/gstreamer-1.0" +export GST_PLUGIN_SYSTEM_PATH_1_0="${GSTREAMER_ROOT}/lib/gstreamer-1.0" +export PYTHONPATH="${GSTREAMER_ROOT}\Lib/site-packages${PYTHONPATH:+:$PYTHONPATH}" +export CFLAGS="-I${GSTREAMER_ROOT}/include ${CFLAGS}" +export CXXFLAGS="-I${GSTREAMER_ROOT}/include ${CXXFLAGS}" +export CPPFLAGS="-I${GSTREAMER_ROOT}/include ${CPPFLAGS}" +export LDFLAGS="-L${GSTREAMER_ROOT}/lib ${LDFLAGS}" +export GIO_EXTRA_MODULES="${GSTREAMER_ROOT}/lib/gio/modules" +export GI_TYPELIB_PATH="${GSTREAMER_ROOT}/lib/girepository-1.0" + + +$SHELL "$@" diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gst-typefind-1.0.exe b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gst-typefind-1.0.exe new file mode 100644 index 000000000..0c16b9a2b Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gst-typefind-1.0.exe differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gst-validate-1.0.exe b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gst-validate-1.0.exe new file mode 100644 index 000000000..e3ba43ebb Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gst-validate-1.0.exe differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gst-validate-launcher b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gst-validate-launcher new file mode 100644 index 000000000..0267c71e3 --- /dev/null +++ b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gst-validate-launcher @@ -0,0 +1,55 @@ +#!/usr/bin/env python3 +# +# Copyright (c) 2014,Thibault Saunier +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this program; if not, write to the +# Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, +# Boston, MA 02110-1301, USA. + +import os +import subprocess +import sys + +LIBDIR = r'C:/projects/repos/cerbero.git/1.28/build/dist/msvc_x86_64/lib' +BUILDDIR = r'C:\projects\repos\cerbero.git\1.28\build\sources\msvc_x86_64\gst-devtools-1.0-1.28.3\b\validate\tools' +SRCDIR = r'C:\projects\repos\cerbero.git\1.28\build\sources\msvc_x86_64\gst-devtools-1.0-1.28.3\validate\tools' + + +def _add_gst_launcher_path(): + f = os.path.abspath(__file__) + if f.startswith(BUILDDIR): + # Make sure to have the configured config.py in the python path + sys.path.insert(0, os.path.abspath(os.path.join(BUILDDIR, ".."))) + root = os.path.abspath(os.path.join(SRCDIR, "../")) + else: + root = os.path.join(LIBDIR, 'gst-validate-launcher', 'python') + + sys.path.insert(0, root) + return os.path.join(root, "launcher") + + +if "__main__" == __name__: + libsdir = _add_gst_launcher_path() + from launcher.main import main + run_profile = os.environ.get('GST_VALIDATE_LAUNCHER_PROFILING', False) + if run_profile: + import cProfile + prof = cProfile.Profile() + try: + res = prof.runcall(main, libsdir) + finally: + prof.dump_stats('gst-validate-launcher-runstats') + exit(res) + + exit(main(libsdir)) diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gst-validate-media-check-1.0.exe b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gst-validate-media-check-1.0.exe new file mode 100644 index 000000000..3e0b15675 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gst-validate-media-check-1.0.exe differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gst-validate-rtsp-server-1.0.exe b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gst-validate-rtsp-server-1.0.exe new file mode 100644 index 000000000..fdd76fa99 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gst-validate-rtsp-server-1.0.exe differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gst-validate-transcoding-1.0.exe b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gst-validate-transcoding-1.0.exe new file mode 100644 index 000000000..8a4088e9c Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gst-validate-transcoding-1.0.exe differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstadaptivedemux-1.0-0.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstadaptivedemux-1.0-0.dll new file mode 100644 index 000000000..6ac5942e3 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstadaptivedemux-1.0-0.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstallocators-1.0-0.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstallocators-1.0-0.dll new file mode 100644 index 000000000..273487df6 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstallocators-1.0-0.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstanalytics-1.0-0.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstanalytics-1.0-0.dll new file mode 100644 index 000000000..23aa739e0 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstanalytics-1.0-0.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstapp-1.0-0.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstapp-1.0-0.dll new file mode 100644 index 000000000..1393c4247 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstapp-1.0-0.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstaudio-1.0-0.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstaudio-1.0-0.dll new file mode 100644 index 000000000..c204b1a32 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstaudio-1.0-0.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstbadaudio-1.0-0.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstbadaudio-1.0-0.dll new file mode 100644 index 000000000..f6969c535 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstbadaudio-1.0-0.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstbase-1.0-0.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstbase-1.0-0.dll new file mode 100644 index 000000000..f1c2eb367 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstbase-1.0-0.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstbasecamerabinsrc-1.0-0.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstbasecamerabinsrc-1.0-0.dll new file mode 100644 index 000000000..887e3dd7e Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstbasecamerabinsrc-1.0-0.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstcheck-1.0-0.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstcheck-1.0-0.dll new file mode 100644 index 000000000..22a046586 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstcheck-1.0-0.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstcodecparsers-1.0-0.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstcodecparsers-1.0-0.dll new file mode 100644 index 000000000..3a22dee1f Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstcodecparsers-1.0-0.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstcodecs-1.0-0.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstcodecs-1.0-0.dll new file mode 100644 index 000000000..a767b8270 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstcodecs-1.0-0.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstcontroller-1.0-0.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstcontroller-1.0-0.dll new file mode 100644 index 000000000..5f2a5ac30 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstcontroller-1.0-0.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstcuda-1.0-0.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstcuda-1.0-0.dll new file mode 100644 index 000000000..9fd1f6acd Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstcuda-1.0-0.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstd3d11-1.0-0.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstd3d11-1.0-0.dll new file mode 100644 index 000000000..2311aee58 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstd3d11-1.0-0.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstd3d12-1.0-0.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstd3d12-1.0-0.dll new file mode 100644 index 000000000..81bb65469 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstd3d12-1.0-0.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstd3dshader-1.0-0.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstd3dshader-1.0-0.dll new file mode 100644 index 000000000..0576eaca8 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstd3dshader-1.0-0.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstdxva-1.0-0.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstdxva-1.0-0.dll new file mode 100644 index 000000000..016beed43 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstdxva-1.0-0.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstfft-1.0-0.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstfft-1.0-0.dll new file mode 100644 index 000000000..6ab13b9cf Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstfft-1.0-0.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstgl-1.0-0.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstgl-1.0-0.dll new file mode 100644 index 000000000..5bc437dd4 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstgl-1.0-0.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstinsertbin-1.0-0.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstinsertbin-1.0-0.dll new file mode 100644 index 000000000..c1e6e1939 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstinsertbin-1.0-0.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstisoff-1.0-0.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstisoff-1.0-0.dll new file mode 100644 index 000000000..71b16a085 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstisoff-1.0-0.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstmpegts-1.0-0.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstmpegts-1.0-0.dll new file mode 100644 index 000000000..cfdddb325 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstmpegts-1.0-0.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstmse-1.0-0.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstmse-1.0-0.dll new file mode 100644 index 000000000..484f5d249 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstmse-1.0-0.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstnet-1.0-0.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstnet-1.0-0.dll new file mode 100644 index 000000000..1082e1e4a Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstnet-1.0-0.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstpbutils-1.0-0.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstpbutils-1.0-0.dll new file mode 100644 index 000000000..4e6d938cd Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstpbutils-1.0-0.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstphotography-1.0-0.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstphotography-1.0-0.dll new file mode 100644 index 000000000..f6f7a7f0e Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstphotography-1.0-0.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstplay-1.0-0.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstplay-1.0-0.dll new file mode 100644 index 000000000..e970d5fb1 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstplay-1.0-0.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstplayer-1.0-0.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstplayer-1.0-0.dll new file mode 100644 index 000000000..7dea461f3 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstplayer-1.0-0.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstreamer-1.0-0.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstreamer-1.0-0.dll new file mode 100644 index 000000000..66afb1858 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstreamer-1.0-0.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstriff-1.0-0.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstriff-1.0-0.dll new file mode 100644 index 000000000..806f9bef4 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstriff-1.0-0.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstrtp-1.0-0.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstrtp-1.0-0.dll new file mode 100644 index 000000000..a55dd77e5 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstrtp-1.0-0.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstrtsp-1.0-0.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstrtsp-1.0-0.dll new file mode 100644 index 000000000..f06a71924 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstrtsp-1.0-0.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstrtspserver-1.0-0.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstrtspserver-1.0-0.dll new file mode 100644 index 000000000..e4e57589b Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstrtspserver-1.0-0.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstsctp-1.0-0.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstsctp-1.0-0.dll new file mode 100644 index 000000000..af7691c83 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstsctp-1.0-0.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstsdp-1.0-0.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstsdp-1.0-0.dll new file mode 100644 index 000000000..bea49d060 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstsdp-1.0-0.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gsttag-1.0-0.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gsttag-1.0-0.dll new file mode 100644 index 000000000..2588224af Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gsttag-1.0-0.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gsttranscoder-1.0-0.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gsttranscoder-1.0-0.dll new file mode 100644 index 000000000..864ff64fe Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gsttranscoder-1.0-0.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gsturidownloader-1.0-0.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gsturidownloader-1.0-0.dll new file mode 100644 index 000000000..425290fb6 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gsturidownloader-1.0-0.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstvalidate-1.0-0.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstvalidate-1.0-0.dll new file mode 100644 index 000000000..4ebcf3356 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstvalidate-1.0-0.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstvideo-1.0-0.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstvideo-1.0-0.dll new file mode 100644 index 000000000..a10e1d149 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstvideo-1.0-0.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstwebrtc-1.0-0.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstwebrtc-1.0-0.dll new file mode 100644 index 000000000..4f80f3fb8 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstwebrtc-1.0-0.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstwebrtcnice-1.0-0.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstwebrtcnice-1.0-0.dll new file mode 100644 index 000000000..de461882e Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstwebrtcnice-1.0-0.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstwinrt-1.0-0.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstwinrt-1.0-0.dll new file mode 100644 index 000000000..fa0a2b56d Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstwinrt-1.0-0.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gthread-2.0-0.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gthread-2.0-0.dll new file mode 100644 index 000000000..e13a20a88 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gthread-2.0-0.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gtk-4-1.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gtk-4-1.dll new file mode 100644 index 000000000..c3fd428f2 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gtk-4-1.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/harfbuzz-cairo.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/harfbuzz-cairo.dll new file mode 100644 index 000000000..66aa54cc7 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/harfbuzz-cairo.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/harfbuzz-gobject.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/harfbuzz-gobject.dll new file mode 100644 index 000000000..8104b7f8e Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/harfbuzz-gobject.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/harfbuzz-subset.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/harfbuzz-subset.dll new file mode 100644 index 000000000..834b8f14a Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/harfbuzz-subset.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/harfbuzz.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/harfbuzz.dll new file mode 100644 index 000000000..0d1243b68 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/harfbuzz.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/iconv-2.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/iconv-2.dll new file mode 100644 index 000000000..9053a75a7 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/iconv-2.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/intl-8.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/intl-8.dll new file mode 100644 index 000000000..d37f3a60f Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/intl-8.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/jpeg8.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/jpeg8.dll new file mode 100644 index 000000000..7a7bfab18 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/jpeg8.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/json-glib-1.0-0.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/json-glib-1.0-0.dll new file mode 100644 index 000000000..1efa5409e Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/json-glib-1.0-0.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/json-glib-format.exe b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/json-glib-format.exe new file mode 100644 index 000000000..0cf0b1027 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/json-glib-format.exe differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/json-glib-validate.exe b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/json-glib-validate.exe new file mode 100644 index 000000000..33d567da6 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/json-glib-validate.exe differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/lcevc_dec_api.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/lcevc_dec_api.dll new file mode 100644 index 000000000..6cd67a1e7 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/lcevc_dec_api.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/lcevc_dec_legacy.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/lcevc_dec_legacy.dll new file mode 100644 index 000000000..6277a45f4 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/lcevc_dec_legacy.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/lcevc_dec_pipeline_cpu.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/lcevc_dec_pipeline_cpu.dll new file mode 100644 index 000000000..d2daeeb8a Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/lcevc_dec_pipeline_cpu.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/lcevc_dec_pipeline_legacy.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/lcevc_dec_pipeline_legacy.dll new file mode 100644 index 000000000..83d64911b Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/lcevc_dec_pipeline_legacy.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/libcrypto-3-x64.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/libcrypto-3-x64.dll new file mode 100644 index 000000000..59688b42c Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/libcrypto-3-x64.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/libcurl.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/libcurl.dll new file mode 100644 index 000000000..8193459cb Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/libcurl.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/libexpat.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/libexpat.dll new file mode 100644 index 000000000..5e0ae1374 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/libexpat.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/libgcc_s_seh-1.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/libgcc_s_seh-1.dll new file mode 100644 index 000000000..ddf0e38ff Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/libgcc_s_seh-1.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/libmpg123-1.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/libmpg123-1.dll new file mode 100644 index 000000000..51c3a9e56 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/libmpg123-1.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/libpng16-config b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/libpng16-config new file mode 100644 index 000000000..0068564c1 --- /dev/null +++ b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/libpng16-config @@ -0,0 +1,127 @@ +#! /bin/sh + +# libpng-config +# provides configuration info for libpng. + +# Copyright (C) 2002, 2004, 2006, 2007 Glenn Randers-Pehrson + +# This code is released under the libpng license. +# For conditions of distribution and use, see the disclaimer +# and license in png.h + +# Modeled after libxml-config. + +version="1.6.56" +prefix="C:/projects/repos/cerbero.git/1.28/build/dist/msvc_x86_64" +exec_prefix="C:/projects/repos/cerbero.git/1.28/build/dist/msvc_x86_64" +libdir="C:/projects/repos/cerbero.git/1.28/build/dist/msvc_x86_64/lib" +includedir="C:/projects/repos/cerbero.git/1.28/build/dist/msvc_x86_64/include/libpng16" +libs="-lpng16" +all_libs="-lpng16 -lz -lm" +I_opts="-I${includedir}" +L_opts="-L${libdir}" +R_opts="" +cppflags="" +ccopts="" +ldopts="" + +usage() +{ + cat < + + + Set hintslight to hintstyle + + + + hintslight + + diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/etc/fonts/conf.d/10-scale-bitmap-fonts.conf b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/etc/fonts/conf.d/10-scale-bitmap-fonts.conf new file mode 100644 index 000000000..0c3a2efc3 --- /dev/null +++ b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/etc/fonts/conf.d/10-scale-bitmap-fonts.conf @@ -0,0 +1,83 @@ + + + + Bitmap scaling + + + + false + + + + pixelsize + pixelsize + + + + + + + false + + + false + + + true + + + + + pixelsizefixupfactor + 1.2 + + + pixelsizefixupfactor + 0.8 + + + + + + + true + + + 1.0 + + + + + + false + + + 1.0 + + + + matrix + + pixelsizefixupfactor 0 + 0 pixelsizefixupfactor + + + + + + size + pixelsizefixupfactor + + + + + diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/etc/fonts/conf.d/10-sub-pixel-none.conf b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/etc/fonts/conf.d/10-sub-pixel-none.conf new file mode 100644 index 000000000..1fb6c98af --- /dev/null +++ b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/etc/fonts/conf.d/10-sub-pixel-none.conf @@ -0,0 +1,15 @@ + + + + Disable sub-pixel rendering + + + + none + + diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/etc/fonts/conf.d/10-yes-antialias.conf b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/etc/fonts/conf.d/10-yes-antialias.conf new file mode 100644 index 000000000..4451f6ed1 --- /dev/null +++ b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/etc/fonts/conf.d/10-yes-antialias.conf @@ -0,0 +1,8 @@ + + + + Enable antialiasing + + true + + diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/etc/fonts/conf.d/11-lcdfilter-default.conf b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/etc/fonts/conf.d/11-lcdfilter-default.conf new file mode 100644 index 000000000..602559783 --- /dev/null +++ b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/etc/fonts/conf.d/11-lcdfilter-default.conf @@ -0,0 +1,17 @@ + + + + Use lcddefault as default for LCD filter + + + + + lcddefault + + + diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/etc/fonts/conf.d/20-unhint-small-vera.conf b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/etc/fonts/conf.d/20-unhint-small-vera.conf new file mode 100644 index 000000000..e4e9c33cb --- /dev/null +++ b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/etc/fonts/conf.d/20-unhint-small-vera.conf @@ -0,0 +1,49 @@ + + + + Disable hinting for Bitstream Vera fonts when the size is less than 8ppem + + + + + Bitstream Vera Sans + + + 7.5 + + + false + + + + + + Bitstream Vera Serif + + + 7.5 + + + false + + + + + + Bitstream Vera Sans Mono + + + 7.5 + + + false + + + + diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/etc/fonts/conf.d/30-metric-aliases.conf b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/etc/fonts/conf.d/30-metric-aliases.conf new file mode 100644 index 000000000..edf4ef852 --- /dev/null +++ b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/etc/fonts/conf.d/30-metric-aliases.conf @@ -0,0 +1,658 @@ + + + + Set substitutions for similar/metric-compatible families + + + + + + + + Helvetica LT Std + + Helvetica + + + + + Nimbus Sans L + + Helvetica + + + + + Nimbus Sans + + Helvetica + + + + + TeX Gyre Heros + + Helvetica + + + + + Nimbus Sans Narrow + + Helvetica Narrow + + + + + TeX Gyre Heros Cn + + Helvetica Narrow + + + + + Nimbus Roman No9 L + + Times + + + + + Nimbus Roman + + Times + + + + + TeX Gyre Termes + + Times + + + + + Courier Std + + Courier + + + + + Nimbus Mono L + + Courier + + + + + Nimbus Mono + + Courier + + + + + Nimbus Mono PS + + Courier + + + + + TeX Gyre Cursor + + Courier + + + + + Avant Garde + + ITC Avant Garde Gothic + + + + + URW Gothic L + + ITC Avant Garde Gothic + + + + + URW Gothic + + ITC Avant Garde Gothic + + + + + TeX Gyre Adventor + + ITC Avant Garde Gothic + + + + + Bookman + + ITC Bookman + + + + + URW Bookman L + + ITC Bookman + + + + + Bookman URW + + ITC Bookman + + + + + URW Bookman + + ITC Bookman + + + + + TeX Gyre Bonum + + ITC Bookman + + + + + Bookman Old Style + + ITC Bookman + + + + + Zapf Chancery + + ITC Zapf Chancery + + + + + URW Chancery L + + ITC Zapf Chancery + + + + + Chancery URW + + ITC Zapf Chancery + + + + + Z003 + + ITC Zapf Chancery + + + + + TeX Gyre Chorus + + ITC Zapf Chancery + + + + + URW Palladio L + + Palatino + + + + + Palladio URW + + Palatino + + + + + P052 + + Palatino + + + + + TeX Gyre Pagella + + Palatino + + + + + Palatino Linotype + + Palatino + + + + + Century Schoolbook L + + New Century Schoolbook + + + + + Century SchoolBook URW + + New Century Schoolbook + + + + + C059 + + New Century Schoolbook + + + + + TeX Gyre Schola + + New Century Schoolbook + + + + + Century Schoolbook + + New Century Schoolbook + + + + + + Arimo + + Arial + + + + + Liberation Sans + + Arial + + + + + Liberation Sans Narrow + + Arial Narrow + + + + + Albany + + Arial + + + + + Albany AMT + + Arial + + + + + Tinos + + Times New Roman + + + + + Liberation Serif + + Times New Roman + + + + + Thorndale + + Times New Roman + + + + + Thorndale AMT + + Times New Roman + + + + + Cousine + + Courier New + + + + + Liberation Mono + + Courier New + + + + + Cumberland + + Courier New + + + + + Cumberland AMT + + Courier New + + + + + Gelasio + + Georgia + + + + + Caladea + + Cambria + + + + + Carlito + + Calibri + + + + + SymbolNeu + + Symbol + + + + + + + + Helvetica + + Arial + + + + + Helvetica Narrow + + Arial Narrow + + + + + Times + + Times New Roman + + + + + Courier + + Courier New + + + + + + Arial + + Helvetica + + + + + Arial Narrow + + Helvetica Narrow + + + + + Times New Roman + + Times + + + + + Courier New + + Courier + + + + + + + + Helvetica + + Helvetica LT Std + + + + + Helvetica + + TeX Gyre Heros + + + + + Helvetica Narrow + + TeX Gyre Heros Cn + + + + + Times + + TeX Gyre Termes + + + + + Courier + + TeX Gyre Cursor + + + + + Courier + + Courier Std + + + + + ITC Avant Garde Gothic + + TeX Gyre Adventor + + + + + ITC Bookman + + Bookman Old Style + TeX Gyre Bonum + + + + + ITC Zapf Chancery + + TeX Gyre Chorus + + + + + Palatino + + Palatino Linotype + TeX Gyre Pagella + + + + + New Century Schoolbook + + Century Schoolbook + TeX Gyre Schola + + + + + + Arial + + Arimo + Liberation Sans + Albany + Albany AMT + + + + + Arial Narrow + + Liberation Sans Narrow + + + + + Times New Roman + + Tinos + Liberation Serif + Thorndale + Thorndale AMT + + + + + Courier New + + Cousine + Liberation Mono + Cumberland + Cumberland AMT + + + + + Georgia + + Gelasio + + + + + Cambria + + Caladea + + + + + Calibri + + Carlito + + + + + Symbol + + SymbolNeu + + + + diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/etc/fonts/conf.d/40-nonlatin.conf b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/etc/fonts/conf.d/40-nonlatin.conf new file mode 100644 index 000000000..f8d96ce81 --- /dev/null +++ b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/etc/fonts/conf.d/40-nonlatin.conf @@ -0,0 +1,332 @@ + + + + Set substitutions for non-Latin fonts + + + + + Nazli + serif + + + Lotoos + serif + + + Mitra + serif + + + Ferdosi + serif + + + Badr + serif + + + Zar + serif + + + Titr + serif + + + Jadid + serif + + + Kochi Mincho + serif + + + AR PL SungtiL GB + serif + + + AR PL Mingti2L Big5 + serif + + + MS 明朝 + serif + + + NanumMyeongjo + serif + + + UnBatang + serif + + + Baekmuk Batang + serif + + + MgOpen Canonica + serif + + + Sazanami Mincho + serif + + + AR PL ZenKai Uni + serif + + + ZYSong18030 + serif + + + FreeSerif + serif + + + SimSun + serif + + + + Arshia + sans-serif + + + Elham + sans-serif + + + Farnaz + sans-serif + + + Nasim + sans-serif + + + Sina + sans-serif + + + Roya + sans-serif + + + Koodak + sans-serif + + + Terafik + sans-serif + + + Kochi Gothic + sans-serif + + + AR PL KaitiM GB + sans-serif + + + AR PL KaitiM Big5 + sans-serif + + + MS ゴシック + sans-serif + + + NanumGothic + sans-serif + + + UnDotum + sans-serif + + + Baekmuk Dotum + sans-serif + + + MgOpen Modata + sans-serif + + + Sazanami Gothic + sans-serif + + + AR PL ShanHeiSun Uni + sans-serif + + + ZYSong18030 + sans-serif + + + FreeSans + sans-serif + + + + NSimSun + monospace + + + ZYSong18030 + monospace + + + NanumGothicCoding + monospace + + + FreeMono + monospace + + + + + Homa + fantasy + + + Kamran + fantasy + + + Fantezi + fantasy + + + Tabassom + fantasy + + + + + IranNastaliq + cursive + + + Nafees Nastaleeq + cursive + + + + + Noto Sans Arabic UI + system-ui + + + Noto Sans Bengali UI + system-ui + + + Noto Sans Devanagari UI + system-ui + + + Noto Sans Gujarati UI + system-ui + + + Noto Sans Gurmukhi UI + system-ui + + + Noto Sans Kannada UI + system-ui + + + Noto Sans Khmer UI + system-ui + + + Noto Sans Lao UI + system-ui + + + Noto Sans Malayalam UI + system-ui + + + Noto Sans Myanmar UI + system-ui + + + Noto Sans Oriya UI + system-ui + + + Noto Sans Sinhala UI + system-ui + + + Noto Sans Tamil UI + system-ui + + + Noto Sans Telugu UI + system-ui + + + Noto Sans Thai UI + system-ui + + + Leelawadee UI + system-ui + + + Nirmala UI + system-ui + + + Yu Gothic UI + system-ui + + + Meiryo UI + system-ui + + + MS UI Gothic + system-ui + + + Khmer UI + system-ui + + + Lao UI + system-ui + + + Microsoft JhengHei UI + system-ui + + + Microsoft YaHei UI + system-ui + + + diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/etc/fonts/conf.d/45-generic.conf b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/etc/fonts/conf.d/45-generic.conf new file mode 100644 index 000000000..5c1bd36b5 --- /dev/null +++ b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/etc/fonts/conf.d/45-generic.conf @@ -0,0 +1,136 @@ + + + + Set substitutions for emoji/math fonts + + + + + + + + Noto Color Emoji + emoji + + + Apple Color Emoji + emoji + + + Segoe UI Emoji + emoji + + + Twitter Color Emoji + emoji + + + EmojiOne Mozilla + emoji + + + + Emoji Two + emoji + + + JoyPixels + emoji + + + Emoji One + emoji + + + + Noto Emoji + emoji + + + Android Emoji + emoji + + + + + + emoji + + + und-zsye + + + + + + und-zsye + + + emoji + + + + + emoji + + + + + + + + + XITS Math + math + + + STIX Two Math + math + + + Cambria Math + math + + + Latin Modern Math + math + + + Minion Math + math + + + Lucida Math + math + + + Asana Math + math + + + + + + math + + + und-zmth + + + + + + und-zmth + + + math + + + + + math + + + + + diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/etc/fonts/conf.d/45-latin.conf b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/etc/fonts/conf.d/45-latin.conf new file mode 100644 index 000000000..53158c767 --- /dev/null +++ b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/etc/fonts/conf.d/45-latin.conf @@ -0,0 +1,309 @@ + + + + Set substitutions for Latin fonts + + + + + Bitstream Vera Serif + serif + + + Cambria + serif + + + Constantia + serif + + + DejaVu Serif + serif + + + Elephant + serif + + + Garamond + serif + + + Georgia + serif + + + Liberation Serif + serif + + + Luxi Serif + serif + + + MS Serif + serif + + + Nimbus Roman No9 L + serif + + + Nimbus Roman + serif + + + Palatino Linotype + serif + + + Thorndale AMT + serif + + + Thorndale + serif + + + Times New Roman + serif + + + Times + serif + + + + Albany AMT + sans-serif + + + Albany + sans-serif + + + Arial Unicode MS + sans-serif + + + Arial + sans-serif + + + Bitstream Vera Sans + sans-serif + + + Britannic + sans-serif + + + Calibri + sans-serif + + + Candara + sans-serif + + + Century Gothic + sans-serif + + + Corbel + sans-serif + + + DejaVu Sans + sans-serif + + + Helvetica LT Std + sans-serif + + + Helvetica + sans-serif + + + Haettenschweiler + sans-serif + + + Liberation Sans + sans-serif + + + MS Sans Serif + sans-serif + + + Nimbus Sans L + sans-serif + + + Nimbus Sans + sans-serif + + + Luxi Sans + sans-serif + + + Tahoma + sans-serif + + + Trebuchet MS + sans-serif + + + Twentieth Century + sans-serif + + + Verdana + sans-serif + + + + Andale Mono + monospace + + + Bitstream Vera Sans Mono + monospace + + + Consolas + monospace + + + Courier New + monospace + + + Courier Std + monospace + + + Courier + monospace + + + Cumberland AMT + monospace + + + Cumberland + monospace + + + DejaVu Sans Mono + monospace + + + Fixedsys + monospace + + + Inconsolata + monospace + + + Liberation Mono + monospace + + + Luxi Mono + monospace + + + Nimbus Mono L + monospace + + + Nimbus Mono + monospace + + + Nimbus Mono PS + monospace + + + Terminal + monospace + + + + Bauhaus Std + fantasy + + + Cooper Std + fantasy + + + Copperplate Gothic Std + fantasy + + + Impact + fantasy + + + + Comic Sans MS + cursive + + + ITC Zapf Chancery Std + cursive + + + Zapfino + cursive + + + + Adwaita Sans + system-ui + + + Cantarell + system-ui + + + Noto Sans UI + system-ui + + + Segoe UI + system-ui + + + Segoe UI Historic + system-ui + + + Segoe UI Symbol + system-ui + + + diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/etc/fonts/conf.d/48-spacing.conf b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/etc/fonts/conf.d/48-spacing.conf new file mode 100644 index 000000000..6df5c1176 --- /dev/null +++ b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/etc/fonts/conf.d/48-spacing.conf @@ -0,0 +1,16 @@ + + + + Add mono to the family when spacing is 100 + + + + 100 + + + monospace + + + diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/etc/fonts/conf.d/49-sansserif.conf b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/etc/fonts/conf.d/49-sansserif.conf new file mode 100644 index 000000000..6cc3a1c49 --- /dev/null +++ b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/etc/fonts/conf.d/49-sansserif.conf @@ -0,0 +1,22 @@ + + + + Add sans-serif to the family when no generic name + + + + sans-serif + + + serif + + + monospace + + + sans-serif + + + diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/etc/fonts/conf.d/50-user.conf b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/etc/fonts/conf.d/50-user.conf new file mode 100644 index 000000000..d019f4d4e --- /dev/null +++ b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/etc/fonts/conf.d/50-user.conf @@ -0,0 +1,16 @@ + + + + Load per-user customization files + + fontconfig/conf.d + fontconfig/fonts.conf + + ~/.fonts.conf.d + ~/.fonts.conf + diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/etc/fonts/conf.d/51-local.conf b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/etc/fonts/conf.d/51-local.conf new file mode 100644 index 000000000..82e3c1b2f --- /dev/null +++ b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/etc/fonts/conf.d/51-local.conf @@ -0,0 +1,7 @@ + + + + Load local customization file + + local.conf + diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/etc/fonts/conf.d/60-generic.conf b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/etc/fonts/conf.d/60-generic.conf new file mode 100644 index 000000000..783150771 --- /dev/null +++ b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/etc/fonts/conf.d/60-generic.conf @@ -0,0 +1,64 @@ + + + + Set preferable fonts for emoji/math fonts + + + + + + + + und-zsye + + + true + + + false + + + true + + + + + + emoji + + + Noto Color Emoji + Apple Color Emoji + Segoe UI Emoji + Twitter Color Emoji + EmojiOne Mozilla + + Emoji Two + JoyPixels + Emoji One + + Noto Emoji + Android Emoji + + + + + + + math + + XITS Math + STIX Two Math + Cambria Math + Latin Modern Math + Minion Math + Lucida Math + Asana Math + + + + diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/etc/fonts/conf.d/60-latin.conf b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/etc/fonts/conf.d/60-latin.conf new file mode 100644 index 000000000..ae3be3c73 --- /dev/null +++ b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/etc/fonts/conf.d/60-latin.conf @@ -0,0 +1,89 @@ + + + + Set preferable fonts for Latin + + serif + + Noto Serif + DejaVu Serif + Times New Roman + Thorndale AMT + Luxi Serif + Nimbus Roman No9 L + Nimbus Roman + Times + + + + sans-serif + + Noto Sans + DejaVu Sans + Verdana + Arial + Albany AMT + Luxi Sans + Nimbus Sans L + Nimbus Sans + Helvetica + Lucida Sans Unicode + BPG Glaho International + Tahoma + + + + monospace + + Noto Sans Mono + DejaVu Sans Mono + Inconsolata + Andale Mono + Courier New + Cumberland AMT + Luxi Mono + Nimbus Mono L + Nimbus Mono + Nimbus Mono PS + Courier + + + + + fantasy + + Impact + Copperplate Gothic Std + Cooper Std + Bauhaus Std + + + + + cursive + + ITC Zapf Chancery Std + Zapfino + Comic Sans MS + + + + + system-ui + + Adwaita Sans + Cantarell + Noto Sans UI + Segoe UI + Segoe UI Historic + Segoe UI Symbol + + + + diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/etc/fonts/conf.d/65-fonts-persian.conf b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/etc/fonts/conf.d/65-fonts-persian.conf new file mode 100644 index 000000000..47da1bb09 --- /dev/null +++ b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/etc/fonts/conf.d/65-fonts-persian.conf @@ -0,0 +1,418 @@ + + + + + + + + + + Nesf + Nesf2 + + + Nesf2 + Persian_sansserif_default + + + + + + Nazanin + Nazli + + + Lotus + Lotoos + + + Yaqut + Yaghoot + + + Yaghut + Yaghoot + + + Traffic + Terafik + + + Ferdowsi + Ferdosi + + + Fantezy + Fantezi + + + + + + + + Jadid + Persian_title + + + Titr + Persian_title + + + + + Kamran + + Persian_fantasy + Homa + + + + Homa + + Persian_fantasy + Kamran + + + + Fantezi + Persian_fantasy + + + Tabassom + Persian_fantasy + + + + + Arshia + Persian_square + + + Nasim + Persian_square + + + Elham + + Persian_square + Farnaz + + + + Farnaz + + Persian_square + Elham + + + + Sina + Persian_square + + + + + + + Persian_title + + Titr + Jadid + Persian_serif + + + + + + Persian_fantasy + + Homa + Kamran + Fantezi + Tabassom + Persian_square + + + + + + Persian_square + + Arshia + Elham + Farnaz + Nasim + Sina + Persian_serif + + + + + + + + Elham + + + farsiweb + + + + + + Homa + + + farsiweb + + + + + + Koodak + + + farsiweb + + + + + + Nazli + + + farsiweb + + + + + + Roya + + + farsiweb + + + + + + Terafik + + + farsiweb + + + + + + Titr + + + farsiweb + + + + + + + + + + TURNED-OFF + + + farsiweb + + + + roman + + + + roman + + + + + matrix + 1-0.2 + 01 + + + + + + oblique + + + + + + + + + farsiweb + + + false + + + false + + + false + + + + + + + + + serif + + Nazli + Lotoos + Mitra + Ferdosi + Badr + Zar + + + + + + sans-serif + + Roya + Koodak + Terafik + + + + + + monospace + + + Terafik + + + + + + fantasy + + Homa + Kamran + Fantezi + Tabassom + + + + + + cursive + + IranNastaliq + Nafees Nastaleeq + + + + + + + + + serif + + + 200 + + + 24 + + + Titr + + + + + + + sans-serif + + + 200 + + + 24 + + + Titr + + + + + + + Persian_sansserif_default + + + 200 + + + 24 + + + Titr + + + + + + + + + Persian_sansserif_default + + + Roya + + + + diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/etc/fonts/conf.d/65-nonlatin.conf b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/etc/fonts/conf.d/65-nonlatin.conf new file mode 100644 index 000000000..3e5d1c7d8 --- /dev/null +++ b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/etc/fonts/conf.d/65-nonlatin.conf @@ -0,0 +1,240 @@ + + + + Set preferable fonts for non-Latin + + serif + + Artsounk + BPG UTF8 M + Kinnari + Norasi + Frank Ruehl + Dror + JG LaoTimes + Saysettha Unicode + Pigiarniq + B Davat + B Compset + Kacst-Qr + Urdu Nastaliq Unicode + Raghindi + Mukti Narrow + malayalam + Sampige + padmaa + Hapax Berbère + MS Mincho + SimSun + PMingLiu + WenQuanYi Zen Hei + WenQuanYi Bitmap Song + AR PL ShanHeiSun Uni + AR PL New Sung + ZYSong18030 + HanyiSong + Hiragino Mincho ProN + Songti SC + Songti TC + SimSong + MgOpen Canonica + Sazanami Mincho + IPAMonaMincho + IPAMincho + Kochi Mincho + AR PL SungtiL GB + AR PL Mingti2L Big5 + AR PL Zenkai Uni + MS 明朝 + ZYSong18030 + NanumMyeongjo + UnBatang + Baekmuk Batang + AppleMyungjo + KacstQura + Frank Ruehl CLM + Lohit Bengali + Lohit Gujarati + Lohit Hindi + Lohit Marathi + Lohit Maithili + Lohit Kashmiri + Lohit Konkani + Lohit Nepali + Lohit Sindhi + Lohit Punjabi + Lohit Tamil + Rachana + Lohit Malayalam + Lohit Kannada + Lohit Telugu + Lohit Oriya + LKLUG + + + + sans-serif + + Nachlieli + Lucida Sans Unicode + Yudit Unicode + Kerkis + ArmNet Helvetica + Artsounk + BPG UTF8 M + Waree + Loma + Garuda + Umpush + Saysettha Unicode + JG Lao Old Arial + GF Zemen Unicode + Pigiarniq + B Davat + B Compset + Kacst-Qr + Urdu Nastaliq Unicode + Raghindi + Mukti Narrow + malayalam + Sampige + padmaa + Hapax Berbère + MS Gothic + UmePlus P Gothic + Microsoft YaHei + Microsoft JhengHei + WenQuanYi Zen Hei + WenQuanYi Bitmap Song + AR PL ShanHeiSun Uni + AR PL New Sung + Hiragino Sans + PingFang SC + PingFang TC + PingFang HK + Hiragino Sans CNS + Hiragino Sans GB + MgOpen Modata + VL Gothic + IPAMonaGothic + IPAGothic + Sazanami Gothic + Kochi Gothic + AR PL KaitiM GB + AR PL KaitiM Big5 + AR PL ShanHeiSun Uni + AR PL SungtiL GB + AR PL Mingti2L Big5 + MS ゴシック + ZYSong18030 + TSCu_Paranar + NanumGothic + UnDotum + Baekmuk Dotum + Baekmuk Gulim + Apple SD Gothic Neo + KacstQura + Lohit Bengali + Lohit Gujarati + Lohit Hindi + Lohit Marathi + Lohit Maithili + Lohit Kashmiri + Lohit Konkani + Lohit Nepali + Lohit Sindhi + Lohit Punjabi + Lohit Tamil + Meera + Lohit Malayalam + Lohit Kannada + Lohit Telugu + Lohit Oriya + LKLUG + + + + monospace + + Miriam Mono + VL Gothic + IPAMonaGothic + IPAGothic + Sazanami Gothic + Kochi Gothic + AR PL KaitiM GB + MS Gothic + UmePlus Gothic + NSimSun + MingLiu + AR PL ShanHeiSun Uni + AR PL New Sung Mono + HanyiSong + AR PL SungtiL GB + AR PL Mingti2L Big5 + ZYSong18030 + NanumGothicCoding + NanumGothic + UnDotum + Baekmuk Dotum + Baekmuk Gulim + TlwgTypo + TlwgTypist + TlwgTypewriter + TlwgMono + Hasida + GF Zemen Unicode + Hapax Berbère + Lohit Bengali + Lohit Gujarati + Lohit Hindi + Lohit Marathi + Lohit Maithili + Lohit Kashmiri + Lohit Konkani + Lohit Nepali + Lohit Sindhi + Lohit Punjabi + Lohit Tamil + Meera + Lohit Malayalam + Lohit Kannada + Lohit Telugu + Lohit Oriya + LKLUG + + + + + system-ui + + Noto Sans Arabic UI + Noto Sans Bengali UI + Noto Sans Devanagari UI + Noto Sans Gujarati UI + Noto Sans Gurmukhi UI + Noto Sans Kannada UI + Noto Sans Khmer UI + Noto Sans Lao UI + Noto Sans Malayalam UI + Noto Sans Myanmar UI + Noto Sans Oriya UI + Noto Sans Sinhala UI + Noto Sans Tamil UI + Noto Sans Telugu UI + Noto Sans Thai UI + Leelawadee UI + Nirmala UI + Yu Gothic UI + Meiryo UI + MS UI Gothic + Khmer UI + Lao UI + Microsoft YaHei UI + Microsoft JhengHei UI + + + + diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/etc/fonts/conf.d/69-unifont.conf b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/etc/fonts/conf.d/69-unifont.conf new file mode 100644 index 000000000..02854ff9d --- /dev/null +++ b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/etc/fonts/conf.d/69-unifont.conf @@ -0,0 +1,28 @@ + + + + + serif + + FreeSerif + Code2000 + Code2001 + + + + sans-serif + + FreeSans + Arial Unicode MS + Arial Unicode + Code2000 + Code2001 + + + + monospace + + FreeMono + + + diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/etc/fonts/conf.d/80-delicious.conf b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/etc/fonts/conf.d/80-delicious.conf new file mode 100644 index 000000000..d20990cd3 --- /dev/null +++ b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/etc/fonts/conf.d/80-delicious.conf @@ -0,0 +1,19 @@ + + + + + + + + + Delicious + + + Heavy + + + heavy + + + + diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/etc/fonts/conf.d/90-synthetic.conf b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/etc/fonts/conf.d/90-synthetic.conf new file mode 100644 index 000000000..dfce674bb --- /dev/null +++ b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/etc/fonts/conf.d/90-synthetic.conf @@ -0,0 +1,64 @@ + + + + + + + + + roman + + + + roman + + + + + matrix + 10.2 + 01 + + + + + + oblique + + + + false + + + + + + + + + medium + + + + bold + + + + true + + + + bold + + + diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/etc/fonts/conf.d/README b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/etc/fonts/conf.d/README new file mode 100644 index 000000000..fd4f5b212 --- /dev/null +++ b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/etc/fonts/conf.d/README @@ -0,0 +1,23 @@ +conf.d/README + +Each file in this directory is a fontconfig configuration file. Fontconfig +scans this directory, loading all files of the form [0-9][0-9]*.conf. +These files are normally installed in C:/projects/repos/cerbero.git/1.28/build/dist/msvc_x86_64/share/fontconfig/conf.avail +and then symlinked here, allowing them to be easily installed and then +enabled/disabled by adjusting the symlinks. + +The files are loaded in numeric order, the structure of the configuration +has led to the following conventions in usage: + + Files beginning with: Contain: + + 00 through 09 Font directories + 10 through 19 system rendering defaults (AA, etc) + 20 through 29 font rendering options + 30 through 39 family substitution + 40 through 49 generic identification, map family->generic + 50 through 59 alternate config file loading + 60 through 69 generic aliases, map generic->family + 70 through 79 select font (adjust which fonts are available) + 80 through 89 match target="scan" (modify scanned patterns) + 90 through 99 font synthesis diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/etc/fonts/fonts.conf b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/etc/fonts/fonts.conf new file mode 100644 index 000000000..c8fdfc24e --- /dev/null +++ b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/etc/fonts/fonts.conf @@ -0,0 +1,103 @@ + + + + + Default configuration file + + + + + + WINDOWSFONTDIR + WINDOWSUSERFONTDIR + + + fonts + + ~/.fonts + + + + + mono + + + monospace + + + + + + + sans serif + + + sans-serif + + + + + + + sans + + + sans-serif + + + + + + system ui + + + system-ui + + + + + conf.d + + + + LOCAL_APPDATA_FONTCONFIG_CACHE + fontconfig + + ~/.fontconfig + + + + + 30 + + + + diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/etc/ssl/certs/ca-certificates.crt b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/etc/ssl/certs/ca-certificates.crt new file mode 100644 index 000000000..9551dfd83 --- /dev/null +++ b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/etc/ssl/certs/ca-certificates.crt @@ -0,0 +1,3451 @@ +## +## Bundle of CA Root Certificates +## +## Certificate data from Mozilla as of: Tue Aug 22 03:12:04 2023 GMT +## +## This is a bundle of X.509 certificates of public Certificate Authorities +## (CA). These were automatically extracted from Mozilla's root certificates +## file (certdata.txt). This file can be found in the mozilla source tree: +## https://hg.mozilla.org/releases/mozilla-release/raw-file/default/security/nss/lib/ckfw/builtins/certdata.txt +## +## It contains the certificates in PEM format and therefore +## can be directly used with curl / libcurl / php_curl, or with +## an Apache+mod_ssl webserver for SSL client authentication. +## Just configure this file as the SSLCACertificateFile. +## +## Conversion done with mk-ca-bundle.pl version 1.29. +## SHA256: 0ff137babc6a5561a9cfbe9f29558972e5b528202681b7d3803d03a3e82922bd +## + + +GlobalSign Root CA +================== +-----BEGIN CERTIFICATE----- +MIIDdTCCAl2gAwIBAgILBAAAAAABFUtaw5QwDQYJKoZIhvcNAQEFBQAwVzELMAkGA1UEBhMCQkUx +GTAXBgNVBAoTEEdsb2JhbFNpZ24gbnYtc2ExEDAOBgNVBAsTB1Jvb3QgQ0ExGzAZBgNVBAMTEkds +b2JhbFNpZ24gUm9vdCBDQTAeFw05ODA5MDExMjAwMDBaFw0yODAxMjgxMjAwMDBaMFcxCzAJBgNV +BAYTAkJFMRkwFwYDVQQKExBHbG9iYWxTaWduIG52LXNhMRAwDgYDVQQLEwdSb290IENBMRswGQYD +VQQDExJHbG9iYWxTaWduIFJvb3QgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDa +DuaZjc6j40+Kfvvxi4Mla+pIH/EqsLmVEQS98GPR4mdmzxzdzxtIK+6NiY6arymAZavpxy0Sy6sc +THAHoT0KMM0VjU/43dSMUBUc71DuxC73/OlS8pF94G3VNTCOXkNz8kHp1Wrjsok6Vjk4bwY8iGlb +Kk3Fp1S4bInMm/k8yuX9ifUSPJJ4ltbcdG6TRGHRjcdGsnUOhugZitVtbNV4FpWi6cgKOOvyJBNP +c1STE4U6G7weNLWLBYy5d4ux2x8gkasJU26Qzns3dLlwR5EiUWMWea6xrkEmCMgZK9FGqkjWZCrX +gzT/LCrBbBlDSgeF59N89iFo7+ryUp9/k5DPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV +HRMBAf8EBTADAQH/MB0GA1UdDgQWBBRge2YaRQ2XyolQL30EzTSo//z9SzANBgkqhkiG9w0BAQUF +AAOCAQEA1nPnfE920I2/7LqivjTFKDK1fPxsnCwrvQmeU79rXqoRSLblCKOzyj1hTdNGCbM+w6Dj +Y1Ub8rrvrTnhQ7k4o+YviiY776BQVvnGCv04zcQLcFGUl5gE38NflNUVyRRBnMRddWQVDf9VMOyG +j/8N7yy5Y0b2qvzfvGn9LhJIZJrglfCm7ymPAbEVtQwdpf5pLGkkeB6zpxxxYu7KyJesF12KwvhH +hm4qxFYxldBniYUr+WymXUadDKqC5JlR3XC321Y9YeRq4VzW9v493kHMB65jUr9TU/Qr6cf9tveC +X4XSQRjbgbMEHMUfpIBvFSDJ3gyICh3WZlXi/EjJKSZp4A== +-----END CERTIFICATE----- + +Entrust.net Premium 2048 Secure Server CA +========================================= +-----BEGIN CERTIFICATE----- +MIIEKjCCAxKgAwIBAgIEOGPe+DANBgkqhkiG9w0BAQUFADCBtDEUMBIGA1UEChMLRW50cnVzdC5u +ZXQxQDA+BgNVBAsUN3d3dy5lbnRydXN0Lm5ldC9DUFNfMjA0OCBpbmNvcnAuIGJ5IHJlZi4gKGxp +bWl0cyBsaWFiLikxJTAjBgNVBAsTHChjKSAxOTk5IEVudHJ1c3QubmV0IExpbWl0ZWQxMzAxBgNV +BAMTKkVudHJ1c3QubmV0IENlcnRpZmljYXRpb24gQXV0aG9yaXR5ICgyMDQ4KTAeFw05OTEyMjQx +NzUwNTFaFw0yOTA3MjQxNDE1MTJaMIG0MRQwEgYDVQQKEwtFbnRydXN0Lm5ldDFAMD4GA1UECxQ3 +d3d3LmVudHJ1c3QubmV0L0NQU18yMDQ4IGluY29ycC4gYnkgcmVmLiAobGltaXRzIGxpYWIuKTEl +MCMGA1UECxMcKGMpIDE5OTkgRW50cnVzdC5uZXQgTGltaXRlZDEzMDEGA1UEAxMqRW50cnVzdC5u +ZXQgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkgKDIwNDgpMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A +MIIBCgKCAQEArU1LqRKGsuqjIAcVFmQqK0vRvwtKTY7tgHalZ7d4QMBzQshowNtTK91euHaYNZOL +Gp18EzoOH1u3Hs/lJBQesYGpjX24zGtLA/ECDNyrpUAkAH90lKGdCCmziAv1h3edVc3kw37XamSr +hRSGlVuXMlBvPci6Zgzj/L24ScF2iUkZ/cCovYmjZy/Gn7xxGWC4LeksyZB2ZnuU4q941mVTXTzW +nLLPKQP5L6RQstRIzgUyVYr9smRMDuSYB3Xbf9+5CFVghTAp+XtIpGmG4zU/HoZdenoVve8AjhUi +VBcAkCaTvA5JaJG/+EfTnZVCwQ5N328mz8MYIWJmQ3DW1cAH4QIDAQABo0IwQDAOBgNVHQ8BAf8E +BAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUVeSB0RGAvtiJuQijMfmhJAkWuXAwDQYJ +KoZIhvcNAQEFBQADggEBADubj1abMOdTmXx6eadNl9cZlZD7Bh/KM3xGY4+WZiT6QBshJ8rmcnPy +T/4xmf3IDExoU8aAghOY+rat2l098c5u9hURlIIM7j+VrxGrD9cv3h8Dj1csHsm7mhpElesYT6Yf +zX1XEC+bBAlahLVu2B064dae0Wx5XnkcFMXj0EyTO2U87d89vqbllRrDtRnDvV5bu/8j72gZyxKT +J1wDLW8w0B62GqzeWvfRqqgnpv55gcR5mTNXuhKwqeBCbJPKVt7+bYQLCIt+jerXmCHG8+c8eS9e +nNFMFY3h7CI3zJpDC5fcgJCNs2ebb0gIFVbPv/ErfF6adulZkMV8gzURZVE= +-----END CERTIFICATE----- + +Baltimore CyberTrust Root +========================= +-----BEGIN CERTIFICATE----- +MIIDdzCCAl+gAwIBAgIEAgAAuTANBgkqhkiG9w0BAQUFADBaMQswCQYDVQQGEwJJRTESMBAGA1UE +ChMJQmFsdGltb3JlMRMwEQYDVQQLEwpDeWJlclRydXN0MSIwIAYDVQQDExlCYWx0aW1vcmUgQ3li +ZXJUcnVzdCBSb290MB4XDTAwMDUxMjE4NDYwMFoXDTI1MDUxMjIzNTkwMFowWjELMAkGA1UEBhMC +SUUxEjAQBgNVBAoTCUJhbHRpbW9yZTETMBEGA1UECxMKQ3liZXJUcnVzdDEiMCAGA1UEAxMZQmFs +dGltb3JlIEN5YmVyVHJ1c3QgUm9vdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKME +uyKrmD1X6CZymrV51Cni4eiVgLGw41uOKymaZN+hXe2wCQVt2yguzmKiYv60iNoS6zjrIZ3AQSsB +UnuId9Mcj8e6uYi1agnnc+gRQKfRzMpijS3ljwumUNKoUMMo6vWrJYeKmpYcqWe4PwzV9/lSEy/C +G9VwcPCPwBLKBsua4dnKM3p31vjsufFoREJIE9LAwqSuXmD+tqYF/LTdB1kC1FkYmGP1pWPgkAx9 +XbIGevOF6uvUA65ehD5f/xXtabz5OTZydc93Uk3zyZAsuT3lySNTPx8kmCFcB5kpvcY67Oduhjpr +l3RjM71oGDHweI12v/yejl0qhqdNkNwnGjkCAwEAAaNFMEMwHQYDVR0OBBYEFOWdWTCCR1jMrPoI +VDaGezq1BE3wMBIGA1UdEwEB/wQIMAYBAf8CAQMwDgYDVR0PAQH/BAQDAgEGMA0GCSqGSIb3DQEB +BQUAA4IBAQCFDF2O5G9RaEIFoN27TyclhAO992T9Ldcw46QQF+vaKSm2eT929hkTI7gQCvlYpNRh +cL0EYWoSihfVCr3FvDB81ukMJY2GQE/szKN+OMY3EU/t3WgxjkzSswF07r51XgdIGn9w/xZchMB5 +hbgF/X++ZRGjD8ACtPhSNzkE1akxehi/oCr0Epn3o0WC4zxe9Z2etciefC7IpJ5OCBRLbf1wbWsa +Y71k5h+3zvDyny67G7fyUIhzksLi4xaNmjICq44Y3ekQEe5+NauQrz4wlHrQMz2nZQ/1/I6eYs9H +RCwBXbsdtTLSR9I4LtD+gdwyah617jzV/OeBHRnDJELqYzmp +-----END CERTIFICATE----- + +Entrust Root Certification Authority +==================================== +-----BEGIN CERTIFICATE----- +MIIEkTCCA3mgAwIBAgIERWtQVDANBgkqhkiG9w0BAQUFADCBsDELMAkGA1UEBhMCVVMxFjAUBgNV +BAoTDUVudHJ1c3QsIEluYy4xOTA3BgNVBAsTMHd3dy5lbnRydXN0Lm5ldC9DUFMgaXMgaW5jb3Jw +b3JhdGVkIGJ5IHJlZmVyZW5jZTEfMB0GA1UECxMWKGMpIDIwMDYgRW50cnVzdCwgSW5jLjEtMCsG +A1UEAxMkRW50cnVzdCBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTA2MTEyNzIwMjM0 +MloXDTI2MTEyNzIwNTM0MlowgbAxCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1FbnRydXN0LCBJbmMu +MTkwNwYDVQQLEzB3d3cuZW50cnVzdC5uZXQvQ1BTIGlzIGluY29ycG9yYXRlZCBieSByZWZlcmVu +Y2UxHzAdBgNVBAsTFihjKSAyMDA2IEVudHJ1c3QsIEluYy4xLTArBgNVBAMTJEVudHJ1c3QgUm9v +dCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB +ALaVtkNC+sZtKm9I35RMOVcF7sN5EUFoNu3s/poBj6E4KPz3EEZmLk0eGrEaTsbRwJWIsMn/MYsz +A9u3g3s+IIRe7bJWKKf44LlAcTfFy0cOlypowCKVYhXbR9n10Cv/gkvJrT7eTNuQgFA/CYqEAOww +Cj0Yzfv9KlmaI5UXLEWeH25DeW0MXJj+SKfFI0dcXv1u5x609mhF0YaDW6KKjbHjKYD+JXGIrb68 +j6xSlkuqUY3kEzEZ6E5Nn9uss2rVvDlUccp6en+Q3X0dgNmBu1kmwhH+5pPi94DkZfs0Nw4pgHBN +rziGLp5/V6+eF67rHMsoIV+2HNjnogQi+dPa2MsCAwEAAaOBsDCBrTAOBgNVHQ8BAf8EBAMCAQYw +DwYDVR0TAQH/BAUwAwEB/zArBgNVHRAEJDAigA8yMDA2MTEyNzIwMjM0MlqBDzIwMjYxMTI3MjA1 +MzQyWjAfBgNVHSMEGDAWgBRokORnpKZTgMeGZqTx90tD+4S9bTAdBgNVHQ4EFgQUaJDkZ6SmU4DH +hmak8fdLQ/uEvW0wHQYJKoZIhvZ9B0EABBAwDhsIVjcuMTo0LjADAgSQMA0GCSqGSIb3DQEBBQUA +A4IBAQCT1DCw1wMgKtD5Y+iRDAUgqV8ZyntyTtSx29CW+1RaGSwMCPeyvIWonX9tO1KzKtvn1ISM +Y/YPyyYBkVBs9F8U4pN0wBOeMDpQ47RgxRzwIkSNcUesyBrJ6ZuaAGAT/3B+XxFNSRuzFVJ7yVTa +v52Vr2ua2J7p8eRDjeIRRDq/r72DQnNSi6q7pynP9WQcCk3RvKqsnyrQ/39/2n3qse0wJcGE2jTS +W3iDVuycNsMm4hH2Z0kdkquM++v/eu6FSqdQgPCnXEqULl8FmTxSQeDNtGPPAUO6nIPcj2A781q0 +tHuu2guQOHXvgR1m0vdXcDazv/wor3ElhVsT/h5/WrQ8 +-----END CERTIFICATE----- + +Comodo AAA Services root +======================== +-----BEGIN CERTIFICATE----- +MIIEMjCCAxqgAwIBAgIBATANBgkqhkiG9w0BAQUFADB7MQswCQYDVQQGEwJHQjEbMBkGA1UECAwS +R3JlYXRlciBNYW5jaGVzdGVyMRAwDgYDVQQHDAdTYWxmb3JkMRowGAYDVQQKDBFDb21vZG8gQ0Eg +TGltaXRlZDEhMB8GA1UEAwwYQUFBIENlcnRpZmljYXRlIFNlcnZpY2VzMB4XDTA0MDEwMTAwMDAw +MFoXDTI4MTIzMTIzNTk1OVowezELMAkGA1UEBhMCR0IxGzAZBgNVBAgMEkdyZWF0ZXIgTWFuY2hl +c3RlcjEQMA4GA1UEBwwHU2FsZm9yZDEaMBgGA1UECgwRQ29tb2RvIENBIExpbWl0ZWQxITAfBgNV +BAMMGEFBQSBDZXJ0aWZpY2F0ZSBTZXJ2aWNlczCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC +ggEBAL5AnfRu4ep2hxxNRUSOvkbIgwadwSr+GB+O5AL686tdUIoWMQuaBtDFcCLNSS1UY8y2bmhG +C1Pqy0wkwLxyTurxFa70VJoSCsN6sjNg4tqJVfMiWPPe3M/vg4aijJRPn2jymJBGhCfHdr/jzDUs +i14HZGWCwEiwqJH5YZ92IFCokcdmtet4YgNW8IoaE+oxox6gmf049vYnMlhvB/VruPsUK6+3qszW +Y19zjNoFmag4qMsXeDZRrOme9Hg6jc8P2ULimAyrL58OAd7vn5lJ8S3frHRNG5i1R8XlKdH5kBjH +Ypy+g8cmez6KJcfA3Z3mNWgQIJ2P2N7Sw4ScDV7oL8kCAwEAAaOBwDCBvTAdBgNVHQ4EFgQUoBEK +Iz6W8Qfs4q8p74Klf9AwpLQwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wewYDVR0f +BHQwcjA4oDagNIYyaHR0cDovL2NybC5jb21vZG9jYS5jb20vQUFBQ2VydGlmaWNhdGVTZXJ2aWNl +cy5jcmwwNqA0oDKGMGh0dHA6Ly9jcmwuY29tb2RvLm5ldC9BQUFDZXJ0aWZpY2F0ZVNlcnZpY2Vz +LmNybDANBgkqhkiG9w0BAQUFAAOCAQEACFb8AvCb6P+k+tZ7xkSAzk/ExfYAWMymtrwUSWgEdujm +7l3sAg9g1o1QGE8mTgHj5rCl7r+8dFRBv/38ErjHT1r0iWAFf2C3BUrz9vHCv8S5dIa2LX1rzNLz +Rt0vxuBqw8M0Ayx9lt1awg6nCpnBBYurDC/zXDrPbDdVCYfeU0BsWO/8tqtlbgT2G9w84FoVxp7Z +8VlIMCFlA2zs6SFz7JsDoeA3raAVGI/6ugLOpyypEBMs1OUIJqsil2D4kF501KKaU73yqWjgom7C +12yxow+ev+to51byrvLjKzg6CYG1a4XXvi3tPxq3smPi9WIsgtRqAEFQ8TmDn5XpNpaYbg== +-----END CERTIFICATE----- + +QuoVadis Root CA 2 +================== +-----BEGIN CERTIFICATE----- +MIIFtzCCA5+gAwIBAgICBQkwDQYJKoZIhvcNAQEFBQAwRTELMAkGA1UEBhMCQk0xGTAXBgNVBAoT +EFF1b1ZhZGlzIExpbWl0ZWQxGzAZBgNVBAMTElF1b1ZhZGlzIFJvb3QgQ0EgMjAeFw0wNjExMjQx +ODI3MDBaFw0zMTExMjQxODIzMzNaMEUxCzAJBgNVBAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBM +aW1pdGVkMRswGQYDVQQDExJRdW9WYWRpcyBSb290IENBIDIwggIiMA0GCSqGSIb3DQEBAQUAA4IC +DwAwggIKAoICAQCaGMpLlA0ALa8DKYrwD4HIrkwZhR0In6spRIXzL4GtMh6QRr+jhiYaHv5+HBg6 +XJxgFyo6dIMzMH1hVBHL7avg5tKifvVrbxi3Cgst/ek+7wrGsxDp3MJGF/hd/aTa/55JWpzmM+Yk +lvc/ulsrHHo1wtZn/qtmUIttKGAr79dgw8eTvI02kfN/+NsRE8Scd3bBrrcCaoF6qUWD4gXmuVbB +lDePSHFjIuwXZQeVikvfj8ZaCuWw419eaxGrDPmF60Tp+ARz8un+XJiM9XOva7R+zdRcAitMOeGy +lZUtQofX1bOQQ7dsE/He3fbE+Ik/0XX1ksOR1YqI0JDs3G3eicJlcZaLDQP9nL9bFqyS2+r+eXyt +66/3FsvbzSUr5R/7mp/iUcw6UwxI5g69ybR2BlLmEROFcmMDBOAENisgGQLodKcftslWZvB1Jdxn +wQ5hYIizPtGo/KPaHbDRsSNU30R2be1B2MGyIrZTHN81Hdyhdyox5C315eXbyOD/5YDXC2Og/zOh +D7osFRXql7PSorW+8oyWHhqPHWykYTe5hnMz15eWniN9gqRMgeKh0bpnX5UHoycR7hYQe7xFSkyy +BNKr79X9DFHOUGoIMfmR2gyPZFwDwzqLID9ujWc9Otb+fVuIyV77zGHcizN300QyNQliBJIWENie +J0f7OyHj+OsdWwIDAQABo4GwMIGtMA8GA1UdEwEB/wQFMAMBAf8wCwYDVR0PBAQDAgEGMB0GA1Ud +DgQWBBQahGK8SEwzJQTU7tD2A8QZRtGUazBuBgNVHSMEZzBlgBQahGK8SEwzJQTU7tD2A8QZRtGU +a6FJpEcwRTELMAkGA1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxGzAZBgNVBAMT +ElF1b1ZhZGlzIFJvb3QgQ0EgMoICBQkwDQYJKoZIhvcNAQEFBQADggIBAD4KFk2fBluornFdLwUv +Z+YTRYPENvbzwCYMDbVHZF34tHLJRqUDGCdViXh9duqWNIAXINzng/iN/Ae42l9NLmeyhP3ZRPx3 +UIHmfLTJDQtyU/h2BwdBR5YM++CCJpNVjP4iH2BlfF/nJrP3MpCYUNQ3cVX2kiF495V5+vgtJodm +VjB3pjd4M1IQWK4/YY7yarHvGH5KWWPKjaJW1acvvFYfzznB4vsKqBUsfU16Y8Zsl0Q80m/DShcK ++JDSV6IZUaUtl0HaB0+pUNqQjZRG4T7wlP0QADj1O+hA4bRuVhogzG9Yje0uRY/W6ZM/57Es3zrW +IozchLsib9D45MY56QSIPMO661V6bYCZJPVsAfv4l7CUW+v90m/xd2gNNWQjrLhVoQPRTUIZ3Ph1 +WVaj+ahJefivDrkRoHy3au000LYmYjgahwz46P0u05B/B5EqHdZ+XIWDmbA4CD/pXvk1B+TJYm5X +f6dQlfe6yJvmjqIBxdZmv3lh8zwc4bmCXF2gw+nYSL0ZohEUGW6yhhtoPkg3Goi3XZZenMfvJ2II +4pEZXNLxId26F0KCl3GBUzGpn/Z9Yr9y4aOTHcyKJloJONDO1w2AFrR4pTqHTI2KpdVGl/IsELm8 +VCLAAVBpQ570su9t+Oza8eOx79+Rj1QqCyXBJhnEUhAFZdWCEOrCMc0u +-----END CERTIFICATE----- + +QuoVadis Root CA 3 +================== +-----BEGIN CERTIFICATE----- +MIIGnTCCBIWgAwIBAgICBcYwDQYJKoZIhvcNAQEFBQAwRTELMAkGA1UEBhMCQk0xGTAXBgNVBAoT +EFF1b1ZhZGlzIExpbWl0ZWQxGzAZBgNVBAMTElF1b1ZhZGlzIFJvb3QgQ0EgMzAeFw0wNjExMjQx +OTExMjNaFw0zMTExMjQxOTA2NDRaMEUxCzAJBgNVBAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBM +aW1pdGVkMRswGQYDVQQDExJRdW9WYWRpcyBSb290IENBIDMwggIiMA0GCSqGSIb3DQEBAQUAA4IC +DwAwggIKAoICAQDMV0IWVJzmmNPTTe7+7cefQzlKZbPoFog02w1ZkXTPkrgEQK0CSzGrvI2RaNgg +DhoB4hp7Thdd4oq3P5kazethq8Jlph+3t723j/z9cI8LoGe+AaJZz3HmDyl2/7FWeUUrH556VOij +KTVopAFPD6QuN+8bv+OPEKhyq1hX51SGyMnzW9os2l2ObjyjPtr7guXd8lyyBTNvijbO0BNO/79K +DDRMpsMhvVAEVeuxu537RR5kFd5VAYwCdrXLoT9CabwvvWhDFlaJKjdhkf2mrk7AyxRllDdLkgbv +BNDInIjbC3uBr7E9KsRlOni27tyAsdLTmZw67mtaa7ONt9XOnMK+pUsvFrGeaDsGb659n/je7Mwp +p5ijJUMv7/FfJuGITfhebtfZFG4ZM2mnO4SJk8RTVROhUXhA+LjJou57ulJCg54U7QVSWllWp5f8 +nT8KKdjcT5EOE7zelaTfi5m+rJsziO+1ga8bxiJTyPbH7pcUsMV8eFLI8M5ud2CEpukqdiDtWAEX +MJPpGovgc2PZapKUSU60rUqFxKMiMPwJ7Wgic6aIDFUhWMXhOp8q3crhkODZc6tsgLjoC2SToJyM +Gf+z0gzskSaHirOi4XCPLArlzW1oUevaPwV/izLmE1xr/l9A4iLItLRkT9a6fUg+qGkM17uGcclz +uD87nSVL2v9A6wIDAQABo4IBlTCCAZEwDwYDVR0TAQH/BAUwAwEB/zCB4QYDVR0gBIHZMIHWMIHT +BgkrBgEEAb5YAAMwgcUwgZMGCCsGAQUFBwICMIGGGoGDQW55IHVzZSBvZiB0aGlzIENlcnRpZmlj +YXRlIGNvbnN0aXR1dGVzIGFjY2VwdGFuY2Ugb2YgdGhlIFF1b1ZhZGlzIFJvb3QgQ0EgMyBDZXJ0 +aWZpY2F0ZSBQb2xpY3kgLyBDZXJ0aWZpY2F0aW9uIFByYWN0aWNlIFN0YXRlbWVudC4wLQYIKwYB +BQUHAgEWIWh0dHA6Ly93d3cucXVvdmFkaXNnbG9iYWwuY29tL2NwczALBgNVHQ8EBAMCAQYwHQYD +VR0OBBYEFPLAE+CCQz777i9nMpY1XNu4ywLQMG4GA1UdIwRnMGWAFPLAE+CCQz777i9nMpY1XNu4 +ywLQoUmkRzBFMQswCQYDVQQGEwJCTTEZMBcGA1UEChMQUXVvVmFkaXMgTGltaXRlZDEbMBkGA1UE +AxMSUXVvVmFkaXMgUm9vdCBDQSAzggIFxjANBgkqhkiG9w0BAQUFAAOCAgEAT62gLEz6wPJv92ZV +qyM07ucp2sNbtrCD2dDQ4iH782CnO11gUyeim/YIIirnv6By5ZwkajGxkHon24QRiSemd1o417+s +hvzuXYO8BsbRd2sPbSQvS3pspweWyuOEn62Iix2rFo1bZhfZFvSLgNLd+LJ2w/w4E6oM3kJpK27z +POuAJ9v1pkQNn1pVWQvVDVJIxa6f8i+AxeoyUDUSly7B4f/xI4hROJ/yZlZ25w9Rl6VSDE1JUZU2 +Pb+iSwwQHYaZTKrzchGT5Or2m9qoXadNt54CrnMAyNojA+j56hl0YgCUyyIgvpSnWbWCar6ZeXqp +8kokUvd0/bpO5qgdAm6xDYBEwa7TIzdfu4V8K5Iu6H6li92Z4b8nby1dqnuH/grdS/yO9SbkbnBC +bjPsMZ57k8HkyWkaPcBrTiJt7qtYTcbQQcEr6k8Sh17rRdhs9ZgC06DYVYoGmRmioHfRMJ6szHXu +g/WwYjnPbFfiTNKRCw51KBuav/0aQ/HKd/s7j2G4aSgWQgRecCocIdiP4b0jWy10QJLZYxkNc91p +vGJHvOB0K7Lrfb5BG7XARsWhIstfTsEokt4YutUqKLsRixeTmJlglFwjz1onl14LBQaTNx47aTbr +qZ5hHY8y2o4M1nQ+ewkk2gF3R8Q7zTSMmfXK4SVhM7JZG+Ju1zdXtg2pEto= +-----END CERTIFICATE----- + +Security Communication Root CA +============================== +-----BEGIN CERTIFICATE----- +MIIDWjCCAkKgAwIBAgIBADANBgkqhkiG9w0BAQUFADBQMQswCQYDVQQGEwJKUDEYMBYGA1UEChMP +U0VDT00gVHJ1c3QubmV0MScwJQYDVQQLEx5TZWN1cml0eSBDb21tdW5pY2F0aW9uIFJvb3RDQTEw +HhcNMDMwOTMwMDQyMDQ5WhcNMjMwOTMwMDQyMDQ5WjBQMQswCQYDVQQGEwJKUDEYMBYGA1UEChMP +U0VDT00gVHJ1c3QubmV0MScwJQYDVQQLEx5TZWN1cml0eSBDb21tdW5pY2F0aW9uIFJvb3RDQTEw +ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCzs/5/022x7xZ8V6UMbXaKL0u/ZPtM7orw +8yl89f/uKuDp6bpbZCKamm8sOiZpUQWZJtzVHGpxxpp9Hp3dfGzGjGdnSj74cbAZJ6kJDKaVv0uM +DPpVmDvY6CKhS3E4eayXkmmziX7qIWgGmBSWh9JhNrxtJ1aeV+7AwFb9Ms+k2Y7CI9eNqPPYJayX +5HA49LY6tJ07lyZDo6G8SVlyTCMwhwFY9k6+HGhWZq/NQV3Is00qVUarH9oe4kA92819uZKAnDfd +DJZkndwi92SL32HeFZRSFaB9UslLqCHJxrHty8OVYNEP8Ktw+N/LTX7s1vqr2b1/VPKl6Xn62dZ2 +JChzAgMBAAGjPzA9MB0GA1UdDgQWBBSgc0mZaNyFW2XjmygvV5+9M7wHSDALBgNVHQ8EBAMCAQYw +DwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQUFAAOCAQEAaECpqLvkT115swW1F7NgE+vGkl3g +0dNq/vu+m22/xwVtWSDEHPC32oRYAmP6SBbvT6UL90qY8j+eG61Ha2POCEfrUj94nK9NrvjVT8+a +mCoQQTlSxN3Zmw7vkwGusi7KaEIkQmywszo+zenaSMQVy+n5Bw+SUEmK3TGXX8npN6o7WWWXlDLJ +s58+OmJYxUmtYg5xpTKqL8aJdkNAExNnPaJUJRDL8Try2frbSVa7pv6nQTXD4IhhyYjH3zYQIphZ +6rBK+1YWc26sTfcioU+tHXotRSflMMFe8toTyyVCUZVHA4xsIcx0Qu1T/zOLjw9XARYvz6buyXAi +FL39vmwLAw== +-----END CERTIFICATE----- + +XRamp Global CA Root +==================== +-----BEGIN CERTIFICATE----- +MIIEMDCCAxigAwIBAgIQUJRs7Bjq1ZxN1ZfvdY+grTANBgkqhkiG9w0BAQUFADCBgjELMAkGA1UE +BhMCVVMxHjAcBgNVBAsTFXd3dy54cmFtcHNlY3VyaXR5LmNvbTEkMCIGA1UEChMbWFJhbXAgU2Vj +dXJpdHkgU2VydmljZXMgSW5jMS0wKwYDVQQDEyRYUmFtcCBHbG9iYWwgQ2VydGlmaWNhdGlvbiBB +dXRob3JpdHkwHhcNMDQxMTAxMTcxNDA0WhcNMzUwMTAxMDUzNzE5WjCBgjELMAkGA1UEBhMCVVMx +HjAcBgNVBAsTFXd3dy54cmFtcHNlY3VyaXR5LmNvbTEkMCIGA1UEChMbWFJhbXAgU2VjdXJpdHkg +U2VydmljZXMgSW5jMS0wKwYDVQQDEyRYUmFtcCBHbG9iYWwgQ2VydGlmaWNhdGlvbiBBdXRob3Jp +dHkwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCYJB69FbS638eMpSe2OAtp87ZOqCwu +IR1cRN8hXX4jdP5efrRKt6atH67gBhbim1vZZ3RrXYCPKZ2GG9mcDZhtdhAoWORlsH9KmHmf4MMx +foArtYzAQDsRhtDLooY2YKTVMIJt2W7QDxIEM5dfT2Fa8OT5kavnHTu86M/0ay00fOJIYRyO82FE +zG+gSqmUsE3a56k0enI4qEHMPJQRfevIpoy3hsvKMzvZPTeL+3o+hiznc9cKV6xkmxnr9A8ECIqs +AxcZZPRaJSKNNCyy9mgdEm3Tih4U2sSPpuIjhdV6Db1q4Ons7Be7QhtnqiXtRYMh/MHJfNViPvry +xS3T/dRlAgMBAAGjgZ8wgZwwEwYJKwYBBAGCNxQCBAYeBABDAEEwCwYDVR0PBAQDAgGGMA8GA1Ud +EwEB/wQFMAMBAf8wHQYDVR0OBBYEFMZPoj0GY4QJnM5i5ASsjVy16bYbMDYGA1UdHwQvMC0wK6Ap +oCeGJWh0dHA6Ly9jcmwueHJhbXBzZWN1cml0eS5jb20vWEdDQS5jcmwwEAYJKwYBBAGCNxUBBAMC +AQEwDQYJKoZIhvcNAQEFBQADggEBAJEVOQMBG2f7Shz5CmBbodpNl2L5JFMn14JkTpAuw0kbK5rc +/Kh4ZzXxHfARvbdI4xD2Dd8/0sm2qlWkSLoC295ZLhVbO50WfUfXN+pfTXYSNrsf16GBBEYgoyxt +qZ4Bfj8pzgCT3/3JknOJiWSe5yvkHJEs0rnOfc5vMZnT5r7SHpDwCRR5XCOrTdLaIR9NmXmd4c8n +nxCbHIgNsIpkQTG4DmyQJKSbXHGPurt+HBvbaoAPIbzp26a3QPSyi6mx5O+aGtA9aZnuqCij4Tyz +8LIRnM98QObd50N9otg6tamN8jSZxNQQ4Qb9CYQQO+7ETPTsJ3xCwnR8gooJybQDJbw= +-----END CERTIFICATE----- + +Go Daddy Class 2 CA +=================== +-----BEGIN CERTIFICATE----- +MIIEADCCAuigAwIBAgIBADANBgkqhkiG9w0BAQUFADBjMQswCQYDVQQGEwJVUzEhMB8GA1UEChMY +VGhlIEdvIERhZGR5IEdyb3VwLCBJbmMuMTEwLwYDVQQLEyhHbyBEYWRkeSBDbGFzcyAyIENlcnRp +ZmljYXRpb24gQXV0aG9yaXR5MB4XDTA0MDYyOTE3MDYyMFoXDTM0MDYyOTE3MDYyMFowYzELMAkG +A1UEBhMCVVMxITAfBgNVBAoTGFRoZSBHbyBEYWRkeSBHcm91cCwgSW5jLjExMC8GA1UECxMoR28g +RGFkZHkgQ2xhc3MgMiBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTCCASAwDQYJKoZIhvcNAQEBBQAD +ggENADCCAQgCggEBAN6d1+pXGEmhW+vXX0iG6r7d/+TvZxz0ZWizV3GgXne77ZtJ6XCAPVYYYwhv +2vLM0D9/AlQiVBDYsoHUwHU9S3/Hd8M+eKsaA7Ugay9qK7HFiH7Eux6wwdhFJ2+qN1j3hybX2C32 +qRe3H3I2TqYXP2WYktsqbl2i/ojgC95/5Y0V4evLOtXiEqITLdiOr18SPaAIBQi2XKVlOARFmR6j +YGB0xUGlcmIbYsUfb18aQr4CUWWoriMYavx4A6lNf4DD+qta/KFApMoZFv6yyO9ecw3ud72a9nmY +vLEHZ6IVDd2gWMZEewo+YihfukEHU1jPEX44dMX4/7VpkI+EdOqXG68CAQOjgcAwgb0wHQYDVR0O +BBYEFNLEsNKR1EwRcbNhyz2h/t2oatTjMIGNBgNVHSMEgYUwgYKAFNLEsNKR1EwRcbNhyz2h/t2o +atTjoWekZTBjMQswCQYDVQQGEwJVUzEhMB8GA1UEChMYVGhlIEdvIERhZGR5IEdyb3VwLCBJbmMu +MTEwLwYDVQQLEyhHbyBEYWRkeSBDbGFzcyAyIENlcnRpZmljYXRpb24gQXV0aG9yaXR5ggEAMAwG +A1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBADJL87LKPpH8EsahB4yOd6AzBhRckB4Y9wim +PQoZ+YeAEW5p5JYXMP80kWNyOO7MHAGjHZQopDH2esRU1/blMVgDoszOYtuURXO1v0XJJLXVggKt +I3lpjbi2Tc7PTMozI+gciKqdi0FuFskg5YmezTvacPd+mSYgFFQlq25zheabIZ0KbIIOqPjCDPoQ +HmyW74cNxA9hi63ugyuV+I6ShHI56yDqg+2DzZduCLzrTia2cyvk0/ZM/iZx4mERdEr/VxqHD3VI +Ls9RaRegAhJhldXRQLIQTO7ErBBDpqWeCtWVYpoNz4iCxTIM5CufReYNnyicsbkqWletNw+vHX/b +vZ8= +-----END CERTIFICATE----- + +Starfield Class 2 CA +==================== +-----BEGIN CERTIFICATE----- +MIIEDzCCAvegAwIBAgIBADANBgkqhkiG9w0BAQUFADBoMQswCQYDVQQGEwJVUzElMCMGA1UEChMc +U3RhcmZpZWxkIFRlY2hub2xvZ2llcywgSW5jLjEyMDAGA1UECxMpU3RhcmZpZWxkIENsYXNzIDIg +Q2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMDQwNjI5MTczOTE2WhcNMzQwNjI5MTczOTE2WjBo +MQswCQYDVQQGEwJVUzElMCMGA1UEChMcU3RhcmZpZWxkIFRlY2hub2xvZ2llcywgSW5jLjEyMDAG +A1UECxMpU3RhcmZpZWxkIENsYXNzIDIgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwggEgMA0GCSqG +SIb3DQEBAQUAA4IBDQAwggEIAoIBAQC3Msj+6XGmBIWtDBFk385N78gDGIc/oav7PKaf8MOh2tTY +bitTkPskpD6E8J7oX+zlJ0T1KKY/e97gKvDIr1MvnsoFAZMej2YcOadN+lq2cwQlZut3f+dZxkqZ +JRRU6ybH838Z1TBwj6+wRir/resp7defqgSHo9T5iaU0X9tDkYI22WY8sbi5gv2cOj4QyDvvBmVm +epsZGD3/cVE8MC5fvj13c7JdBmzDI1aaK4UmkhynArPkPw2vCHmCuDY96pzTNbO8acr1zJ3o/WSN +F4Azbl5KXZnJHoe0nRrA1W4TNSNe35tfPe/W93bC6j67eA0cQmdrBNj41tpvi/JEoAGrAgEDo4HF +MIHCMB0GA1UdDgQWBBS/X7fRzt0fhvRbVazc1xDCDqmI5zCBkgYDVR0jBIGKMIGHgBS/X7fRzt0f +hvRbVazc1xDCDqmI56FspGowaDELMAkGA1UEBhMCVVMxJTAjBgNVBAoTHFN0YXJmaWVsZCBUZWNo +bm9sb2dpZXMsIEluYy4xMjAwBgNVBAsTKVN0YXJmaWVsZCBDbGFzcyAyIENlcnRpZmljYXRpb24g +QXV0aG9yaXR5ggEAMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAAWdP4id0ckaVaGs +afPzWdqbAYcaT1epoXkJKtv3L7IezMdeatiDh6GX70k1PncGQVhiv45YuApnP+yz3SFmH8lU+nLM +PUxA2IGvd56Deruix/U0F47ZEUD0/CwqTRV/p2JdLiXTAAsgGh1o+Re49L2L7ShZ3U0WixeDyLJl +xy16paq8U4Zt3VekyvggQQto8PT7dL5WXXp59fkdheMtlb71cZBDzI0fmgAKhynpVSJYACPq4xJD +KVtHCN2MQWplBqjlIapBtJUhlbl90TSrE9atvNziPTnNvT51cKEYWQPJIrSPnNVeKtelttQKbfi3 +QBFGmh95DmK/D5fs4C8fF5Q= +-----END CERTIFICATE----- + +DigiCert Assured ID Root CA +=========================== +-----BEGIN CERTIFICATE----- +MIIDtzCCAp+gAwIBAgIQDOfg5RfYRv6P5WD8G/AwOTANBgkqhkiG9w0BAQUFADBlMQswCQYDVQQG +EwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMSQw +IgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgQ0EwHhcNMDYxMTEwMDAwMDAwWhcNMzEx +MTEwMDAwMDAwWjBlMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQL +ExB3d3cuZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgQ0Ew +ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCtDhXO5EOAXLGH87dg+XESpa7cJpSIqvTO +9SA5KFhgDPiA2qkVlTJhPLWxKISKityfCgyDF3qPkKyK53lTXDGEKvYPmDI2dsze3Tyoou9q+yHy +UmHfnyDXH+Kx2f4YZNISW1/5WBg1vEfNoTb5a3/UsDg+wRvDjDPZ2C8Y/igPs6eD1sNuRMBhNZYW +/lmci3Zt1/GiSw0r/wty2p5g0I6QNcZ4VYcgoc/lbQrISXwxmDNsIumH0DJaoroTghHtORedmTpy +oeb6pNnVFzF1roV9Iq4/AUaG9ih5yLHa5FcXxH4cDrC0kqZWs72yl+2qp/C3xag/lRbQ/6GW6whf +GHdPAgMBAAGjYzBhMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBRF +66Kv9JLLgjEtUYunpyGd823IDzAfBgNVHSMEGDAWgBRF66Kv9JLLgjEtUYunpyGd823IDzANBgkq +hkiG9w0BAQUFAAOCAQEAog683+Lt8ONyc3pklL/3cmbYMuRCdWKuh+vy1dneVrOfzM4UKLkNl2Bc +EkxY5NM9g0lFWJc1aRqoR+pWxnmrEthngYTffwk8lOa4JiwgvT2zKIn3X/8i4peEH+ll74fg38Fn +SbNd67IJKusm7Xi+fT8r87cmNW1fiQG2SVufAQWbqz0lwcy2f8Lxb4bG+mRo64EtlOtCt/qMHt1i +8b5QZ7dsvfPxH2sMNgcWfzd8qVttevESRmCD1ycEvkvOl77DZypoEd+A5wwzZr8TDRRu838fYxAe ++o0bJW1sj6W3YQGx0qMmoRBxna3iw/nDmVG3KwcIzi7mULKn+gpFL6Lw8g== +-----END CERTIFICATE----- + +DigiCert Global Root CA +======================= +-----BEGIN CERTIFICATE----- +MIIDrzCCApegAwIBAgIQCDvgVpBCRrGhdWrJWZHHSjANBgkqhkiG9w0BAQUFADBhMQswCQYDVQQG +EwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMSAw +HgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBDQTAeFw0wNjExMTAwMDAwMDBaFw0zMTExMTAw +MDAwMDBaMGExCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3 +dy5kaWdpY2VydC5jb20xIDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IENBMIIBIjANBgkq +hkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4jvhEXLeqKTTo1eqUKKPC3eQyaKl7hLOllsBCSDMAZOn +TjC3U/dDxGkAV53ijSLdhwZAAIEJzs4bg7/fzTtxRuLWZscFs3YnFo97nh6Vfe63SKMI2tavegw5 +BmV/Sl0fvBf4q77uKNd0f3p4mVmFaG5cIzJLv07A6Fpt43C/dxC//AH2hdmoRBBYMql1GNXRor5H +4idq9Joz+EkIYIvUX7Q6hL+hqkpMfT7PT19sdl6gSzeRntwi5m3OFBqOasv+zbMUZBfHWymeMr/y +7vrTC0LUq7dBMtoM1O/4gdW7jVg/tRvoSSiicNoxBN33shbyTApOB6jtSj1etX+jkMOvJwIDAQAB +o2MwYTAOBgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUA95QNVbRTLtm +8KPiGxvDl7I90VUwHwYDVR0jBBgwFoAUA95QNVbRTLtm8KPiGxvDl7I90VUwDQYJKoZIhvcNAQEF +BQADggEBAMucN6pIExIK+t1EnE9SsPTfrgT1eXkIoyQY/EsrhMAtudXH/vTBH1jLuG2cenTnmCmr +EbXjcKChzUyImZOMkXDiqw8cvpOp/2PV5Adg06O/nVsJ8dWO41P0jmP6P6fbtGbfYmbW0W5BjfIt +tep3Sp+dWOIrWcBAI+0tKIJFPnlUkiaY4IBIqDfv8NZ5YBberOgOzW6sRBc4L0na4UU+Krk2U886 +UAb3LujEV0lsYSEY1QSteDwsOoBrp+uvFRTp2InBuThs4pFsiv9kuXclVzDAGySj4dzp30d8tbQk +CAUw7C29C79Fv1C5qfPrmAESrciIxpg0X40KPMbp1ZWVbd4= +-----END CERTIFICATE----- + +DigiCert High Assurance EV Root CA +================================== +-----BEGIN CERTIFICATE----- +MIIDxTCCAq2gAwIBAgIQAqxcJmoLQJuPC3nyrkYldzANBgkqhkiG9w0BAQUFADBsMQswCQYDVQQG +EwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMSsw +KQYDVQQDEyJEaWdpQ2VydCBIaWdoIEFzc3VyYW5jZSBFViBSb290IENBMB4XDTA2MTExMDAwMDAw +MFoXDTMxMTExMDAwMDAwMFowbDELMAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZ +MBcGA1UECxMQd3d3LmRpZ2ljZXJ0LmNvbTErMCkGA1UEAxMiRGlnaUNlcnQgSGlnaCBBc3N1cmFu +Y2UgRVYgUm9vdCBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMbM5XPm+9S75S0t +Mqbf5YE/yc0lSbZxKsPVlDRnogocsF9ppkCxxLeyj9CYpKlBWTrT3JTWPNt0OKRKzE0lgvdKpVMS +OO7zSW1xkX5jtqumX8OkhPhPYlG++MXs2ziS4wblCJEMxChBVfvLWokVfnHoNb9Ncgk9vjo4UFt3 +MRuNs8ckRZqnrG0AFFoEt7oT61EKmEFBIk5lYYeBQVCmeVyJ3hlKV9Uu5l0cUyx+mM0aBhakaHPQ +NAQTXKFx01p8VdteZOE3hzBWBOURtCmAEvF5OYiiAhF8J2a3iLd48soKqDirCmTCv2ZdlYTBoSUe +h10aUAsgEsxBu24LUTi4S8sCAwEAAaNjMGEwDgYDVR0PAQH/BAQDAgGGMA8GA1UdEwEB/wQFMAMB +Af8wHQYDVR0OBBYEFLE+w2kD+L9HAdSYJhoIAu9jZCvDMB8GA1UdIwQYMBaAFLE+w2kD+L9HAdSY +JhoIAu9jZCvDMA0GCSqGSIb3DQEBBQUAA4IBAQAcGgaX3NecnzyIZgYIVyHbIUf4KmeqvxgydkAQ +V8GK83rZEWWONfqe/EW1ntlMMUu4kehDLI6zeM7b41N5cdblIZQB2lWHmiRk9opmzN6cN82oNLFp +myPInngiK3BD41VHMWEZ71jFhS9OMPagMRYjyOfiZRYzy78aG6A9+MpeizGLYAiJLQwGXFK3xPkK +mNEVX58Svnw2Yzi9RKR/5CYrCsSXaQ3pjOLAEFe4yHYSkVXySGnYvCoCWw9E1CAx2/S6cCZdkGCe +vEsXCS+0yx5DaMkHJ8HSXPfqIbloEpw8nL+e/IBcm2PN7EeqJSdnoDfzAIJ9VNep+OkuE6N36B9K +-----END CERTIFICATE----- + +SwissSign Gold CA - G2 +====================== +-----BEGIN CERTIFICATE----- +MIIFujCCA6KgAwIBAgIJALtAHEP1Xk+wMA0GCSqGSIb3DQEBBQUAMEUxCzAJBgNVBAYTAkNIMRUw +EwYDVQQKEwxTd2lzc1NpZ24gQUcxHzAdBgNVBAMTFlN3aXNzU2lnbiBHb2xkIENBIC0gRzIwHhcN +MDYxMDI1MDgzMDM1WhcNMzYxMDI1MDgzMDM1WjBFMQswCQYDVQQGEwJDSDEVMBMGA1UEChMMU3dp +c3NTaWduIEFHMR8wHQYDVQQDExZTd2lzc1NpZ24gR29sZCBDQSAtIEcyMIICIjANBgkqhkiG9w0B +AQEFAAOCAg8AMIICCgKCAgEAr+TufoskDhJuqVAtFkQ7kpJcyrhdhJJCEyq8ZVeCQD5XJM1QiyUq +t2/876LQwB8CJEoTlo8jE+YoWACjR8cGp4QjK7u9lit/VcyLwVcfDmJlD909Vopz2q5+bbqBHH5C +jCA12UNNhPqE21Is8w4ndwtrvxEvcnifLtg+5hg3Wipy+dpikJKVyh+c6bM8K8vzARO/Ws/BtQpg +vd21mWRTuKCWs2/iJneRjOBiEAKfNA+k1ZIzUd6+jbqEemA8atufK+ze3gE/bk3lUIbLtK/tREDF +ylqM2tIrfKjuvqblCqoOpd8FUrdVxyJdMmqXl2MT28nbeTZ7hTpKxVKJ+STnnXepgv9VHKVxaSvR +AiTysybUa9oEVeXBCsdtMDeQKuSeFDNeFhdVxVu1yzSJkvGdJo+hB9TGsnhQ2wwMC3wLjEHXuend +jIj3o02yMszYF9rNt85mndT9Xv+9lz4pded+p2JYryU0pUHHPbwNUMoDAw8IWh+Vc3hiv69yFGkO +peUDDniOJihC8AcLYiAQZzlG+qkDzAQ4embvIIO1jEpWjpEA/I5cgt6IoMPiaG59je883WX0XaxR +7ySArqpWl2/5rX3aYT+YdzylkbYcjCbaZaIJbcHiVOO5ykxMgI93e2CaHt+28kgeDrpOVG2Y4OGi +GqJ3UM/EY5LsRxmd6+ZrzsECAwEAAaOBrDCBqTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUw +AwEB/zAdBgNVHQ4EFgQUWyV7lqRlUX64OfPAeGZe6Drn8O4wHwYDVR0jBBgwFoAUWyV7lqRlUX64 +OfPAeGZe6Drn8O4wRgYDVR0gBD8wPTA7BglghXQBWQECAQEwLjAsBggrBgEFBQcCARYgaHR0cDov +L3JlcG9zaXRvcnkuc3dpc3NzaWduLmNvbS8wDQYJKoZIhvcNAQEFBQADggIBACe645R88a7A3hfm +5djV9VSwg/S7zV4Fe0+fdWavPOhWfvxyeDgD2StiGwC5+OlgzczOUYrHUDFu4Up+GC9pWbY9ZIEr +44OE5iKHjn3g7gKZYbge9LgriBIWhMIxkziWMaa5O1M/wySTVltpkuzFwbs4AOPsF6m43Md8AYOf +Mke6UiI0HTJ6CVanfCU2qT1L2sCCbwq7EsiHSycR+R4tx5M/nttfJmtS2S6K8RTGRI0Vqbe/vd6m +Gu6uLftIdxf+u+yvGPUqUfA5hJeVbG4bwyvEdGB5JbAKJ9/fXtI5z0V9QkvfsywexcZdylU6oJxp +mo/a77KwPJ+HbBIrZXAVUjEaJM9vMSNQH4xPjyPDdEFjHFWoFN0+4FFQz/EbMFYOkrCChdiDyyJk +vC24JdVUorgG6q2SpCSgwYa1ShNqR88uC1aVVMvOmttqtKay20EIhid392qgQmwLOM7XdVAyksLf +KzAiSNDVQTglXaTpXZ/GlHXQRf0wl0OPkKsKx4ZzYEppLd6leNcG2mqeSz53OiATIgHQv2ieY2Br +NU0LbbqhPcCT4H8js1WtciVORvnSFu+wZMEBnunKoGqYDs/YYPIvSbjkQuE4NRb0yG5P94FW6Lqj +viOvrv1vA+ACOzB2+httQc8Bsem4yWb02ybzOqR08kkkW8mw0FfB+j564ZfJ +-----END CERTIFICATE----- + +SwissSign Silver CA - G2 +======================== +-----BEGIN CERTIFICATE----- +MIIFvTCCA6WgAwIBAgIITxvUL1S7L0swDQYJKoZIhvcNAQEFBQAwRzELMAkGA1UEBhMCQ0gxFTAT +BgNVBAoTDFN3aXNzU2lnbiBBRzEhMB8GA1UEAxMYU3dpc3NTaWduIFNpbHZlciBDQSAtIEcyMB4X +DTA2MTAyNTA4MzI0NloXDTM2MTAyNTA4MzI0NlowRzELMAkGA1UEBhMCQ0gxFTATBgNVBAoTDFN3 +aXNzU2lnbiBBRzEhMB8GA1UEAxMYU3dpc3NTaWduIFNpbHZlciBDQSAtIEcyMIICIjANBgkqhkiG +9w0BAQEFAAOCAg8AMIICCgKCAgEAxPGHf9N4Mfc4yfjDmUO8x/e8N+dOcbpLj6VzHVxumK4DV644 +N0MvFz0fyM5oEMF4rhkDKxD6LHmD9ui5aLlV8gREpzn5/ASLHvGiTSf5YXu6t+WiE7brYT7QbNHm ++/pe7R20nqA1W6GSy/BJkv6FCgU+5tkL4k+73JU3/JHpMjUi0R86TieFnbAVlDLaYQ1HTWBCrpJH +6INaUFjpiou5XaHc3ZlKHzZnu0jkg7Y360g6rw9njxcH6ATK72oxh9TAtvmUcXtnZLi2kUpCe2Uu +MGoM9ZDulebyzYLs2aFK7PayS+VFheZteJMELpyCbTapxDFkH4aDCyr0NQp4yVXPQbBH6TCfmb5h +qAaEuSh6XzjZG6k4sIN/c8HDO0gqgg8hm7jMqDXDhBuDsz6+pJVpATqJAHgE2cn0mRmrVn5bi4Y5 +FZGkECwJMoBgs5PAKrYYC51+jUnyEEp/+dVGLxmSo5mnJqy7jDzmDrxHB9xzUfFwZC8I+bRHHTBs +ROopN4WSaGa8gzj+ezku01DwH/teYLappvonQfGbGHLy9YR0SslnxFSuSGTfjNFusB3hB48IHpmc +celM2KX3RxIfdNFRnobzwqIjQAtz20um53MGjMGg6cFZrEb65i/4z3GcRm25xBWNOHkDRUjvxF3X +CO6HOSKGsg0PWEP3calILv3q1h8CAwEAAaOBrDCBqTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/ +BAUwAwEB/zAdBgNVHQ4EFgQUF6DNweRBtjpbO8tFnb0cwpj6hlgwHwYDVR0jBBgwFoAUF6DNweRB +tjpbO8tFnb0cwpj6hlgwRgYDVR0gBD8wPTA7BglghXQBWQEDAQEwLjAsBggrBgEFBQcCARYgaHR0 +cDovL3JlcG9zaXRvcnkuc3dpc3NzaWduLmNvbS8wDQYJKoZIhvcNAQEFBQADggIBAHPGgeAn0i0P +4JUw4ppBf1AsX19iYamGamkYDHRJ1l2E6kFSGG9YrVBWIGrGvShpWJHckRE1qTodvBqlYJ7YH39F +kWnZfrt4csEGDyrOj4VwYaygzQu4OSlWhDJOhrs9xCrZ1x9y7v5RoSJBsXECYxqCsGKrXlcSH9/L +3XWgwF15kIwb4FDm3jH+mHtwX6WQ2K34ArZv02DdQEsixT2tOnqfGhpHkXkzuoLcMmkDlm4fS/Bx +/uNncqCxv1yL5PqZIseEuRuNI5c/7SXgz2W79WEE790eslpBIlqhn10s6FvJbakMDHiqYMZWjwFa +DGi8aRl5xB9+lwW/xekkUV7U1UtT7dkjWjYDZaPBA61BMPNGG4WQr2W11bHkFlt4dR2Xem1ZqSqP +e97Dh4kQmUlzeMg9vVE1dCrV8X5pGyq7O70luJpaPXJhkGaH7gzWTdQRdAtq/gsD/KNVV4n+Ssuu +WxcFyPKNIzFTONItaj+CuY0IavdeQXRuwxF+B6wpYJE/OMpXEA29MC/HpeZBoNquBYeaoKRlbEwJ +DIm6uNO5wJOKMPqN5ZprFQFOZ6raYlY+hAhm0sQ2fac+EPyI4NSA5QC9qvNOBqN6avlicuMJT+ub +DgEj8Z+7fNzcbBGXJbLytGMU0gYqZ4yD9c7qB9iaah7s5Aq7KkzrCWA5zspi2C5u +-----END CERTIFICATE----- + +SecureTrust CA +============== +-----BEGIN CERTIFICATE----- +MIIDuDCCAqCgAwIBAgIQDPCOXAgWpa1Cf/DrJxhZ0DANBgkqhkiG9w0BAQUFADBIMQswCQYDVQQG +EwJVUzEgMB4GA1UEChMXU2VjdXJlVHJ1c3QgQ29ycG9yYXRpb24xFzAVBgNVBAMTDlNlY3VyZVRy +dXN0IENBMB4XDTA2MTEwNzE5MzExOFoXDTI5MTIzMTE5NDA1NVowSDELMAkGA1UEBhMCVVMxIDAe +BgNVBAoTF1NlY3VyZVRydXN0IENvcnBvcmF0aW9uMRcwFQYDVQQDEw5TZWN1cmVUcnVzdCBDQTCC +ASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKukgeWVzfX2FI7CT8rU4niVWJxB4Q2ZQCQX +OZEzZum+4YOvYlyJ0fwkW2Gz4BERQRwdbvC4u/jep4G6pkjGnx29vo6pQT64lO0pGtSO0gMdA+9t +DWccV9cGrcrI9f4Or2YlSASWC12juhbDCE/RRvgUXPLIXgGZbf2IzIaowW8xQmxSPmjL8xk037uH +GFaAJsTQ3MBv396gwpEWoGQRS0S8Hvbn+mPeZqx2pHGj7DaUaHp3pLHnDi+BeuK1cobvomuL8A/b +01k/unK8RCSc43Oz969XL0Imnal0ugBS8kvNU3xHCzaFDmapCJcWNFfBZveA4+1wVMeT4C4oFVmH +ursCAwEAAaOBnTCBmjATBgkrBgEEAYI3FAIEBh4EAEMAQTALBgNVHQ8EBAMCAYYwDwYDVR0TAQH/ +BAUwAwEB/zAdBgNVHQ4EFgQUQjK2FvoE/f5dS3rD/fdMQB1aQ68wNAYDVR0fBC0wKzApoCegJYYj +aHR0cDovL2NybC5zZWN1cmV0cnVzdC5jb20vU1RDQS5jcmwwEAYJKwYBBAGCNxUBBAMCAQAwDQYJ +KoZIhvcNAQEFBQADggEBADDtT0rhWDpSclu1pqNlGKa7UTt36Z3q059c4EVlew3KW+JwULKUBRSu +SceNQQcSc5R+DCMh/bwQf2AQWnL1mA6s7Ll/3XpvXdMc9P+IBWlCqQVxyLesJugutIxq/3HcuLHf +mbx8IVQr5Fiiu1cprp6poxkmD5kuCLDv/WnPmRoJjeOnnyvJNjR7JLN4TJUXpAYmHrZkUjZfYGfZ +nMUFdAvnZyPSCPyI6a6Lf+Ew9Dd+/cYy2i2eRDAwbO4H3tI0/NL/QPZL9GZGBlSm8jIKYyYwa5vR +3ItHuuG51WLQoqD0ZwV4KWMabwTW+MZMo5qxN7SN5ShLHZ4swrhovO0C7jE= +-----END CERTIFICATE----- + +Secure Global CA +================ +-----BEGIN CERTIFICATE----- +MIIDvDCCAqSgAwIBAgIQB1YipOjUiolN9BPI8PjqpTANBgkqhkiG9w0BAQUFADBKMQswCQYDVQQG +EwJVUzEgMB4GA1UEChMXU2VjdXJlVHJ1c3QgQ29ycG9yYXRpb24xGTAXBgNVBAMTEFNlY3VyZSBH +bG9iYWwgQ0EwHhcNMDYxMTA3MTk0MjI4WhcNMjkxMjMxMTk1MjA2WjBKMQswCQYDVQQGEwJVUzEg +MB4GA1UEChMXU2VjdXJlVHJ1c3QgQ29ycG9yYXRpb24xGTAXBgNVBAMTEFNlY3VyZSBHbG9iYWwg +Q0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCvNS7YrGxVaQZx5RNoJLNP2MwhR/jx +YDiJiQPpvepeRlMJ3Fz1Wuj3RSoC6zFh1ykzTM7HfAo3fg+6MpjhHZevj8fcyTiW89sa/FHtaMbQ +bqR8JNGuQsiWUGMu4P51/pinX0kuleM5M2SOHqRfkNJnPLLZ/kG5VacJjnIFHovdRIWCQtBJwB1g +8NEXLJXr9qXBkqPFwqcIYA1gBBCWeZ4WNOaptvolRTnIHmX5k/Wq8VLcmZg9pYYaDDUz+kulBAYV +HDGA76oYa8J719rO+TMg1fW9ajMtgQT7sFzUnKPiXB3jqUJ1XnvUd+85VLrJChgbEplJL4hL/VBi +0XPnj3pDAgMBAAGjgZ0wgZowEwYJKwYBBAGCNxQCBAYeBABDAEEwCwYDVR0PBAQDAgGGMA8GA1Ud +EwEB/wQFMAMBAf8wHQYDVR0OBBYEFK9EBMJBfkiD2045AuzshHrmzsmkMDQGA1UdHwQtMCswKaAn +oCWGI2h0dHA6Ly9jcmwuc2VjdXJldHJ1c3QuY29tL1NHQ0EuY3JsMBAGCSsGAQQBgjcVAQQDAgEA +MA0GCSqGSIb3DQEBBQUAA4IBAQBjGghAfaReUw132HquHw0LURYD7xh8yOOvaliTFGCRsoTciE6+ +OYo68+aCiV0BN7OrJKQVDpI1WkpEXk5X+nXOH0jOZvQ8QCaSmGwb7iRGDBezUqXbpZGRzzfTb+cn +CDpOGR86p1hcF895P4vkp9MmI50mD1hp/Ed+stCNi5O/KU9DaXR2Z0vPB4zmAve14bRDtUstFJ/5 +3CYNv6ZHdAbYiNE6KTCEztI5gGIbqMdXSbxqVVFnFUq+NQfk1XWYN3kwFNspnWzFacxHVaIw98xc +f8LDmBxrThaA63p4ZUWiABqvDA1VZDRIuJK58bRQKfJPIx/abKwfROHdI3hRW8cW +-----END CERTIFICATE----- + +COMODO Certification Authority +============================== +-----BEGIN CERTIFICATE----- +MIIEHTCCAwWgAwIBAgIQToEtioJl4AsC7j41AkblPTANBgkqhkiG9w0BAQUFADCBgTELMAkGA1UE +BhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEaMBgG +A1UEChMRQ09NT0RPIENBIExpbWl0ZWQxJzAlBgNVBAMTHkNPTU9ETyBDZXJ0aWZpY2F0aW9uIEF1 +dGhvcml0eTAeFw0wNjEyMDEwMDAwMDBaFw0yOTEyMzEyMzU5NTlaMIGBMQswCQYDVQQGEwJHQjEb +MBkGA1UECBMSR3JlYXRlciBNYW5jaGVzdGVyMRAwDgYDVQQHEwdTYWxmb3JkMRowGAYDVQQKExFD +T01PRE8gQ0EgTGltaXRlZDEnMCUGA1UEAxMeQ09NT0RPIENlcnRpZmljYXRpb24gQXV0aG9yaXR5 +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0ECLi3LjkRv3UcEbVASY06m/weaKXTuH ++7uIzg3jLz8GlvCiKVCZrts7oVewdFFxze1CkU1B/qnI2GqGd0S7WWaXUF601CxwRM/aN5VCaTww +xHGzUvAhTaHYujl8HJ6jJJ3ygxaYqhZ8Q5sVW7euNJH+1GImGEaaP+vB+fGQV+useg2L23IwambV +4EajcNxo2f8ESIl33rXp+2dtQem8Ob0y2WIC8bGoPW43nOIv4tOiJovGuFVDiOEjPqXSJDlqR6sA +1KGzqSX+DT+nHbrTUcELpNqsOO9VUCQFZUaTNE8tja3G1CEZ0o7KBWFxB3NH5YoZEr0ETc5OnKVI +rLsm9wIDAQABo4GOMIGLMB0GA1UdDgQWBBQLWOWLxkwVN6RAqTCpIb5HNlpW/zAOBgNVHQ8BAf8E +BAMCAQYwDwYDVR0TAQH/BAUwAwEB/zBJBgNVHR8EQjBAMD6gPKA6hjhodHRwOi8vY3JsLmNvbW9k +b2NhLmNvbS9DT01PRE9DZXJ0aWZpY2F0aW9uQXV0aG9yaXR5LmNybDANBgkqhkiG9w0BAQUFAAOC +AQEAPpiem/Yb6dc5t3iuHXIYSdOH5EOC6z/JqvWote9VfCFSZfnVDeFs9D6Mk3ORLgLETgdxb8CP +OGEIqB6BCsAvIC9Bi5HcSEW88cbeunZrM8gALTFGTO3nnc+IlP8zwFboJIYmuNg4ON8qa90SzMc/ +RxdMosIGlgnW2/4/PEZB31jiVg88O8EckzXZOFKs7sjsLjBOlDW0JB9LeGna8gI4zJVSk/BwJVmc +IGfE7vmLV2H0knZ9P4SNVbfo5azV8fUZVqZa+5Acr5Pr5RzUZ5ddBA6+C4OmF4O5MBKgxTMVBbkN ++8cFduPYSo38NBejxiEovjBFMR7HeL5YYTisO+IBZQ== +-----END CERTIFICATE----- + +COMODO ECC Certification Authority +================================== +-----BEGIN CERTIFICATE----- +MIICiTCCAg+gAwIBAgIQH0evqmIAcFBUTAGem2OZKjAKBggqhkjOPQQDAzCBhTELMAkGA1UEBhMC +R0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEaMBgGA1UE +ChMRQ09NT0RPIENBIExpbWl0ZWQxKzApBgNVBAMTIkNPTU9ETyBFQ0MgQ2VydGlmaWNhdGlvbiBB +dXRob3JpdHkwHhcNMDgwMzA2MDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCBhTELMAkGA1UEBhMCR0Ix +GzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEaMBgGA1UEChMR +Q09NT0RPIENBIExpbWl0ZWQxKzApBgNVBAMTIkNPTU9ETyBFQ0MgQ2VydGlmaWNhdGlvbiBBdXRo +b3JpdHkwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQDR3svdcmCFYX7deSRFtSrYpn1PlILBs5BAH+X +4QokPB0BBO490o0JlwzgdeT6+3eKKvUDYEs2ixYjFq0JcfRK9ChQtP6IHG4/bC8vCVlbpVsLM5ni +wz2J+Wos77LTBumjQjBAMB0GA1UdDgQWBBR1cacZSBm8nZ3qQUfflMRId5nTeTAOBgNVHQ8BAf8E +BAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAKBggqhkjOPQQDAwNoADBlAjEA7wNbeqy3eApyt4jf/7VG +FAkK+qDmfQjGGoe9GKhzvSbKYAydzpmfz1wPMOG+FDHqAjAU9JM8SaczepBGR7NjfRObTrdvGDeA +U/7dIOA1mjbRxwG55tzd8/8dLDoWV9mSOdY= +-----END CERTIFICATE----- + +Certigna +======== +-----BEGIN CERTIFICATE----- +MIIDqDCCApCgAwIBAgIJAP7c4wEPyUj/MA0GCSqGSIb3DQEBBQUAMDQxCzAJBgNVBAYTAkZSMRIw +EAYDVQQKDAlEaGlteW90aXMxETAPBgNVBAMMCENlcnRpZ25hMB4XDTA3MDYyOTE1MTMwNVoXDTI3 +MDYyOTE1MTMwNVowNDELMAkGA1UEBhMCRlIxEjAQBgNVBAoMCURoaW15b3RpczERMA8GA1UEAwwI +Q2VydGlnbmEwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDIaPHJ1tazNHUmgh7stL7q +XOEm7RFHYeGifBZ4QCHkYJ5ayGPhxLGWkv8YbWkj4Sti993iNi+RB7lIzw7sebYs5zRLcAglozyH +GxnygQcPOJAZ0xH+hrTy0V4eHpbNgGzOOzGTtvKg0KmVEn2lmsxryIRWijOp5yIVUxbwzBfsV1/p +ogqYCd7jX5xv3EjjhQsVWqa6n6xI4wmy9/Qy3l40vhx4XUJbzg4ij02Q130yGLMLLGq/jj8UEYkg +DncUtT2UCIf3JR7VsmAA7G8qKCVuKj4YYxclPz5EIBb2JsglrgVKtOdjLPOMFlN+XPsRGgjBRmKf +Irjxwo1p3Po6WAbfAgMBAAGjgbwwgbkwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUGu3+QTmQ +tCRZvgHyUtVF9lo53BEwZAYDVR0jBF0wW4AUGu3+QTmQtCRZvgHyUtVF9lo53BGhOKQ2MDQxCzAJ +BgNVBAYTAkZSMRIwEAYDVQQKDAlEaGlteW90aXMxETAPBgNVBAMMCENlcnRpZ25hggkA/tzjAQ/J +SP8wDgYDVR0PAQH/BAQDAgEGMBEGCWCGSAGG+EIBAQQEAwIABzANBgkqhkiG9w0BAQUFAAOCAQEA +hQMeknH2Qq/ho2Ge6/PAD/Kl1NqV5ta+aDY9fm4fTIrv0Q8hbV6lUmPOEvjvKtpv6zf+EwLHyzs+ +ImvaYS5/1HI93TDhHkxAGYwP15zRgzB7mFncfca5DClMoTOi62c6ZYTTluLtdkVwj7Ur3vkj1klu +PBS1xp81HlDQwY9qcEQCYsuuHWhBp6pX6FOqB9IG9tUUBguRA3UsbHK1YZWaDYu5Def131TN3ubY +1gkIl2PlwS6wt0QmwCbAr1UwnjvVNioZBPRcHv/PLLf/0P2HQBHVESO7SMAhqaQoLf0V+LBOK/Qw +WyH8EZE0vkHve52Xdf+XlcCWWC/qu0bXu+TZLg== +-----END CERTIFICATE----- + +ePKI Root Certification Authority +================================= +-----BEGIN CERTIFICATE----- +MIIFsDCCA5igAwIBAgIQFci9ZUdcr7iXAF7kBtK8nTANBgkqhkiG9w0BAQUFADBeMQswCQYDVQQG +EwJUVzEjMCEGA1UECgwaQ2h1bmdod2EgVGVsZWNvbSBDby4sIEx0ZC4xKjAoBgNVBAsMIWVQS0kg +Um9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw0wNDEyMjAwMjMxMjdaFw0zNDEyMjAwMjMx +MjdaMF4xCzAJBgNVBAYTAlRXMSMwIQYDVQQKDBpDaHVuZ2h3YSBUZWxlY29tIENvLiwgTHRkLjEq +MCgGA1UECwwhZVBLSSBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIICIjANBgkqhkiG9w0B +AQEFAAOCAg8AMIICCgKCAgEA4SUP7o3biDN1Z82tH306Tm2d0y8U82N0ywEhajfqhFAHSyZbCUNs +IZ5qyNUD9WBpj8zwIuQf5/dqIjG3LBXy4P4AakP/h2XGtRrBp0xtInAhijHyl3SJCRImHJ7K2RKi +lTza6We/CKBk49ZCt0Xvl/T29de1ShUCWH2YWEtgvM3XDZoTM1PRYfl61dd4s5oz9wCGzh1NlDiv +qOx4UXCKXBCDUSH3ET00hl7lSM2XgYI1TBnsZfZrxQWh7kcT1rMhJ5QQCtkkO7q+RBNGMD+XPNjX +12ruOzjjK9SXDrkb5wdJfzcq+Xd4z1TtW0ado4AOkUPB1ltfFLqfpo0kR0BZv3I4sjZsN/+Z0V0O +WQqraffAsgRFelQArr5T9rXn4fg8ozHSqf4hUmTFpmfwdQcGlBSBVcYn5AGPF8Fqcde+S/uUWH1+ +ETOxQvdibBjWzwloPn9s9h6PYq2lY9sJpx8iQkEeb5mKPtf5P0B6ebClAZLSnT0IFaUQAS2zMnao +lQ2zepr7BxB4EW/hj8e6DyUadCrlHJhBmd8hh+iVBmoKs2pHdmX2Os+PYhcZewoozRrSgx4hxyy/ +vv9haLdnG7t4TY3OZ+XkwY63I2binZB1NJipNiuKmpS5nezMirH4JYlcWrYvjB9teSSnUmjDhDXi +Zo1jDiVN1Rmy5nk3pyKdVDECAwEAAaNqMGgwHQYDVR0OBBYEFB4M97Zn8uGSJglFwFU5Lnc/Qkqi +MAwGA1UdEwQFMAMBAf8wOQYEZyoHAAQxMC8wLQIBADAJBgUrDgMCGgUAMAcGBWcqAwAABBRFsMLH +ClZ87lt4DJX5GFPBphzYEDANBgkqhkiG9w0BAQUFAAOCAgEACbODU1kBPpVJufGBuvl2ICO1J2B0 +1GqZNF5sAFPZn/KmsSQHRGoqxqWOeBLoR9lYGxMqXnmbnwoqZ6YlPwZpVnPDimZI+ymBV3QGypzq +KOg4ZyYr8dW1P2WT+DZdjo2NQCCHGervJ8A9tDkPJXtoUHRVnAxZfVo9QZQlUgjgRywVMRnVvwdV +xrsStZf0X4OFunHB2WyBEXYKCrC/gpf36j36+uwtqSiUO1bd0lEursC9CBWMd1I0ltabrNMdjmEP +NXubrjlpC2JgQCA2j6/7Nu4tCEoduL+bXPjqpRugc6bY+G7gMwRfaKonh+3ZwZCc7b3jajWvY9+r +GNm65ulK6lCKD2GTHuItGeIwlDWSXQ62B68ZgI9HkFFLLk3dheLSClIKF5r8GrBQAuUBo2M3IUxE +xJtRmREOc5wGj1QupyheRDmHVi03vYVElOEMSyycw5KFNGHLD7ibSkNS/jQ6fbjpKdx2qcgw+BRx +gMYeNkh0IkFch4LoGHGLQYlE535YW6i4jRPpp2zDR+2zGp1iro2C6pSe3VkQw63d4k3jMdXH7Ojy +sP6SHhYKGvzZ8/gntsm+HbRsZJB/9OTEW9c3rkIO3aQab3yIVMUWbuF6aC74Or8NpDyJO3inTmOD +BCEIZ43ygknQW/2xzQ+DhNQ+IIX3Sj0rnP0qCglN6oH4EZw= +-----END CERTIFICATE----- + +certSIGN ROOT CA +================ +-----BEGIN CERTIFICATE----- +MIIDODCCAiCgAwIBAgIGIAYFFnACMA0GCSqGSIb3DQEBBQUAMDsxCzAJBgNVBAYTAlJPMREwDwYD +VQQKEwhjZXJ0U0lHTjEZMBcGA1UECxMQY2VydFNJR04gUk9PVCBDQTAeFw0wNjA3MDQxNzIwMDRa +Fw0zMTA3MDQxNzIwMDRaMDsxCzAJBgNVBAYTAlJPMREwDwYDVQQKEwhjZXJ0U0lHTjEZMBcGA1UE +CxMQY2VydFNJR04gUk9PVCBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALczuX7I +JUqOtdu0KBuqV5Do0SLTZLrTk+jUrIZhQGpgV2hUhE28alQCBf/fm5oqrl0Hj0rDKH/v+yv6efHH +rfAQUySQi2bJqIirr1qjAOm+ukbuW3N7LBeCgV5iLKECZbO9xSsAfsT8AzNXDe3i+s5dRdY4zTW2 +ssHQnIFKquSyAVwdj1+ZxLGt24gh65AIgoDzMKND5pCCrlUoSe1b16kQOA7+j0xbm0bqQfWwCHTD +0IgztnzXdN/chNFDDnU5oSVAKOp4yw4sLjmdjItuFhwvJoIQ4uNllAoEwF73XVv4EOLQunpL+943 +AAAaWyjj0pxzPjKHmKHJUS/X3qwzs08CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8B +Af8EBAMCAcYwHQYDVR0OBBYEFOCMm9slSbPxfIbWskKHC9BroNnkMA0GCSqGSIb3DQEBBQUAA4IB +AQA+0hyJLjX8+HXd5n9liPRyTMks1zJO890ZeUe9jjtbkw9QSSQTaxQGcu8J06Gh40CEyecYMnQ8 +SG4Pn0vU9x7Tk4ZkVJdjclDVVc/6IJMCopvDI5NOFlV2oHB5bc0hH88vLbwZ44gx+FkagQnIl6Z0 +x2DEW8xXjrJ1/RsCCdtZb3KTafcxQdaIOL+Hsr0Wefmq5L6IJd1hJyMctTEHBDa0GpC9oHRxUIlt +vBTjD4au8as+x6AJzKNI0eDbZOeStc+vckNwi/nDhDwTqn6Sm1dTk/pwwpEOMfmbZ13pljheX7Nz +TogVZ96edhBiIL5VaZVDADlN9u6wWk5JRFRYX0KD +-----END CERTIFICATE----- + +NetLock Arany (Class Gold) Főtanúsítvány +======================================== +-----BEGIN CERTIFICATE----- +MIIEFTCCAv2gAwIBAgIGSUEs5AAQMA0GCSqGSIb3DQEBCwUAMIGnMQswCQYDVQQGEwJIVTERMA8G +A1UEBwwIQnVkYXBlc3QxFTATBgNVBAoMDE5ldExvY2sgS2Z0LjE3MDUGA1UECwwuVGFuw7pzw610 +dsOhbnlraWFkw7NrIChDZXJ0aWZpY2F0aW9uIFNlcnZpY2VzKTE1MDMGA1UEAwwsTmV0TG9jayBB +cmFueSAoQ2xhc3MgR29sZCkgRsWRdGFuw7pzw610dsOhbnkwHhcNMDgxMjExMTUwODIxWhcNMjgx +MjA2MTUwODIxWjCBpzELMAkGA1UEBhMCSFUxETAPBgNVBAcMCEJ1ZGFwZXN0MRUwEwYDVQQKDAxO +ZXRMb2NrIEtmdC4xNzA1BgNVBAsMLlRhbsO6c8OtdHbDoW55a2lhZMOzayAoQ2VydGlmaWNhdGlv +biBTZXJ2aWNlcykxNTAzBgNVBAMMLE5ldExvY2sgQXJhbnkgKENsYXNzIEdvbGQpIEbFkXRhbsO6 +c8OtdHbDoW55MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxCRec75LbRTDofTjl5Bu +0jBFHjzuZ9lk4BqKf8owyoPjIMHj9DrTlF8afFttvzBPhCf2nx9JvMaZCpDyD/V/Q4Q3Y1GLeqVw +/HpYzY6b7cNGbIRwXdrzAZAj/E4wqX7hJ2Pn7WQ8oLjJM2P+FpD/sLj916jAwJRDC7bVWaaeVtAk +H3B5r9s5VA1lddkVQZQBr17s9o3x/61k/iCa11zr/qYfCGSji3ZVrR47KGAuhyXoqq8fxmRGILdw +fzzeSNuWU7c5d+Qa4scWhHaXWy+7GRWF+GmF9ZmnqfI0p6m2pgP8b4Y9VHx2BJtr+UBdADTHLpl1 +neWIA6pN+APSQnbAGwIDAKiLo0UwQzASBgNVHRMBAf8ECDAGAQH/AgEEMA4GA1UdDwEB/wQEAwIB +BjAdBgNVHQ4EFgQUzPpnk/C2uNClwB7zU/2MU9+D15YwDQYJKoZIhvcNAQELBQADggEBAKt/7hwW +qZw8UQCgwBEIBaeZ5m8BiFRhbvG5GK1Krf6BQCOUL/t1fC8oS2IkgYIL9WHxHG64YTjrgfpioTta +YtOUZcTh5m2C+C8lcLIhJsFyUR+MLMOEkMNaj7rP9KdlpeuY0fsFskZ1FSNqb4VjMIDw1Z4fKRzC +bLBQWV2QWzuoDTDPv31/zvGdg73JRm4gpvlhUbohL3u+pRVjodSVh/GeufOJ8z2FuLjbvrW5Kfna +NwUASZQDhETnv0Mxz3WLJdH0pmT1kvarBes96aULNmLazAZfNou2XjG4Kvte9nHfRCaexOYNkbQu +dZWAUWpLMKawYqGT8ZvYzsRjdT9ZR7E= +-----END CERTIFICATE----- + +SecureSign RootCA11 +=================== +-----BEGIN CERTIFICATE----- +MIIDbTCCAlWgAwIBAgIBATANBgkqhkiG9w0BAQUFADBYMQswCQYDVQQGEwJKUDErMCkGA1UEChMi +SmFwYW4gQ2VydGlmaWNhdGlvbiBTZXJ2aWNlcywgSW5jLjEcMBoGA1UEAxMTU2VjdXJlU2lnbiBS +b290Q0ExMTAeFw0wOTA0MDgwNDU2NDdaFw0yOTA0MDgwNDU2NDdaMFgxCzAJBgNVBAYTAkpQMSsw +KQYDVQQKEyJKYXBhbiBDZXJ0aWZpY2F0aW9uIFNlcnZpY2VzLCBJbmMuMRwwGgYDVQQDExNTZWN1 +cmVTaWduIFJvb3RDQTExMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA/XeqpRyQBTvL +TJszi1oURaTnkBbR31fSIRCkF/3frNYfp+TbfPfs37gD2pRY/V1yfIw/XwFndBWW4wI8h9uuywGO +wvNmxoVF9ALGOrVisq/6nL+k5tSAMJjzDbaTj6nU2DbysPyKyiyhFTOVMdrAG/LuYpmGYz+/3ZMq +g6h2uRMft85OQoWPIucuGvKVCbIFtUROd6EgvanyTgp9UK31BQ1FT0Zx/Sg+U/sE2C3XZR1KG/rP +O7AxmjVuyIsG0wCR8pQIZUyxNAYAeoni8McDWc/V1uinMrPmmECGxc0nEovMe863ETxiYAcjPitA +bpSACW22s293bzUIUPsCh8U+iQIDAQABo0IwQDAdBgNVHQ4EFgQUW/hNT7KlhtQ60vFjmqC+CfZX +t94wDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAKCh +OBZmLqdWHyGcBvod7bkixTgm2E5P7KN/ed5GIaGHd48HCJqypMWvDzKYC3xmKbabfSVSSUOrTC4r +bnpwrxYO4wJs+0LmGJ1F2FXI6Dvd5+H0LgscNFxsWEr7jIhQX5Ucv+2rIrVls4W6ng+4reV6G4pQ +Oh29Dbx7VFALuUKvVaAYga1lme++5Jy/xIWrQbJUb9wlze144o4MjQlJ3WN7WmmWAiGovVJZ6X01 +y8hSyn+B/tlr0/cR7SXf+Of5pPpyl4RTDaXQMhhRdlkUbA/r7F+AjHVDg8OFmP9Mni0N5HeDk061 +lgeLKBObjBmNQSdJQO7e5iNEOdyhIta6A/I= +-----END CERTIFICATE----- + +Microsec e-Szigno Root CA 2009 +============================== +-----BEGIN CERTIFICATE----- +MIIECjCCAvKgAwIBAgIJAMJ+QwRORz8ZMA0GCSqGSIb3DQEBCwUAMIGCMQswCQYDVQQGEwJIVTER +MA8GA1UEBwwIQnVkYXBlc3QxFjAUBgNVBAoMDU1pY3Jvc2VjIEx0ZC4xJzAlBgNVBAMMHk1pY3Jv +c2VjIGUtU3ppZ25vIFJvb3QgQ0EgMjAwOTEfMB0GCSqGSIb3DQEJARYQaW5mb0BlLXN6aWduby5o +dTAeFw0wOTA2MTYxMTMwMThaFw0yOTEyMzAxMTMwMThaMIGCMQswCQYDVQQGEwJIVTERMA8GA1UE +BwwIQnVkYXBlc3QxFjAUBgNVBAoMDU1pY3Jvc2VjIEx0ZC4xJzAlBgNVBAMMHk1pY3Jvc2VjIGUt +U3ppZ25vIFJvb3QgQ0EgMjAwOTEfMB0GCSqGSIb3DQEJARYQaW5mb0BlLXN6aWduby5odTCCASIw +DQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAOn4j/NjrdqG2KfgQvvPkd6mJviZpWNwrZuuyjNA +fW2WbqEORO7hE52UQlKavXWFdCyoDh2Tthi3jCyoz/tccbna7P7ofo/kLx2yqHWH2Leh5TvPmUpG +0IMZfcChEhyVbUr02MelTTMuhTlAdX4UfIASmFDHQWe4oIBhVKZsTh/gnQ4H6cm6M+f+wFUoLAKA +pxn1ntxVUwOXewdI/5n7N4okxFnMUBBjjqqpGrCEGob5X7uxUG6k0QrM1XF+H6cbfPVTbiJfyyvm +1HxdrtbCxkzlBQHZ7Vf8wSN5/PrIJIOV87VqUQHQd9bpEqH5GoP7ghu5sJf0dgYzQ0mg/wu1+rUC +AwEAAaOBgDB+MA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBTLD8bf +QkPMPcu1SCOhGnqmKrs0aDAfBgNVHSMEGDAWgBTLD8bfQkPMPcu1SCOhGnqmKrs0aDAbBgNVHREE +FDASgRBpbmZvQGUtc3ppZ25vLmh1MA0GCSqGSIb3DQEBCwUAA4IBAQDJ0Q5eLtXMs3w+y/w9/w0o +lZMEyL/azXm4Q5DwpL7v8u8hmLzU1F0G9u5C7DBsoKqpyvGvivo/C3NqPuouQH4frlRheesuCDfX +I/OMn74dseGkddug4lQUsbocKaQY9hK6ohQU4zE1yED/t+AFdlfBHFny+L/k7SViXITwfn4fs775 +tyERzAMBVnCnEJIeGzSBHq2cGsMEPO0CYdYeBvNfOofyK/FFh+U9rNHHV4S9a67c2Pm2G2JwCz02 +yULyMtd6YebS2z3PyKnJm9zbWETXbzivf3jTo60adbocwTZ8jx5tHMN1Rq41Bab2XD0h7lbwyYIi +LXpUq3DDfSJlgnCW +-----END CERTIFICATE----- + +GlobalSign Root CA - R3 +======================= +-----BEGIN CERTIFICATE----- +MIIDXzCCAkegAwIBAgILBAAAAAABIVhTCKIwDQYJKoZIhvcNAQELBQAwTDEgMB4GA1UECxMXR2xv +YmFsU2lnbiBSb290IENBIC0gUjMxEzARBgNVBAoTCkdsb2JhbFNpZ24xEzARBgNVBAMTCkdsb2Jh +bFNpZ24wHhcNMDkwMzE4MTAwMDAwWhcNMjkwMzE4MTAwMDAwWjBMMSAwHgYDVQQLExdHbG9iYWxT +aWduIFJvb3QgQ0EgLSBSMzETMBEGA1UEChMKR2xvYmFsU2lnbjETMBEGA1UEAxMKR2xvYmFsU2ln +bjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMwldpB5BngiFvXAg7aEyiie/QV2EcWt +iHL8RgJDx7KKnQRfJMsuS+FggkbhUqsMgUdwbN1k0ev1LKMPgj0MK66X17YUhhB5uzsTgHeMCOFJ +0mpiLx9e+pZo34knlTifBtc+ycsmWQ1z3rDI6SYOgxXG71uL0gRgykmmKPZpO/bLyCiR5Z2KYVc3 +rHQU3HTgOu5yLy6c+9C7v/U9AOEGM+iCK65TpjoWc4zdQQ4gOsC0p6Hpsk+QLjJg6VfLuQSSaGjl +OCZgdbKfd/+RFO+uIEn8rUAVSNECMWEZXriX7613t2Saer9fwRPvm2L7DWzgVGkWqQPabumDk3F2 +xmmFghcCAwEAAaNCMEAwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYE +FI/wS3+oLkUkrk1Q+mOai97i3Ru8MA0GCSqGSIb3DQEBCwUAA4IBAQBLQNvAUKr+yAzv95ZURUm7 +lgAJQayzE4aGKAczymvmdLm6AC2upArT9fHxD4q/c2dKg8dEe3jgr25sbwMpjjM5RcOO5LlXbKr8 +EpbsU8Yt5CRsuZRj+9xTaGdWPoO4zzUhw8lo/s7awlOqzJCK6fBdRoyV3XpYKBovHd7NADdBj+1E +bddTKJd+82cEHhXXipa0095MJ6RMG3NzdvQXmcIfeg7jLQitChws/zyrVQ4PkX4268NXSb7hLi18 +YIvDQVETI53O9zJrlAGomecsMx86OyXShkDOOyyGeMlhLxS67ttVb9+E7gUJTb0o2HLO02JQZR7r +kpeDMdmztcpHWD9f +-----END CERTIFICATE----- + +Autoridad de Certificacion Firmaprofesional CIF A62634068 +========================================================= +-----BEGIN CERTIFICATE----- +MIIGFDCCA/ygAwIBAgIIU+w77vuySF8wDQYJKoZIhvcNAQEFBQAwUTELMAkGA1UEBhMCRVMxQjBA +BgNVBAMMOUF1dG9yaWRhZCBkZSBDZXJ0aWZpY2FjaW9uIEZpcm1hcHJvZmVzaW9uYWwgQ0lGIEE2 +MjYzNDA2ODAeFw0wOTA1MjAwODM4MTVaFw0zMDEyMzEwODM4MTVaMFExCzAJBgNVBAYTAkVTMUIw +QAYDVQQDDDlBdXRvcmlkYWQgZGUgQ2VydGlmaWNhY2lvbiBGaXJtYXByb2Zlc2lvbmFsIENJRiBB +NjI2MzQwNjgwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDKlmuO6vj78aI14H9M2uDD +Utd9thDIAl6zQyrET2qyyhxdKJp4ERppWVevtSBC5IsP5t9bpgOSL/UR5GLXMnE42QQMcas9UX4P +B99jBVzpv5RvwSmCwLTaUbDBPLutN0pcyvFLNg4kq7/DhHf9qFD0sefGL9ItWY16Ck6WaVICqjaY +7Pz6FIMMNx/Jkjd/14Et5cS54D40/mf0PmbR0/RAz15iNA9wBj4gGFrO93IbJWyTdBSTo3OxDqqH +ECNZXyAFGUftaI6SEspd/NYrspI8IM/hX68gvqB2f3bl7BqGYTM+53u0P6APjqK5am+5hyZvQWyI +plD9amML9ZMWGxmPsu2bm8mQ9QEM3xk9Dz44I8kvjwzRAv4bVdZO0I08r0+k8/6vKtMFnXkIoctX +MbScyJCyZ/QYFpM6/EfY0XiWMR+6KwxfXZmtY4laJCB22N/9q06mIqqdXuYnin1oKaPnirjaEbsX +LZmdEyRG98Xi2J+Of8ePdG1asuhy9azuJBCtLxTa/y2aRnFHvkLfuwHb9H/TKI8xWVvTyQKmtFLK +bpf7Q8UIJm+K9Lv9nyiqDdVF8xM6HdjAeI9BZzwelGSuewvF6NkBiDkal4ZkQdU7hwxu+g/GvUgU +vzlN1J5Bto+WHWOWk9mVBngxaJ43BjuAiUVhOSPHG0SjFeUc+JIwuwIDAQABo4HvMIHsMBIGA1Ud +EwEB/wQIMAYBAf8CAQEwDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBRlzeurNR4APn7VdMActHNH +DhpkLzCBpgYDVR0gBIGeMIGbMIGYBgRVHSAAMIGPMC8GCCsGAQUFBwIBFiNodHRwOi8vd3d3LmZp +cm1hcHJvZmVzaW9uYWwuY29tL2NwczBcBggrBgEFBQcCAjBQHk4AUABhAHMAZQBvACAAZABlACAA +bABhACAAQgBvAG4AYQBuAG8AdgBhACAANAA3ACAAQgBhAHIAYwBlAGwAbwBuAGEAIAAwADgAMAAx +ADcwDQYJKoZIhvcNAQEFBQADggIBABd9oPm03cXF661LJLWhAqvdpYhKsg9VSytXjDvlMd3+xDLx +51tkljYyGOylMnfX40S2wBEqgLk9am58m9Ot/MPWo+ZkKXzR4Tgegiv/J2Wv+xYVxC5xhOW1//qk +R71kMrv2JYSiJ0L1ILDCExARzRAVukKQKtJE4ZYm6zFIEv0q2skGz3QeqUvVhyj5eTSSPi5E6PaP +T481PyWzOdxjKpBrIF/EUhJOlywqrJ2X3kjyo2bbwtKDlaZmp54lD+kLM5FlClrD2VQS3a/DTg4f +Jl4N3LON7NWBcN7STyQF82xO9UxJZo3R/9ILJUFI/lGExkKvgATP0H5kSeTy36LssUzAKh3ntLFl +osS88Zj0qnAHY7S42jtM+kAiMFsRpvAFDsYCA0irhpuF3dvd6qJ2gHN99ZwExEWN57kci57q13XR +crHedUTnQn3iV2t93Jm8PYMo6oCTjcVMZcFwgbg4/EMxsvYDNEeyrPsiBsse3RdHHF9mudMaotoR +saS8I8nkvof/uZS2+F0gStRf571oe2XyFR7SOqkt6dhrJKyXWERHrVkY8SFlcN7ONGCoQPHzPKTD +KCOM/iczQ0CgFzzr6juwcqajuUpLXhZI9LK8yIySxZ2frHI2vDSANGupi5LAuBft7HZT9SQBjLMi +6Et8Vcad+qMUu2WFbm5PEn4KPJ2V +-----END CERTIFICATE----- + +Izenpe.com +========== +-----BEGIN CERTIFICATE----- +MIIF8TCCA9mgAwIBAgIQALC3WhZIX7/hy/WL1xnmfTANBgkqhkiG9w0BAQsFADA4MQswCQYDVQQG +EwJFUzEUMBIGA1UECgwLSVpFTlBFIFMuQS4xEzARBgNVBAMMCkl6ZW5wZS5jb20wHhcNMDcxMjEz +MTMwODI4WhcNMzcxMjEzMDgyNzI1WjA4MQswCQYDVQQGEwJFUzEUMBIGA1UECgwLSVpFTlBFIFMu +QS4xEzARBgNVBAMMCkl6ZW5wZS5jb20wggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDJ +03rKDx6sp4boFmVqscIbRTJxldn+EFvMr+eleQGPicPK8lVx93e+d5TzcqQsRNiekpsUOqHnJJAK +ClaOxdgmlOHZSOEtPtoKct2jmRXagaKH9HtuJneJWK3W6wyyQXpzbm3benhB6QiIEn6HLmYRY2xU ++zydcsC8Lv/Ct90NduM61/e0aL6i9eOBbsFGb12N4E3GVFWJGjMxCrFXuaOKmMPsOzTFlUFpfnXC +PCDFYbpRR6AgkJOhkEvzTnyFRVSa0QUmQbC1TR0zvsQDyCV8wXDbO/QJLVQnSKwv4cSsPsjLkkxT +OTcj7NMB+eAJRE1NZMDhDVqHIrytG6P+JrUV86f8hBnp7KGItERphIPzidF0BqnMC9bC3ieFUCbK +F7jJeodWLBoBHmy+E60QrLUk9TiRodZL2vG70t5HtfG8gfZZa88ZU+mNFctKy6lvROUbQc/hhqfK +0GqfvEyNBjNaooXlkDWgYlwWTvDjovoDGrQscbNYLN57C9saD+veIR8GdwYDsMnvmfzAuU8Lhij+ +0rnq49qlw0dpEuDb8PYZi+17cNcC1u2HGCgsBCRMd+RIihrGO5rUD8r6ddIBQFqNeb+Lz0vPqhbB +leStTIo+F5HUsWLlguWABKQDfo2/2n+iD5dPDNMN+9fR5XJ+HMh3/1uaD7euBUbl8agW7EekFwID +AQABo4H2MIHzMIGwBgNVHREEgagwgaWBD2luZm9AaXplbnBlLmNvbaSBkTCBjjFHMEUGA1UECgw+ +SVpFTlBFIFMuQS4gLSBDSUYgQTAxMzM3MjYwLVJNZXJjLlZpdG9yaWEtR2FzdGVpeiBUMTA1NSBG +NjIgUzgxQzBBBgNVBAkMOkF2ZGEgZGVsIE1lZGl0ZXJyYW5lbyBFdG9yYmlkZWEgMTQgLSAwMTAx +MCBWaXRvcmlhLUdhc3RlaXowDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0O +BBYEFB0cZQ6o8iV7tJHP5LGx5r1VdGwFMA0GCSqGSIb3DQEBCwUAA4ICAQB4pgwWSp9MiDrAyw6l +Fn2fuUhfGI8NYjb2zRlrrKvV9pF9rnHzP7MOeIWblaQnIUdCSnxIOvVFfLMMjlF4rJUT3sb9fbga +kEyrkgPH7UIBzg/YsfqikuFgba56awmqxinuaElnMIAkejEWOVt+8Rwu3WwJrfIxwYJOubv5vr8q +hT/AQKM6WfxZSzwoJNu0FXWuDYi6LnPAvViH5ULy617uHjAimcs30cQhbIHsvm0m5hzkQiCeR7Cs +g1lwLDXWrzY0tM07+DKo7+N4ifuNRSzanLh+QBxh5z6ikixL8s36mLYp//Pye6kfLqCTVyvehQP5 +aTfLnnhqBbTFMXiJ7HqnheG5ezzevh55hM6fcA5ZwjUukCox2eRFekGkLhObNA5me0mrZJfQRsN5 +nXJQY6aYWwa9SG3YOYNw6DXwBdGqvOPbyALqfP2C2sJbUjWumDqtujWTI6cfSN01RpiyEGjkpTHC +ClguGYEQyVB1/OpaFs4R1+7vUIgtYf8/QnMFlEPVjjxOAToZpR9GTnfQXeWBIiGH/pR9hNiTrdZo +Q0iy2+tzJOeRf1SktoA+naM8THLCV8Sg1Mw4J87VBp6iSNnpn86CcDaTmjvfliHjWbcM2pE38P1Z +WrOZyGlsQyYBNWNgVYkDOnXYukrZVP/u3oDYLdE41V4tC5h9Pmzb/CaIxw== +-----END CERTIFICATE----- + +Go Daddy Root Certificate Authority - G2 +======================================== +-----BEGIN CERTIFICATE----- +MIIDxTCCAq2gAwIBAgIBADANBgkqhkiG9w0BAQsFADCBgzELMAkGA1UEBhMCVVMxEDAOBgNVBAgT +B0FyaXpvbmExEzARBgNVBAcTClNjb3R0c2RhbGUxGjAYBgNVBAoTEUdvRGFkZHkuY29tLCBJbmMu +MTEwLwYDVQQDEyhHbyBEYWRkeSBSb290IENlcnRpZmljYXRlIEF1dGhvcml0eSAtIEcyMB4XDTA5 +MDkwMTAwMDAwMFoXDTM3MTIzMTIzNTk1OVowgYMxCzAJBgNVBAYTAlVTMRAwDgYDVQQIEwdBcml6 +b25hMRMwEQYDVQQHEwpTY290dHNkYWxlMRowGAYDVQQKExFHb0RhZGR5LmNvbSwgSW5jLjExMC8G +A1UEAxMoR28gRGFkZHkgUm9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkgLSBHMjCCASIwDQYJKoZI +hvcNAQEBBQADggEPADCCAQoCggEBAL9xYgjx+lk09xvJGKP3gElY6SKDE6bFIEMBO4Tx5oVJnyfq +9oQbTqC023CYxzIBsQU+B07u9PpPL1kwIuerGVZr4oAH/PMWdYA5UXvl+TW2dE6pjYIT5LY/qQOD ++qK+ihVqf94Lw7YZFAXK6sOoBJQ7RnwyDfMAZiLIjWltNowRGLfTshxgtDj6AozO091GB94KPutd +fMh8+7ArU6SSYmlRJQVhGkSBjCypQ5Yj36w6gZoOKcUcqeldHraenjAKOc7xiID7S13MMuyFYkMl +NAJWJwGRtDtwKj9useiciAF9n9T521NtYJ2/LOdYq7hfRvzOxBsDPAnrSTFcaUaz4EcCAwEAAaNC +MEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFDqahQcQZyi27/a9 +BUFuIMGU2g/eMA0GCSqGSIb3DQEBCwUAA4IBAQCZ21151fmXWWcDYfF+OwYxdS2hII5PZYe096ac +vNjpL9DbWu7PdIxztDhC2gV7+AJ1uP2lsdeu9tfeE8tTEH6KRtGX+rcuKxGrkLAngPnon1rpN5+r +5N9ss4UXnT3ZJE95kTXWXwTrgIOrmgIttRD02JDHBHNA7XIloKmf7J6raBKZV8aPEjoJpL1E/QYV +N8Gb5DKj7Tjo2GTzLH4U/ALqn83/B2gX2yKQOC16jdFU8WnjXzPKej17CuPKf1855eJ1usV2GDPO +LPAvTK33sefOT6jEm0pUBsV/fdUID+Ic/n4XuKxe9tQWskMJDE32p2u0mYRlynqI4uJEvlz36hz1 +-----END CERTIFICATE----- + +Starfield Root Certificate Authority - G2 +========================================= +-----BEGIN CERTIFICATE----- +MIID3TCCAsWgAwIBAgIBADANBgkqhkiG9w0BAQsFADCBjzELMAkGA1UEBhMCVVMxEDAOBgNVBAgT +B0FyaXpvbmExEzARBgNVBAcTClNjb3R0c2RhbGUxJTAjBgNVBAoTHFN0YXJmaWVsZCBUZWNobm9s +b2dpZXMsIEluYy4xMjAwBgNVBAMTKVN0YXJmaWVsZCBSb290IENlcnRpZmljYXRlIEF1dGhvcml0 +eSAtIEcyMB4XDTA5MDkwMTAwMDAwMFoXDTM3MTIzMTIzNTk1OVowgY8xCzAJBgNVBAYTAlVTMRAw +DgYDVQQIEwdBcml6b25hMRMwEQYDVQQHEwpTY290dHNkYWxlMSUwIwYDVQQKExxTdGFyZmllbGQg +VGVjaG5vbG9naWVzLCBJbmMuMTIwMAYDVQQDEylTdGFyZmllbGQgUm9vdCBDZXJ0aWZpY2F0ZSBB +dXRob3JpdHkgLSBHMjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAL3twQP89o/8ArFv +W59I2Z154qK3A2FWGMNHttfKPTUuiUP3oWmb3ooa/RMgnLRJdzIpVv257IzdIvpy3Cdhl+72WoTs +bhm5iSzchFvVdPtrX8WJpRBSiUZV9Lh1HOZ/5FSuS/hVclcCGfgXcVnrHigHdMWdSL5stPSksPNk +N3mSwOxGXn/hbVNMYq/NHwtjuzqd+/x5AJhhdM8mgkBj87JyahkNmcrUDnXMN/uLicFZ8WJ/X7Nf +ZTD4p7dNdloedl40wOiWVpmKs/B/pM293DIxfJHP4F8R+GuqSVzRmZTRouNjWwl2tVZi4Ut0HZbU +JtQIBFnQmA4O5t78w+wfkPECAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMC +AQYwHQYDVR0OBBYEFHwMMh+n2TB/xH1oo2Kooc6rB1snMA0GCSqGSIb3DQEBCwUAA4IBAQARWfol +TwNvlJk7mh+ChTnUdgWUXuEok21iXQnCoKjUsHU48TRqneSfioYmUeYs0cYtbpUgSpIB7LiKZ3sx +4mcujJUDJi5DnUox9g61DLu34jd/IroAow57UvtruzvE03lRTs2Q9GcHGcg8RnoNAX3FWOdt5oUw +F5okxBDgBPfg8n/Uqgr/Qh037ZTlZFkSIHc40zI+OIF1lnP6aI+xy84fxez6nH7PfrHxBy22/L/K +pL/QlwVKvOoYKAKQvVR4CSFx09F9HdkWsKlhPdAKACL8x3vLCWRFCztAgfd9fDL1mMpYjn0q7pBZ +c2T5NnReJaH1ZgUufzkVqSr7UIuOhWn0 +-----END CERTIFICATE----- + +Starfield Services Root Certificate Authority - G2 +================================================== +-----BEGIN CERTIFICATE----- +MIID7zCCAtegAwIBAgIBADANBgkqhkiG9w0BAQsFADCBmDELMAkGA1UEBhMCVVMxEDAOBgNVBAgT +B0FyaXpvbmExEzARBgNVBAcTClNjb3R0c2RhbGUxJTAjBgNVBAoTHFN0YXJmaWVsZCBUZWNobm9s +b2dpZXMsIEluYy4xOzA5BgNVBAMTMlN0YXJmaWVsZCBTZXJ2aWNlcyBSb290IENlcnRpZmljYXRl +IEF1dGhvcml0eSAtIEcyMB4XDTA5MDkwMTAwMDAwMFoXDTM3MTIzMTIzNTk1OVowgZgxCzAJBgNV +BAYTAlVTMRAwDgYDVQQIEwdBcml6b25hMRMwEQYDVQQHEwpTY290dHNkYWxlMSUwIwYDVQQKExxT +dGFyZmllbGQgVGVjaG5vbG9naWVzLCBJbmMuMTswOQYDVQQDEzJTdGFyZmllbGQgU2VydmljZXMg +Um9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkgLSBHMjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC +AQoCggEBANUMOsQq+U7i9b4Zl1+OiFOxHz/Lz58gE20pOsgPfTz3a3Y4Y9k2YKibXlwAgLIvWX/2 +h/klQ4bnaRtSmpDhcePYLQ1Ob/bISdm28xpWriu2dBTrz/sm4xq6HZYuajtYlIlHVv8loJNwU4Pa +hHQUw2eeBGg6345AWh1KTs9DkTvnVtYAcMtS7nt9rjrnvDH5RfbCYM8TWQIrgMw0R9+53pBlbQLP +LJGmpufehRhJfGZOozptqbXuNC66DQO4M99H67FrjSXZm86B0UVGMpZwh94CDklDhbZsc7tk6mFB +rMnUVN+HL8cisibMn1lUaJ/8viovxFUcdUBgF4UCVTmLfwUCAwEAAaNCMEAwDwYDVR0TAQH/BAUw +AwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFJxfAN+qAdcwKziIorhtSpzyEZGDMA0GCSqG +SIb3DQEBCwUAA4IBAQBLNqaEd2ndOxmfZyMIbw5hyf2E3F/YNoHN2BtBLZ9g3ccaaNnRbobhiCPP +E95Dz+I0swSdHynVv/heyNXBve6SbzJ08pGCL72CQnqtKrcgfU28elUSwhXqvfdqlS5sdJ/PHLTy +xQGjhdByPq1zqwubdQxtRbeOlKyWN7Wg0I8VRw7j6IPdj/3vQQF3zCepYoUz8jcI73HPdwbeyBkd +iEDPfUYd/x7H4c7/I9vG+o1VTqkC50cRRj70/b17KSa7qWFiNyi2LSr2EIZkyXCn0q23KXB56jza +YyWf/Wi3MOxw+3WKt21gZ7IeyLnp2KhvAotnDU0mV3HaIPzBSlCNsSi6 +-----END CERTIFICATE----- + +AffirmTrust Commercial +====================== +-----BEGIN CERTIFICATE----- +MIIDTDCCAjSgAwIBAgIId3cGJyapsXwwDQYJKoZIhvcNAQELBQAwRDELMAkGA1UEBhMCVVMxFDAS +BgNVBAoMC0FmZmlybVRydXN0MR8wHQYDVQQDDBZBZmZpcm1UcnVzdCBDb21tZXJjaWFsMB4XDTEw +MDEyOTE0MDYwNloXDTMwMTIzMTE0MDYwNlowRDELMAkGA1UEBhMCVVMxFDASBgNVBAoMC0FmZmly +bVRydXN0MR8wHQYDVQQDDBZBZmZpcm1UcnVzdCBDb21tZXJjaWFsMIIBIjANBgkqhkiG9w0BAQEF +AAOCAQ8AMIIBCgKCAQEA9htPZwcroRX1BiLLHwGy43NFBkRJLLtJJRTWzsO3qyxPxkEylFf6Eqdb +DuKPHx6GGaeqtS25Xw2Kwq+FNXkyLbscYjfysVtKPcrNcV/pQr6U6Mje+SJIZMblq8Yrba0F8PrV +C8+a5fBQpIs7R6UjW3p6+DM/uO+Zl+MgwdYoic+U+7lF7eNAFxHUdPALMeIrJmqbTFeurCA+ukV6 +BfO9m2kVrn1OIGPENXY6BwLJN/3HR+7o8XYdcxXyl6S1yHp52UKqK39c/s4mT6NmgTWvRLpUHhww +MmWd5jyTXlBOeuM61G7MGvv50jeuJCqrVwMiKA1JdX+3KNp1v47j3A55MQIDAQABo0IwQDAdBgNV +HQ4EFgQUnZPGU4teyq8/nx4P5ZmVvCT2lI8wDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMC +AQYwDQYJKoZIhvcNAQELBQADggEBAFis9AQOzcAN/wr91LoWXym9e2iZWEnStB03TX8nfUYGXUPG +hi4+c7ImfU+TqbbEKpqrIZcUsd6M06uJFdhrJNTxFq7YpFzUf1GO7RgBsZNjvbz4YYCanrHOQnDi +qX0GJX0nof5v7LMeJNrjS1UaADs1tDvZ110w/YETifLCBivtZ8SOyUOyXGsViQK8YvxO8rUzqrJv +0wqiUOP2O+guRMLbZjipM1ZI8W0bM40NjD9gN53Tym1+NH4Nn3J2ixufcv1SNUFFApYvHLKac0kh +sUlHRUe072o0EclNmsxZt9YCnlpOZbWUrhvfKbAW8b8Angc6F2S1BLUjIZkKlTuXfO8= +-----END CERTIFICATE----- + +AffirmTrust Networking +====================== +-----BEGIN CERTIFICATE----- +MIIDTDCCAjSgAwIBAgIIfE8EORzUmS0wDQYJKoZIhvcNAQEFBQAwRDELMAkGA1UEBhMCVVMxFDAS +BgNVBAoMC0FmZmlybVRydXN0MR8wHQYDVQQDDBZBZmZpcm1UcnVzdCBOZXR3b3JraW5nMB4XDTEw +MDEyOTE0MDgyNFoXDTMwMTIzMTE0MDgyNFowRDELMAkGA1UEBhMCVVMxFDASBgNVBAoMC0FmZmly +bVRydXN0MR8wHQYDVQQDDBZBZmZpcm1UcnVzdCBOZXR3b3JraW5nMIIBIjANBgkqhkiG9w0BAQEF +AAOCAQ8AMIIBCgKCAQEAtITMMxcua5Rsa2FSoOujz3mUTOWUgJnLVWREZY9nZOIG41w3SfYvm4SE +Hi3yYJ0wTsyEheIszx6e/jarM3c1RNg1lho9Nuh6DtjVR6FqaYvZ/Ls6rnla1fTWcbuakCNrmreI +dIcMHl+5ni36q1Mr3Lt2PpNMCAiMHqIjHNRqrSK6mQEubWXLviRmVSRLQESxG9fhwoXA3hA/Pe24 +/PHxI1Pcv2WXb9n5QHGNfb2V1M6+oF4nI979ptAmDgAp6zxG8D1gvz9Q0twmQVGeFDdCBKNwV6gb +h+0t+nvujArjqWaJGctB+d1ENmHP4ndGyH329JKBNv3bNPFyfvMMFr20FQIDAQABo0IwQDAdBgNV +HQ4EFgQUBx/S55zawm6iQLSwelAQUHTEyL0wDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMC +AQYwDQYJKoZIhvcNAQEFBQADggEBAIlXshZ6qML91tmbmzTCnLQyFE2npN/svqe++EPbkTfOtDIu +UFUaNU52Q3Eg75N3ThVwLofDwR1t3Mu1J9QsVtFSUzpE0nPIxBsFZVpikpzuQY0x2+c06lkh1QF6 +12S4ZDnNye2v7UsDSKegmQGA3GWjNq5lWUhPgkvIZfFXHeVZLgo/bNjR9eUJtGxUAArgFU2HdW23 +WJZa3W3SAKD0m0i+wzekujbgfIeFlxoVot4uolu9rxj5kFDNcFn4J2dHy8egBzp90SxdbBk6ZrV9 +/ZFvgrG+CJPbFEfxojfHRZ48x3evZKiT3/Zpg4Jg8klCNO1aAFSFHBY2kgxc+qatv9s= +-----END CERTIFICATE----- + +AffirmTrust Premium +=================== +-----BEGIN CERTIFICATE----- +MIIFRjCCAy6gAwIBAgIIbYwURrGmCu4wDQYJKoZIhvcNAQEMBQAwQTELMAkGA1UEBhMCVVMxFDAS +BgNVBAoMC0FmZmlybVRydXN0MRwwGgYDVQQDDBNBZmZpcm1UcnVzdCBQcmVtaXVtMB4XDTEwMDEy +OTE0MTAzNloXDTQwMTIzMTE0MTAzNlowQTELMAkGA1UEBhMCVVMxFDASBgNVBAoMC0FmZmlybVRy +dXN0MRwwGgYDVQQDDBNBZmZpcm1UcnVzdCBQcmVtaXVtMIICIjANBgkqhkiG9w0BAQEFAAOCAg8A +MIICCgKCAgEAxBLfqV/+Qd3d9Z+K4/as4Tx4mrzY8H96oDMq3I0gW64tb+eT2TZwamjPjlGjhVtn +BKAQJG9dKILBl1fYSCkTtuG+kU3fhQxTGJoeJKJPj/CihQvL9Cl/0qRY7iZNyaqoe5rZ+jjeRFcV +5fiMyNlI4g0WJx0eyIOFJbe6qlVBzAMiSy2RjYvmia9mx+n/K+k8rNrSs8PhaJyJ+HoAVt70VZVs ++7pk3WKL3wt3MutizCaam7uqYoNMtAZ6MMgpv+0GTZe5HMQxK9VfvFMSF5yZVylmd2EhMQcuJUmd +GPLu8ytxjLW6OQdJd/zvLpKQBY0tL3d770O/Nbua2Plzpyzy0FfuKE4mX4+QaAkvuPjcBukumj5R +p9EixAqnOEhss/n/fauGV+O61oV4d7pD6kh/9ti+I20ev9E2bFhc8e6kGVQa9QPSdubhjL08s9NI +S+LI+H+SqHZGnEJlPqQewQcDWkYtuJfzt9WyVSHvutxMAJf7FJUnM7/oQ0dG0giZFmA7mn7S5u04 +6uwBHjxIVkkJx0w3AJ6IDsBz4W9m6XJHMD4Q5QsDyZpCAGzFlH5hxIrff4IaC1nEWTJ3s7xgaVY5 +/bQGeyzWZDbZvUjthB9+pSKPKrhC9IK31FOQeE4tGv2Bb0TXOwF0lkLgAOIua+rF7nKsu7/+6qqo ++Nz2snmKtmcCAwEAAaNCMEAwHQYDVR0OBBYEFJ3AZ6YMItkm9UWrpmVSESfYRaxjMA8GA1UdEwEB +/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMA0GCSqGSIb3DQEBDAUAA4ICAQCzV00QYk465KzquByv +MiPIs0laUZx2KI15qldGF9X1Uva3ROgIRL8YhNILgM3FEv0AVQVhh0HctSSePMTYyPtwni94loMg +Nt58D2kTiKV1NpgIpsbfrM7jWNa3Pt668+s0QNiigfV4Py/VpfzZotReBA4Xrf5B8OWycvpEgjNC +6C1Y91aMYj+6QrCcDFx+LmUmXFNPALJ4fqENmS2NuB2OosSw/WDQMKSOyARiqcTtNd56l+0OOF6S +L5Nwpamcb6d9Ex1+xghIsV5n61EIJenmJWtSKZGc0jlzCFfemQa0W50QBuHCAKi4HEoCChTQwUHK ++4w1IX2COPKpVJEZNZOUbWo6xbLQu4mGk+ibyQ86p3q4ofB4Rvr8Ny/lioTz3/4E2aFooC8k4gmV +BtWVyuEklut89pMFu+1z6S3RdTnX5yTb2E5fQ4+e0BQ5v1VwSJlXMbSc7kqYA5YwH2AG7hsj/oFg +IxpHYoWlzBk0gG+zrBrjn/B7SK3VAdlntqlyk+otZrWyuOQ9PLLvTIzq6we/qzWaVYa8GKa1qF60 +g2xraUDTn9zxw2lrueFtCfTxqlB2Cnp9ehehVZZCmTEJ3WARjQUwfuaORtGdFNrHF+QFlozEJLUb +zxQHskD4o55BhrwE0GuWyCqANP2/7waj3VjFhT0+j/6eKeC2uAloGRwYQw== +-----END CERTIFICATE----- + +AffirmTrust Premium ECC +======================= +-----BEGIN CERTIFICATE----- +MIIB/jCCAYWgAwIBAgIIdJclisc/elQwCgYIKoZIzj0EAwMwRTELMAkGA1UEBhMCVVMxFDASBgNV +BAoMC0FmZmlybVRydXN0MSAwHgYDVQQDDBdBZmZpcm1UcnVzdCBQcmVtaXVtIEVDQzAeFw0xMDAx +MjkxNDIwMjRaFw00MDEyMzExNDIwMjRaMEUxCzAJBgNVBAYTAlVTMRQwEgYDVQQKDAtBZmZpcm1U +cnVzdDEgMB4GA1UEAwwXQWZmaXJtVHJ1c3QgUHJlbWl1bSBFQ0MwdjAQBgcqhkjOPQIBBgUrgQQA +IgNiAAQNMF4bFZ0D0KF5Nbc6PJJ6yhUczWLznCZcBz3lVPqj1swS6vQUX+iOGasvLkjmrBhDeKzQ +N8O9ss0s5kfiGuZjuD0uL3jET9v0D6RoTFVya5UdThhClXjMNzyR4ptlKymjQjBAMB0GA1UdDgQW +BBSaryl6wBE1NSZRMADDav5A1a7WPDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAK +BggqhkjOPQQDAwNnADBkAjAXCfOHiFBar8jAQr9HX/VsaobgxCd05DhT1wV/GzTjxi+zygk8N53X +57hG8f2h4nECMEJZh0PUUd+60wkyWs6Iflc9nF9Ca/UHLbXwgpP5WW+uZPpY5Yse42O+tYHNbwKM +eQ== +-----END CERTIFICATE----- + +Certum Trusted Network CA +========================= +-----BEGIN CERTIFICATE----- +MIIDuzCCAqOgAwIBAgIDBETAMA0GCSqGSIb3DQEBBQUAMH4xCzAJBgNVBAYTAlBMMSIwIAYDVQQK +ExlVbml6ZXRvIFRlY2hub2xvZ2llcyBTLkEuMScwJQYDVQQLEx5DZXJ0dW0gQ2VydGlmaWNhdGlv +biBBdXRob3JpdHkxIjAgBgNVBAMTGUNlcnR1bSBUcnVzdGVkIE5ldHdvcmsgQ0EwHhcNMDgxMDIy +MTIwNzM3WhcNMjkxMjMxMTIwNzM3WjB+MQswCQYDVQQGEwJQTDEiMCAGA1UEChMZVW5pemV0byBU +ZWNobm9sb2dpZXMgUy5BLjEnMCUGA1UECxMeQ2VydHVtIENlcnRpZmljYXRpb24gQXV0aG9yaXR5 +MSIwIAYDVQQDExlDZXJ0dW0gVHJ1c3RlZCBOZXR3b3JrIENBMIIBIjANBgkqhkiG9w0BAQEFAAOC +AQ8AMIIBCgKCAQEA4/t9o3K6wvDJFIf1awFO4W5AB7ptJ11/91sts1rHUV+rpDKmYYe2bg+G0jAC +l/jXaVehGDldamR5xgFZrDwxSjh80gTSSyjoIF87B6LMTXPb865Px1bVWqeWifrzq2jUI4ZZJ88J +J7ysbnKDHDBy3+Ci6dLhdHUZvSqeexVUBBvXQzmtVSjF4hq79MDkrjhJM8x2hZ85RdKknvISjFH4 +fOQtf/WsX+sWn7Et0brMkUJ3TCXJkDhv2/DM+44el1k+1WBO5gUo7Ul5E0u6SNsv+XLTOcr+H9g0 +cvW0QM8xAcPs3hEtF10fuFDRXhmnad4HMyjKUJX5p1TLVIZQRan5SQIDAQABo0IwQDAPBgNVHRMB +Af8EBTADAQH/MB0GA1UdDgQWBBQIds3LB/8k9sXN7buQvOKEN0Z19zAOBgNVHQ8BAf8EBAMCAQYw +DQYJKoZIhvcNAQEFBQADggEBAKaorSLOAT2mo/9i0Eidi15ysHhE49wcrwn9I0j6vSrEuVUEtRCj +jSfeC4Jj0O7eDDd5QVsisrCaQVymcODU0HfLI9MA4GxWL+FpDQ3Zqr8hgVDZBqWo/5U30Kr+4rP1 +mS1FhIrlQgnXdAIv94nYmem8J9RHjboNRhx3zxSkHLmkMcScKHQDNP8zGSal6Q10tz6XxnboJ5aj +Zt3hrvJBW8qYVoNzcOSGGtIxQbovvi0TWnZvTuhOgQ4/WwMioBK+ZlgRSssDxLQqKi2WF+A5VLxI +03YnnZotBqbJ7DnSq9ufmgsnAjUpsUCV5/nonFWIGUbWtzT1fs45mtk48VH3Tyw= +-----END CERTIFICATE----- + +TWCA Root Certification Authority +================================= +-----BEGIN CERTIFICATE----- +MIIDezCCAmOgAwIBAgIBATANBgkqhkiG9w0BAQUFADBfMQswCQYDVQQGEwJUVzESMBAGA1UECgwJ +VEFJV0FOLUNBMRAwDgYDVQQLDAdSb290IENBMSowKAYDVQQDDCFUV0NBIFJvb3QgQ2VydGlmaWNh +dGlvbiBBdXRob3JpdHkwHhcNMDgwODI4MDcyNDMzWhcNMzAxMjMxMTU1OTU5WjBfMQswCQYDVQQG +EwJUVzESMBAGA1UECgwJVEFJV0FOLUNBMRAwDgYDVQQLDAdSb290IENBMSowKAYDVQQDDCFUV0NB +IFJvb3QgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK +AoIBAQCwfnK4pAOU5qfeCTiRShFAh6d8WWQUe7UREN3+v9XAu1bihSX0NXIP+FPQQeFEAcK0HMMx +QhZHhTMidrIKbw/lJVBPhYa+v5guEGcevhEFhgWQxFnQfHgQsIBct+HHK3XLfJ+utdGdIzdjp9xC +oi2SBBtQwXu4PhvJVgSLL1KbralW6cH/ralYhzC2gfeXRfwZVzsrb+RH9JlF/h3x+JejiB03HFyP +4HYlmlD4oFT/RJB2I9IyxsOrBr/8+7/zrX2SYgJbKdM1o5OaQ2RgXbL6Mv87BK9NQGr5x+PvI/1r +y+UPizgN7gr8/g+YnzAx3WxSZfmLgb4i4RxYA7qRG4kHAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIB +BjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBRqOFsmjd6LWvJPelSDGRjjCDWmujANBgkqhkiG +9w0BAQUFAAOCAQEAPNV3PdrfibqHDAhUaiBQkr6wQT25JmSDCi/oQMCXKCeCMErJk/9q56YAf4lC +mtYR5VPOL8zy2gXE/uJQxDqGfczafhAJO5I1KlOy/usrBdlsXebQ79NqZp4VKIV66IIArB6nCWlW +QtNoURi+VJq/REG6Sb4gumlc7rh3zc5sH62Dlhh9DrUUOYTxKOkto557HnpyWoOzeW/vtPzQCqVY +T0bf+215WfKEIlKuD8z7fDvnaspHYcN6+NOSBB+4IIThNlQWx0DeO4pz3N/GCUzf7Nr/1FNCocny +Yh0igzyXxfkZYiesZSLX0zzG5Y6yU8xJzrww/nsOM5D77dIUkR8Hrw== +-----END CERTIFICATE----- + +Security Communication RootCA2 +============================== +-----BEGIN CERTIFICATE----- +MIIDdzCCAl+gAwIBAgIBADANBgkqhkiG9w0BAQsFADBdMQswCQYDVQQGEwJKUDElMCMGA1UEChMc +U0VDT00gVHJ1c3QgU3lzdGVtcyBDTy4sTFRELjEnMCUGA1UECxMeU2VjdXJpdHkgQ29tbXVuaWNh +dGlvbiBSb290Q0EyMB4XDTA5MDUyOTA1MDAzOVoXDTI5MDUyOTA1MDAzOVowXTELMAkGA1UEBhMC +SlAxJTAjBgNVBAoTHFNFQ09NIFRydXN0IFN5c3RlbXMgQ08uLExURC4xJzAlBgNVBAsTHlNlY3Vy +aXR5IENvbW11bmljYXRpb24gUm9vdENBMjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB +ANAVOVKxUrO6xVmCxF1SrjpDZYBLx/KWvNs2l9amZIyoXvDjChz335c9S672XewhtUGrzbl+dp++ ++T42NKA7wfYxEUV0kz1XgMX5iZnK5atq1LXaQZAQwdbWQonCv/Q4EpVMVAX3NuRFg3sUZdbcDE3R +3n4MqzvEFb46VqZab3ZpUql6ucjrappdUtAtCms1FgkQhNBqyjoGADdH5H5XTz+L62e4iKrFvlNV +spHEfbmwhRkGeC7bYRr6hfVKkaHnFtWOojnflLhwHyg/i/xAXmODPIMqGplrz95Zajv8bxbXH/1K +EOtOghY6rCcMU/Gt1SSwawNQwS08Ft1ENCcadfsCAwEAAaNCMEAwHQYDVR0OBBYEFAqFqXdlBZh8 +QIH4D5csOPEK7DzPMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEB +CwUAA4IBAQBMOqNErLlFsceTfsgLCkLfZOoc7llsCLqJX2rKSpWeeo8HxdpFcoJxDjrSzG+ntKEj +u/Ykn8sX/oymzsLS28yN/HH8AynBbF0zX2S2ZTuJbxh2ePXcokgfGT+Ok+vx+hfuzU7jBBJV1uXk +3fs+BXziHV7Gp7yXT2g69ekuCkO2r1dcYmh8t/2jioSgrGK+KwmHNPBqAbubKVY8/gA3zyNs8U6q +tnRGEmyR7jTV7JqR50S+kDFy1UkC9gLl9B/rfNmWVan/7Ir5mUf/NVoCqgTLiluHcSmRvaS0eg29 +mvVXIwAHIRc/SjnRBUkLp7Y3gaVdjKozXoEofKd9J+sAro03 +-----END CERTIFICATE----- + +Actalis Authentication Root CA +============================== +-----BEGIN CERTIFICATE----- +MIIFuzCCA6OgAwIBAgIIVwoRl0LE48wwDQYJKoZIhvcNAQELBQAwazELMAkGA1UEBhMCSVQxDjAM +BgNVBAcMBU1pbGFuMSMwIQYDVQQKDBpBY3RhbGlzIFMucC5BLi8wMzM1ODUyMDk2NzEnMCUGA1UE +AwweQWN0YWxpcyBBdXRoZW50aWNhdGlvbiBSb290IENBMB4XDTExMDkyMjExMjIwMloXDTMwMDky +MjExMjIwMlowazELMAkGA1UEBhMCSVQxDjAMBgNVBAcMBU1pbGFuMSMwIQYDVQQKDBpBY3RhbGlz +IFMucC5BLi8wMzM1ODUyMDk2NzEnMCUGA1UEAwweQWN0YWxpcyBBdXRoZW50aWNhdGlvbiBSb290 +IENBMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAp8bEpSmkLO/lGMWwUKNvUTufClrJ +wkg4CsIcoBh/kbWHuUA/3R1oHwiD1S0eiKD4j1aPbZkCkpAW1V8IbInX4ay8IMKx4INRimlNAJZa +by/ARH6jDuSRzVju3PvHHkVH3Se5CAGfpiEd9UEtL0z9KK3giq0itFZljoZUj5NDKd45RnijMCO6 +zfB9E1fAXdKDa0hMxKufgFpbOr3JpyI/gCczWw63igxdBzcIy2zSekciRDXFzMwujt0q7bd9Zg1f +YVEiVRvjRuPjPdA1YprbrxTIW6HMiRvhMCb8oJsfgadHHwTrozmSBp+Z07/T6k9QnBn+locePGX2 +oxgkg4YQ51Q+qDp2JE+BIcXjDwL4k5RHILv+1A7TaLndxHqEguNTVHnd25zS8gebLra8Pu2Fbe8l +EfKXGkJh90qX6IuxEAf6ZYGyojnP9zz/GPvG8VqLWeICrHuS0E4UT1lF9gxeKF+w6D9Fz8+vm2/7 +hNN3WpVvrJSEnu68wEqPSpP4RCHiMUVhUE4Q2OM1fEwZtN4Fv6MGn8i1zeQf1xcGDXqVdFUNaBr8 +EBtiZJ1t4JWgw5QHVw0U5r0F+7if5t+L4sbnfpb2U8WANFAoWPASUHEXMLrmeGO89LKtmyuy/uE5 +jF66CyCU3nuDuP/jVo23Eek7jPKxwV2dpAtMK9myGPW1n0sCAwEAAaNjMGEwHQYDVR0OBBYEFFLY +iDrIn3hm7YnzezhwlMkCAjbQMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAUUtiIOsifeGbt +ifN7OHCUyQICNtAwDgYDVR0PAQH/BAQDAgEGMA0GCSqGSIb3DQEBCwUAA4ICAQALe3KHwGCmSUyI +WOYdiPcUZEim2FgKDk8TNd81HdTtBjHIgT5q1d07GjLukD0R0i70jsNjLiNmsGe+b7bAEzlgqqI0 +JZN1Ut6nna0Oh4lScWoWPBkdg/iaKWW+9D+a2fDzWochcYBNy+A4mz+7+uAwTc+G02UQGRjRlwKx +K3JCaKygvU5a2hi/a5iB0P2avl4VSM0RFbnAKVy06Ij3Pjaut2L9HmLecHgQHEhb2rykOLpn7VU+ +Xlff1ANATIGk0k9jpwlCCRT8AKnCgHNPLsBA2RF7SOp6AsDT6ygBJlh0wcBzIm2Tlf05fbsq4/aC +4yyXX04fkZT6/iyj2HYauE2yOE+b+h1IYHkm4vP9qdCa6HCPSXrW5b0KDtst842/6+OkfcvHlXHo +2qN8xcL4dJIEG4aspCJTQLas/kx2z/uUMsA1n3Y/buWQbqCmJqK4LL7RK4X9p2jIugErsWx0Hbhz +lefut8cl8ABMALJ+tguLHPPAUJ4lueAI3jZm/zel0btUZCzJJ7VLkn5l/9Mt4blOvH+kQSGQQXem +OR/qnuOf0GZvBeyqdn6/axag67XH/JJULysRJyU3eExRarDzzFhdFPFqSBX/wge2sY0PjlxQRrM9 +vwGYT7JZVEc+NHt4bVaTLnPqZih4zR0Uv6CPLy64Lo7yFIrM6bV8+2ydDKXhlg== +-----END CERTIFICATE----- + +Buypass Class 2 Root CA +======================= +-----BEGIN CERTIFICATE----- +MIIFWTCCA0GgAwIBAgIBAjANBgkqhkiG9w0BAQsFADBOMQswCQYDVQQGEwJOTzEdMBsGA1UECgwU +QnV5cGFzcyBBUy05ODMxNjMzMjcxIDAeBgNVBAMMF0J1eXBhc3MgQ2xhc3MgMiBSb290IENBMB4X +DTEwMTAyNjA4MzgwM1oXDTQwMTAyNjA4MzgwM1owTjELMAkGA1UEBhMCTk8xHTAbBgNVBAoMFEJ1 +eXBhc3MgQVMtOTgzMTYzMzI3MSAwHgYDVQQDDBdCdXlwYXNzIENsYXNzIDIgUm9vdCBDQTCCAiIw +DQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBANfHXvfBB9R3+0Mh9PT1aeTuMgHbo4Yf5FkNuud1 +g1Lr6hxhFUi7HQfKjK6w3Jad6sNgkoaCKHOcVgb/S2TwDCo3SbXlzwx87vFKu3MwZfPVL4O2fuPn +9Z6rYPnT8Z2SdIrkHJasW4DptfQxh6NR/Md+oW+OU3fUl8FVM5I+GC911K2GScuVr1QGbNgGE41b +/+EmGVnAJLqBcXmQRFBoJJRfuLMR8SlBYaNByyM21cHxMlAQTn/0hpPshNOOvEu/XAFOBz3cFIqU +CqTqc/sLUegTBxj6DvEr0VQVfTzh97QZQmdiXnfgolXsttlpF9U6r0TtSsWe5HonfOV116rLJeff +awrbD02TTqigzXsu8lkBarcNuAeBfos4GzjmCleZPe4h6KP1DBbdi+w0jpwqHAAVF41og9JwnxgI +zRFo1clrUs3ERo/ctfPYV3Me6ZQ5BL/T3jjetFPsaRyifsSP5BtwrfKi+fv3FmRmaZ9JUaLiFRhn +Bkp/1Wy1TbMz4GHrXb7pmA8y1x1LPC5aAVKRCfLf6o3YBkBjqhHk/sM3nhRSP/TizPJhk9H9Z2vX +Uq6/aKtAQ6BXNVN48FP4YUIHZMbXb5tMOA1jrGKvNouicwoN9SG9dKpN6nIDSdvHXx1iY8f93ZHs +M+71bbRuMGjeyNYmsHVee7QHIJihdjK4TWxPAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wHQYD +VR0OBBYEFMmAd+BikoL1RpzzuvdMw964o605MA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQsF +AAOCAgEAU18h9bqwOlI5LJKwbADJ784g7wbylp7ppHR/ehb8t/W2+xUbP6umwHJdELFx7rxP462s +A20ucS6vxOOto70MEae0/0qyexAQH6dXQbLArvQsWdZHEIjzIVEpMMpghq9Gqx3tOluwlN5E40EI +osHsHdb9T7bWR9AUC8rmyrV7d35BH16Dx7aMOZawP5aBQW9gkOLo+fsicdl9sz1Gv7SEr5AcD48S +aq/v7h56rgJKihcrdv6sVIkkLE8/trKnToyokZf7KcZ7XC25y2a2t6hbElGFtQl+Ynhw/qlqYLYd +DnkM/crqJIByw5c/8nerQyIKx+u2DISCLIBrQYoIwOula9+ZEsuK1V6ADJHgJgg2SMX6OBE1/yWD +LfJ6v9r9jv6ly0UsH8SIU653DtmadsWOLB2jutXsMq7Aqqz30XpN69QH4kj3Io6wpJ9qzo6ysmD0 +oyLQI+uUWnpp3Q+/QFesa1lQ2aOZ4W7+jQF5JyMV3pKdewlNWudLSDBaGOYKbeaP4NK75t98biGC +wWg5TbSYWGZizEqQXsP6JwSxeRV0mcy+rSDeJmAc61ZRpqPq5KM/p/9h3PFaTWwyI0PurKju7koS +CTxdccK+efrCh2gdC/1cacwG0Jp9VJkqyTkaGa9LKkPzY11aWOIv4x3kqdbQCtCev9eBCfHJxyYN +rJgWVqA= +-----END CERTIFICATE----- + +Buypass Class 3 Root CA +======================= +-----BEGIN CERTIFICATE----- +MIIFWTCCA0GgAwIBAgIBAjANBgkqhkiG9w0BAQsFADBOMQswCQYDVQQGEwJOTzEdMBsGA1UECgwU +QnV5cGFzcyBBUy05ODMxNjMzMjcxIDAeBgNVBAMMF0J1eXBhc3MgQ2xhc3MgMyBSb290IENBMB4X +DTEwMTAyNjA4Mjg1OFoXDTQwMTAyNjA4Mjg1OFowTjELMAkGA1UEBhMCTk8xHTAbBgNVBAoMFEJ1 +eXBhc3MgQVMtOTgzMTYzMzI3MSAwHgYDVQQDDBdCdXlwYXNzIENsYXNzIDMgUm9vdCBDQTCCAiIw +DQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAKXaCpUWUOOV8l6ddjEGMnqb8RB2uACatVI2zSRH +sJ8YZLya9vrVediQYkwiL944PdbgqOkcLNt4EemOaFEVcsfzM4fkoF0LXOBXByow9c3EN3coTRiR +5r/VUv1xLXA+58bEiuPwKAv0dpihi4dVsjoT/Lc+JzeOIuOoTyrvYLs9tznDDgFHmV0ST9tD+leh +7fmdvhFHJlsTmKtdFoqwNxxXnUX/iJY2v7vKB3tvh2PX0DJq1l1sDPGzbjniazEuOQAnFN44wOwZ +ZoYS6J1yFhNkUsepNxz9gjDthBgd9K5c/3ATAOux9TN6S9ZV+AWNS2mw9bMoNlwUxFFzTWsL8TQH +2xc519woe2v1n/MuwU8XKhDzzMro6/1rqy6any2CbgTUUgGTLT2G/H783+9CHaZr77kgxve9oKeV +/afmiSTYzIw0bOIjL9kSGiG5VZFvC5F5GQytQIgLcOJ60g7YaEi7ghM5EFjp2CoHxhLbWNvSO1UQ +RwUVZ2J+GGOmRj8JDlQyXr8NYnon74Do29lLBlo3WiXQCBJ31G8JUJc9yB3D34xFMFbG02SrZvPA +Xpacw8Tvw3xrizp5f7NJzz3iiZ+gMEuFuZyUJHmPfWupRWgPK9Dx2hzLabjKSWJtyNBjYt1gD1iq +j6G8BaVmos8bdrKEZLFMOVLAMLrwjEsCsLa3AgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wHQYD +VR0OBBYEFEe4zf/lb+74suwvTg75JbCOPGvDMA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQsF +AAOCAgEAACAjQTUEkMJAYmDv4jVM1z+s4jSQuKFvdvoWFqRINyzpkMLyPPgKn9iB5btb2iUspKdV +cSQy9sgL8rxq+JOssgfCX5/bzMiKqr5qb+FJEMwx14C7u8jYog5kV+qi9cKpMRXSIGrs/CIBKM+G +uIAeqcwRpTzyFrNHnfzSgCHEy9BHcEGhyoMZCCxt8l13nIoUE9Q2HJLw5QY33KbmkJs4j1xrG0aG +Q0JfPgEHU1RdZX33inOhmlRaHylDFCfChQ+1iHsaO5S3HWCntZznKWlXWpuTekMwGwPXYshApqr8 +ZORK15FTAaggiG6cX0S5y2CBNOxv033aSF/rtJC8LakcC6wc1aJoIIAE1vyxjy+7SjENSoYc6+I2 +KSb12tjE8nVhz36udmNKekBlk4f4HoCMhuWG1o8O/FMsYOgWYRqiPkN7zTlgVGr18okmAWiDSKIz +6MkEkbIRNBE+6tBDGR8Dk5AM/1E9V/RBbuHLoL7ryWPNbczk+DaqaJ3tvV2XcEQNtg413OEMXbug +UZTLfhbrES+jkkXITHHZvMmZUldGL1DPvTVp9D0VzgalLA8+9oG6lLvDu79leNKGef9JOxqDDPDe +eOzI8k1MGt6CKfjBWtrt7uYnXuhF0J0cUahoq0Tj0Itq4/g7u9xN12TyUb7mqqta6THuBrxzvxNi +Cp/HuZc= +-----END CERTIFICATE----- + +T-TeleSec GlobalRoot Class 3 +============================ +-----BEGIN CERTIFICATE----- +MIIDwzCCAqugAwIBAgIBATANBgkqhkiG9w0BAQsFADCBgjELMAkGA1UEBhMCREUxKzApBgNVBAoM +IlQtU3lzdGVtcyBFbnRlcnByaXNlIFNlcnZpY2VzIEdtYkgxHzAdBgNVBAsMFlQtU3lzdGVtcyBU +cnVzdCBDZW50ZXIxJTAjBgNVBAMMHFQtVGVsZVNlYyBHbG9iYWxSb290IENsYXNzIDMwHhcNMDgx +MDAxMTAyOTU2WhcNMzMxMDAxMjM1OTU5WjCBgjELMAkGA1UEBhMCREUxKzApBgNVBAoMIlQtU3lz +dGVtcyBFbnRlcnByaXNlIFNlcnZpY2VzIEdtYkgxHzAdBgNVBAsMFlQtU3lzdGVtcyBUcnVzdCBD +ZW50ZXIxJTAjBgNVBAMMHFQtVGVsZVNlYyBHbG9iYWxSb290IENsYXNzIDMwggEiMA0GCSqGSIb3 +DQEBAQUAA4IBDwAwggEKAoIBAQC9dZPwYiJvJK7genasfb3ZJNW4t/zN8ELg63iIVl6bmlQdTQyK +9tPPcPRStdiTBONGhnFBSivwKixVA9ZIw+A5OO3yXDw/RLyTPWGrTs0NvvAgJ1gORH8EGoel15YU +NpDQSXuhdfsaa3Ox+M6pCSzyU9XDFES4hqX2iys52qMzVNn6chr3IhUciJFrf2blw2qAsCTz34ZF +iP0Zf3WHHx+xGwpzJFu5ZeAsVMhg02YXP+HMVDNzkQI6pn97djmiH5a2OK61yJN0HZ65tOVgnS9W +0eDrXltMEnAMbEQgqxHY9Bn20pxSN+f6tsIxO0rUFJmtxxr1XV/6B7h8DR/Wgx6zAgMBAAGjQjBA +MA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBS1A/d2O2GCahKqGFPr +AyGUv/7OyjANBgkqhkiG9w0BAQsFAAOCAQEAVj3vlNW92nOyWL6ukK2YJ5f+AbGwUgC4TeQbIXQb +fsDuXmkqJa9c1h3a0nnJ85cp4IaH3gRZD/FZ1GSFS5mvJQQeyUapl96Cshtwn5z2r3Ex3XsFpSzT +ucpH9sry9uetuUg/vBa3wW306gmv7PO15wWeph6KU1HWk4HMdJP2udqmJQV0eVp+QD6CSyYRMG7h +P0HHRwA11fXT91Q+gT3aSWqas+8QPebrb9HIIkfLzM8BMZLZGOMivgkeGj5asuRrDFR6fUNOuIml +e9eiPZaGzPImNC1qkp2aGtAw4l1OBLBfiyB+d8E9lYLRRpo7PHi4b6HQDWSieB4pTpPDpFQUWw== +-----END CERTIFICATE----- + +D-TRUST Root Class 3 CA 2 2009 +============================== +-----BEGIN CERTIFICATE----- +MIIEMzCCAxugAwIBAgIDCYPzMA0GCSqGSIb3DQEBCwUAME0xCzAJBgNVBAYTAkRFMRUwEwYDVQQK +DAxELVRydXN0IEdtYkgxJzAlBgNVBAMMHkQtVFJVU1QgUm9vdCBDbGFzcyAzIENBIDIgMjAwOTAe +Fw0wOTExMDUwODM1NThaFw0yOTExMDUwODM1NThaME0xCzAJBgNVBAYTAkRFMRUwEwYDVQQKDAxE +LVRydXN0IEdtYkgxJzAlBgNVBAMMHkQtVFJVU1QgUm9vdCBDbGFzcyAzIENBIDIgMjAwOTCCASIw +DQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANOySs96R+91myP6Oi/WUEWJNTrGa9v+2wBoqOAD +ER03UAifTUpolDWzU9GUY6cgVq/eUXjsKj3zSEhQPgrfRlWLJ23DEE0NkVJD2IfgXU42tSHKXzlA +BF9bfsyjxiupQB7ZNoTWSPOSHjRGICTBpFGOShrvUD9pXRl/RcPHAY9RySPocq60vFYJfxLLHLGv +KZAKyVXMD9O0Gu1HNVpK7ZxzBCHQqr0ME7UAyiZsxGsMlFqVlNpQmvH/pStmMaTJOKDfHR+4CS7z +p+hnUquVH+BGPtikw8paxTGA6Eian5Rp/hnd2HN8gcqW3o7tszIFZYQ05ub9VxC1X3a/L7AQDcUC +AwEAAaOCARowggEWMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFP3aFMSfMN4hvR5COfyrYyNJ +4PGEMA4GA1UdDwEB/wQEAwIBBjCB0wYDVR0fBIHLMIHIMIGAoH6gfIZ6bGRhcDovL2RpcmVjdG9y +eS5kLXRydXN0Lm5ldC9DTj1ELVRSVVNUJTIwUm9vdCUyMENsYXNzJTIwMyUyMENBJTIwMiUyMDIw +MDksTz1ELVRydXN0JTIwR21iSCxDPURFP2NlcnRpZmljYXRlcmV2b2NhdGlvbmxpc3QwQ6BBoD+G +PWh0dHA6Ly93d3cuZC10cnVzdC5uZXQvY3JsL2QtdHJ1c3Rfcm9vdF9jbGFzc18zX2NhXzJfMjAw +OS5jcmwwDQYJKoZIhvcNAQELBQADggEBAH+X2zDI36ScfSF6gHDOFBJpiBSVYEQBrLLpME+bUMJm +2H6NMLVwMeniacfzcNsgFYbQDfC+rAF1hM5+n02/t2A7nPPKHeJeaNijnZflQGDSNiH+0LS4F9p0 +o3/U37CYAqxva2ssJSRyoWXuJVrl5jLn8t+rSfrzkGkj2wTZ51xY/GXUl77M/C4KzCUqNQT4YJEV +dT1B/yMfGchs64JTBKbkTCJNjYy6zltz7GRUUG3RnFX7acM2w4y8PIWmawomDeCTmGCufsYkl4ph +X5GOZpIJhzbNi5stPvZR1FDUWSi9g/LMKHtThm3YJohw1+qRzT65ysCQblrGXnRl11z+o+I= +-----END CERTIFICATE----- + +D-TRUST Root Class 3 CA 2 EV 2009 +================================= +-----BEGIN CERTIFICATE----- +MIIEQzCCAyugAwIBAgIDCYP0MA0GCSqGSIb3DQEBCwUAMFAxCzAJBgNVBAYTAkRFMRUwEwYDVQQK +DAxELVRydXN0IEdtYkgxKjAoBgNVBAMMIUQtVFJVU1QgUm9vdCBDbGFzcyAzIENBIDIgRVYgMjAw +OTAeFw0wOTExMDUwODUwNDZaFw0yOTExMDUwODUwNDZaMFAxCzAJBgNVBAYTAkRFMRUwEwYDVQQK +DAxELVRydXN0IEdtYkgxKjAoBgNVBAMMIUQtVFJVU1QgUm9vdCBDbGFzcyAzIENBIDIgRVYgMjAw +OTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAJnxhDRwui+3MKCOvXwEz75ivJn9gpfS +egpnljgJ9hBOlSJzmY3aFS3nBfwZcyK3jpgAvDw9rKFs+9Z5JUut8Mxk2og+KbgPCdM03TP1YtHh +zRnp7hhPTFiu4h7WDFsVWtg6uMQYZB7jM7K1iXdODL/ZlGsTl28So/6ZqQTMFexgaDbtCHu39b+T +7WYxg4zGcTSHThfqr4uRjRxWQa4iN1438h3Z0S0NL2lRp75mpoo6Kr3HGrHhFPC+Oh25z1uxav60 +sUYgovseO3Dvk5h9jHOW8sXvhXCtKSb8HgQ+HKDYD8tSg2J87otTlZCpV6LqYQXY+U3EJ/pure35 +11H3a6UCAwEAAaOCASQwggEgMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFNOUikxiEyoZLsyv +cop9NteaHNxnMA4GA1UdDwEB/wQEAwIBBjCB3QYDVR0fBIHVMIHSMIGHoIGEoIGBhn9sZGFwOi8v +ZGlyZWN0b3J5LmQtdHJ1c3QubmV0L0NOPUQtVFJVU1QlMjBSb290JTIwQ2xhc3MlMjAzJTIwQ0El +MjAyJTIwRVYlMjAyMDA5LE89RC1UcnVzdCUyMEdtYkgsQz1ERT9jZXJ0aWZpY2F0ZXJldm9jYXRp +b25saXN0MEagRKBChkBodHRwOi8vd3d3LmQtdHJ1c3QubmV0L2NybC9kLXRydXN0X3Jvb3RfY2xh +c3NfM19jYV8yX2V2XzIwMDkuY3JsMA0GCSqGSIb3DQEBCwUAA4IBAQA07XtaPKSUiO8aEXUHL7P+ +PPoeUSbrh/Yp3uDx1MYkCenBz1UbtDDZzhr+BlGmFaQt77JLvyAoJUnRpjZ3NOhk31KxEcdzes05 +nsKtjHEh8lprr988TlWvsoRlFIm5d8sqMb7Po23Pb0iUMkZv53GMoKaEGTcH8gNFCSuGdXzfX2lX +ANtu2KZyIktQ1HWYVt+3GP9DQ1CuekR78HlR10M9p9OB0/DJT7naxpeG0ILD5EJt/rDiZE4OJudA +NCa1CInXCGNjOCd1HjPqbqjdn5lPdE2BiYBL3ZqXKVwvvoFBuYz/6n1gBp7N1z3TLqMVvKjmJuVv +w9y4AyHqnxbxLFS1 +-----END CERTIFICATE----- + +CA Disig Root R2 +================ +-----BEGIN CERTIFICATE----- +MIIFaTCCA1GgAwIBAgIJAJK4iNuwisFjMA0GCSqGSIb3DQEBCwUAMFIxCzAJBgNVBAYTAlNLMRMw +EQYDVQQHEwpCcmF0aXNsYXZhMRMwEQYDVQQKEwpEaXNpZyBhLnMuMRkwFwYDVQQDExBDQSBEaXNp +ZyBSb290IFIyMB4XDTEyMDcxOTA5MTUzMFoXDTQyMDcxOTA5MTUzMFowUjELMAkGA1UEBhMCU0sx +EzARBgNVBAcTCkJyYXRpc2xhdmExEzARBgNVBAoTCkRpc2lnIGEucy4xGTAXBgNVBAMTEENBIERp +c2lnIFJvb3QgUjIwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCio8QACdaFXS1tFPbC +w3OeNcJxVX6B+6tGUODBfEl45qt5WDza/3wcn9iXAng+a0EE6UG9vgMsRfYvZNSrXaNHPWSb6Wia +xswbP7q+sos0Ai6YVRn8jG+qX9pMzk0DIaPY0jSTVpbLTAwAFjxfGs3Ix2ymrdMxp7zo5eFm1tL7 +A7RBZckQrg4FY8aAamkw/dLukO8NJ9+flXP04SXabBbeQTg06ov80egEFGEtQX6sx3dOy1FU+16S +GBsEWmjGycT6txOgmLcRK7fWV8x8nhfRyyX+hk4kLlYMeE2eARKmK6cBZW58Yh2EhN/qwGu1pSqV +g8NTEQxzHQuyRpDRQjrOQG6Vrf/GlK1ul4SOfW+eioANSW1z4nuSHsPzwfPrLgVv2RvPN3YEyLRa +5Beny912H9AZdugsBbPWnDTYltxhh5EF5EQIM8HauQhl1K6yNg3ruji6DOWbnuuNZt2Zz9aJQfYE +koopKW1rOhzndX0CcQ7zwOe9yxndnWCywmZgtrEE7snmhrmaZkCo5xHtgUUDi/ZnWejBBhG93c+A +Ak9lQHhcR1DIm+YfgXvkRKhbhZri3lrVx/k6RGZL5DJUfORsnLMOPReisjQS1n6yqEm70XooQL6i +Fh/f5DcfEXP7kAplQ6INfPgGAVUzfbANuPT1rqVCV3w2EYx7XsQDnYx5nQIDAQABo0IwQDAPBgNV +HRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUtZn4r7CU9eMg1gqtzk5WpC5u +Qu0wDQYJKoZIhvcNAQELBQADggIBACYGXnDnZTPIgm7ZnBc6G3pmsgH2eDtpXi/q/075KMOYKmFM +tCQSin1tERT3nLXK5ryeJ45MGcipvXrA1zYObYVybqjGom32+nNjf7xueQgcnYqfGopTpti72TVV +sRHFqQOzVju5hJMiXn7B9hJSi+osZ7z+Nkz1uM/Rs0mSO9MpDpkblvdhuDvEK7Z4bLQjb/D907Je +dR+Zlais9trhxTF7+9FGs9K8Z7RiVLoJ92Owk6Ka+elSLotgEqv89WBW7xBci8QaQtyDW2QOy7W8 +1k/BfDxujRNt+3vrMNDcTa/F1balTFtxyegxvug4BkihGuLq0t4SOVga/4AOgnXmt8kHbA7v/zjx +mHHEt38OFdAlab0inSvtBfZGR6ztwPDUO+Ls7pZbkBNOHlY667DvlruWIxG68kOGdGSVyCh13x01 +utI3gzhTODY7z2zp+WsO0PsE6E9312UBeIYMej4hYvF/Y3EMyZ9E26gnonW+boE+18DrG5gPcFw0 +sorMwIUY6256s/daoQe/qUKS82Ail+QUoQebTnbAjn39pCXHR+3/H3OszMOl6W8KjptlwlCFtaOg +UxLMVYdh84GuEEZhvUQhuMI9dM9+JDX6HAcOmz0iyu8xL4ysEr3vQCj8KWefshNPZiTEUxnpHikV +7+ZtsH8tZ/3zbBt1RqPlShfppNcL +-----END CERTIFICATE----- + +ACCVRAIZ1 +========= +-----BEGIN CERTIFICATE----- +MIIH0zCCBbugAwIBAgIIXsO3pkN/pOAwDQYJKoZIhvcNAQEFBQAwQjESMBAGA1UEAwwJQUNDVlJB +SVoxMRAwDgYDVQQLDAdQS0lBQ0NWMQ0wCwYDVQQKDARBQ0NWMQswCQYDVQQGEwJFUzAeFw0xMTA1 +MDUwOTM3MzdaFw0zMDEyMzEwOTM3MzdaMEIxEjAQBgNVBAMMCUFDQ1ZSQUlaMTEQMA4GA1UECwwH +UEtJQUNDVjENMAsGA1UECgwEQUNDVjELMAkGA1UEBhMCRVMwggIiMA0GCSqGSIb3DQEBAQUAA4IC +DwAwggIKAoICAQCbqau/YUqXry+XZpp0X9DZlv3P4uRm7x8fRzPCRKPfmt4ftVTdFXxpNRFvu8gM +jmoYHtiP2Ra8EEg2XPBjs5BaXCQ316PWywlxufEBcoSwfdtNgM3802/J+Nq2DoLSRYWoG2ioPej0 +RGy9ocLLA76MPhMAhN9KSMDjIgro6TenGEyxCQ0jVn8ETdkXhBilyNpAlHPrzg5XPAOBOp0KoVdD +aaxXbXmQeOW1tDvYvEyNKKGno6e6Ak4l0Squ7a4DIrhrIA8wKFSVf+DuzgpmndFALW4ir50awQUZ +0m/A8p/4e7MCQvtQqR0tkw8jq8bBD5L/0KIV9VMJcRz/RROE5iZe+OCIHAr8Fraocwa48GOEAqDG +WuzndN9wrqODJerWx5eHk6fGioozl2A3ED6XPm4pFdahD9GILBKfb6qkxkLrQaLjlUPTAYVtjrs7 +8yM2x/474KElB0iryYl0/wiPgL/AlmXz7uxLaL2diMMxs0Dx6M/2OLuc5NF/1OVYm3z61PMOm3WR +5LpSLhl+0fXNWhn8ugb2+1KoS5kE3fj5tItQo05iifCHJPqDQsGH+tUtKSpacXpkatcnYGMN285J +9Y0fkIkyF/hzQ7jSWpOGYdbhdQrqeWZ2iE9x6wQl1gpaepPluUsXQA+xtrn13k/c4LOsOxFwYIRK +Q26ZIMApcQrAZQIDAQABo4ICyzCCAscwfQYIKwYBBQUHAQEEcTBvMEwGCCsGAQUFBzAChkBodHRw +Oi8vd3d3LmFjY3YuZXMvZmlsZWFkbWluL0FyY2hpdm9zL2NlcnRpZmljYWRvcy9yYWl6YWNjdjEu +Y3J0MB8GCCsGAQUFBzABhhNodHRwOi8vb2NzcC5hY2N2LmVzMB0GA1UdDgQWBBTSh7Tj3zcnk1X2 +VuqB5TbMjB4/vTAPBgNVHRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFNKHtOPfNyeTVfZW6oHlNsyM +Hj+9MIIBcwYDVR0gBIIBajCCAWYwggFiBgRVHSAAMIIBWDCCASIGCCsGAQUFBwICMIIBFB6CARAA +QQB1AHQAbwByAGkAZABhAGQAIABkAGUAIABDAGUAcgB0AGkAZgBpAGMAYQBjAGkA8wBuACAAUgBh +AO0AegAgAGQAZQAgAGwAYQAgAEEAQwBDAFYAIAAoAEEAZwBlAG4AYwBpAGEAIABkAGUAIABUAGUA +YwBuAG8AbABvAGcA7QBhACAAeQAgAEMAZQByAHQAaQBmAGkAYwBhAGMAaQDzAG4AIABFAGwAZQBj +AHQAcgDzAG4AaQBjAGEALAAgAEMASQBGACAAUQA0ADYAMAAxADEANQA2AEUAKQAuACAAQwBQAFMA +IABlAG4AIABoAHQAdABwADoALwAvAHcAdwB3AC4AYQBjAGMAdgAuAGUAczAwBggrBgEFBQcCARYk +aHR0cDovL3d3dy5hY2N2LmVzL2xlZ2lzbGFjaW9uX2MuaHRtMFUGA1UdHwROMEwwSqBIoEaGRGh0 +dHA6Ly93d3cuYWNjdi5lcy9maWxlYWRtaW4vQXJjaGl2b3MvY2VydGlmaWNhZG9zL3JhaXphY2N2 +MV9kZXIuY3JsMA4GA1UdDwEB/wQEAwIBBjAXBgNVHREEEDAOgQxhY2N2QGFjY3YuZXMwDQYJKoZI +hvcNAQEFBQADggIBAJcxAp/n/UNnSEQU5CmH7UwoZtCPNdpNYbdKl02125DgBS4OxnnQ8pdpD70E +R9m+27Up2pvZrqmZ1dM8MJP1jaGo/AaNRPTKFpV8M9xii6g3+CfYCS0b78gUJyCpZET/LtZ1qmxN +YEAZSUNUY9rizLpm5U9EelvZaoErQNV/+QEnWCzI7UiRfD+mAM/EKXMRNt6GGT6d7hmKG9Ww7Y49 +nCrADdg9ZuM8Db3VlFzi4qc1GwQA9j9ajepDvV+JHanBsMyZ4k0ACtrJJ1vnE5Bc5PUzolVt3OAJ +TS+xJlsndQAJxGJ3KQhfnlmstn6tn1QwIgPBHnFk/vk4CpYY3QIUrCPLBhwepH2NDd4nQeit2hW3 +sCPdK6jT2iWH7ehVRE2I9DZ+hJp4rPcOVkkO1jMl1oRQQmwgEh0q1b688nCBpHBgvgW1m54ERL5h +I6zppSSMEYCUWqKiuUnSwdzRp+0xESyeGabu4VXhwOrPDYTkF7eifKXeVSUG7szAh1xA2syVP1Xg +Nce4hL60Xc16gwFy7ofmXx2utYXGJt/mwZrpHgJHnyqobalbz+xFd3+YJ5oyXSrjhO7FmGYvliAd +3djDJ9ew+f7Zfc3Qn48LFFhRny+Lwzgt3uiP1o2HpPVWQxaZLPSkVrQ0uGE3ycJYgBugl6H8WY3p +EfbRD0tVNEYqi4Y7 +-----END CERTIFICATE----- + +TWCA Global Root CA +=================== +-----BEGIN CERTIFICATE----- +MIIFQTCCAymgAwIBAgICDL4wDQYJKoZIhvcNAQELBQAwUTELMAkGA1UEBhMCVFcxEjAQBgNVBAoT +CVRBSVdBTi1DQTEQMA4GA1UECxMHUm9vdCBDQTEcMBoGA1UEAxMTVFdDQSBHbG9iYWwgUm9vdCBD +QTAeFw0xMjA2MjcwNjI4MzNaFw0zMDEyMzExNTU5NTlaMFExCzAJBgNVBAYTAlRXMRIwEAYDVQQK +EwlUQUlXQU4tQ0ExEDAOBgNVBAsTB1Jvb3QgQ0ExHDAaBgNVBAMTE1RXQ0EgR2xvYmFsIFJvb3Qg +Q0EwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCwBdvI64zEbooh745NnHEKH1Jw7W2C +nJfF10xORUnLQEK1EjRsGcJ0pDFfhQKX7EMzClPSnIyOt7h52yvVavKOZsTuKwEHktSz0ALfUPZV +r2YOy+BHYC8rMjk1Ujoog/h7FsYYuGLWRyWRzvAZEk2tY/XTP3VfKfChMBwqoJimFb3u/Rk28OKR +Q4/6ytYQJ0lM793B8YVwm8rqqFpD/G2Gb3PpN0Wp8DbHzIh1HrtsBv+baz4X7GGqcXzGHaL3SekV +tTzWoWH1EfcFbx39Eb7QMAfCKbAJTibc46KokWofwpFFiFzlmLhxpRUZyXx1EcxwdE8tmx2RRP1W +KKD+u4ZqyPpcC1jcxkt2yKsi2XMPpfRaAok/T54igu6idFMqPVMnaR1sjjIsZAAmY2E2TqNGtz99 +sy2sbZCilaLOz9qC5wc0GZbpuCGqKX6mOL6OKUohZnkfs8O1CWfe1tQHRvMq2uYiN2DLgbYPoA/p +yJV/v1WRBXrPPRXAb94JlAGD1zQbzECl8LibZ9WYkTunhHiVJqRaCPgrdLQABDzfuBSO6N+pjWxn +kjMdwLfS7JLIvgm/LCkFbwJrnu+8vyq8W8BQj0FwcYeyTbcEqYSjMq+u7msXi7Kx/mzhkIyIqJdI +zshNy/MGz19qCkKxHh53L46g5pIOBvwFItIm4TFRfTLcDwIDAQABoyMwITAOBgNVHQ8BAf8EBAMC +AQYwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAgEAXzSBdu+WHdXltdkCY4QWwa6g +cFGn90xHNcgL1yg9iXHZqjNB6hQbbCEAwGxCGX6faVsgQt+i0trEfJdLjbDorMjupWkEmQqSpqsn +LhpNgb+E1HAerUf+/UqdM+DyucRFCCEK2mlpc3INvjT+lIutwx4116KD7+U4x6WFH6vPNOw/KP4M +8VeGTslV9xzU2KV9Bnpv1d8Q34FOIWWxtuEXeZVFBs5fzNxGiWNoRI2T9GRwoD2dKAXDOXC4Ynsg +/eTb6QihuJ49CcdP+yz4k3ZB3lLg4VfSnQO8d57+nile98FRYB/e2guyLXW3Q0iT5/Z5xoRdgFlg +lPx4mI88k1HtQJAH32RjJMtOcQWh15QaiDLxInQirqWm2BJpTGCjAu4r7NRjkgtevi92a6O2JryP +A9gK8kxkRr05YuWW6zRjESjMlfGt7+/cgFhI6Uu46mWs6fyAtbXIRfmswZ/ZuepiiI7E8UuDEq3m +i4TWnsLrgxifarsbJGAzcMzs9zLzXNl5fe+epP7JI8Mk7hWSsT2RTyaGvWZzJBPqpK5jwa19hAM8 +EHiGG3njxPPyBJUgriOCxLM6AGK/5jYk4Ve6xx6QddVfP5VhK8E7zeWzaGHQRiapIVJpLesux+t3 +zqY6tQMzT3bR51xUAV3LePTJDL/PEo4XLSNolOer/qmyKwbQBM0= +-----END CERTIFICATE----- + +TeliaSonera Root CA v1 +====================== +-----BEGIN CERTIFICATE----- +MIIFODCCAyCgAwIBAgIRAJW+FqD3LkbxezmCcvqLzZYwDQYJKoZIhvcNAQEFBQAwNzEUMBIGA1UE +CgwLVGVsaWFTb25lcmExHzAdBgNVBAMMFlRlbGlhU29uZXJhIFJvb3QgQ0EgdjEwHhcNMDcxMDE4 +MTIwMDUwWhcNMzIxMDE4MTIwMDUwWjA3MRQwEgYDVQQKDAtUZWxpYVNvbmVyYTEfMB0GA1UEAwwW +VGVsaWFTb25lcmEgUm9vdCBDQSB2MTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAMK+ +6yfwIaPzaSZVfp3FVRaRXP3vIb9TgHot0pGMYzHw7CTww6XScnwQbfQ3t+XmfHnqjLWCi65ItqwA +3GV17CpNX8GH9SBlK4GoRz6JI5UwFpB/6FcHSOcZrr9FZ7E3GwYq/t75rH2D+1665I+XZ75Ljo1k +B1c4VWk0Nj0TSO9P4tNmHqTPGrdeNjPUtAa9GAH9d4RQAEX1jF3oI7x+/jXh7VB7qTCNGdMJjmhn +Xb88lxhTuylixcpecsHHltTbLaC0H2kD7OriUPEMPPCs81Mt8Bz17Ww5OXOAFshSsCPN4D7c3TxH +oLs1iuKYaIu+5b9y7tL6pe0S7fyYGKkmdtwoSxAgHNN/Fnct7W+A90m7UwW7XWjH1Mh1Fj+JWov3 +F0fUTPHSiXk+TT2YqGHeOh7S+F4D4MHJHIzTjU3TlTazN19jY5szFPAtJmtTfImMMsJu7D0hADnJ +oWjiUIMusDor8zagrC/kb2HCUQk5PotTubtn2txTuXZZNp1D5SDgPTJghSJRt8czu90VL6R4pgd7 +gUY2BIbdeTXHlSw7sKMXNeVzH7RcWe/a6hBle3rQf5+ztCo3O3CLm1u5K7fsslESl1MpWtTwEhDc +TwK7EpIvYtQ/aUN8Ddb8WHUBiJ1YFkveupD/RwGJBmr2X7KQarMCpgKIv7NHfirZ1fpoeDVNAgMB +AAGjPzA9MA8GA1UdEwEB/wQFMAMBAf8wCwYDVR0PBAQDAgEGMB0GA1UdDgQWBBTwj1k4ALP1j5qW +DNXr+nuqF+gTEjANBgkqhkiG9w0BAQUFAAOCAgEAvuRcYk4k9AwI//DTDGjkk0kiP0Qnb7tt3oNm +zqjMDfz1mgbldxSR651Be5kqhOX//CHBXfDkH1e3damhXwIm/9fH907eT/j3HEbAek9ALCI18Bmx +0GtnLLCo4MBANzX2hFxc469CeP6nyQ1Q6g2EdvZR74NTxnr/DlZJLo961gzmJ1TjTQpgcmLNkQfW +pb/ImWvtxBnmq0wROMVvMeJuScg/doAmAyYp4Db29iBT4xdwNBedY2gea+zDTYa4EzAvXUYNR0PV +G6pZDrlcjQZIrXSHX8f8MVRBE+LHIQ6e4B4N4cB7Q4WQxYpYxmUKeFfyxiMPAdkgS94P+5KFdSpc +c41teyWRyu5FrgZLAMzTsVlQ2jqIOylDRl6XK1TOU2+NSueW+r9xDkKLfP0ooNBIytrEgUy7onOT +JsjrDNYmiLbAJM+7vVvrdX3pCI6GMyx5dwlppYn8s3CQh3aP0yK7Qs69cwsgJirQmz1wHiRszYd2 +qReWt88NkvuOGKmYSdGe/mBEciG5Ge3C9THxOUiIkCR1VBatzvT4aRRkOfujuLpwQMcnHL/EVlP6 +Y2XQ8xwOFvVrhlhNGNTkDY6lnVuR3HYkUD/GKvvZt5y11ubQ2egZixVxSK236thZiNSQvxaz2ems +WWFUyBy6ysHK4bkgTI86k4mloMy/0/Z1pHWWbVY= +-----END CERTIFICATE----- + +T-TeleSec GlobalRoot Class 2 +============================ +-----BEGIN CERTIFICATE----- +MIIDwzCCAqugAwIBAgIBATANBgkqhkiG9w0BAQsFADCBgjELMAkGA1UEBhMCREUxKzApBgNVBAoM +IlQtU3lzdGVtcyBFbnRlcnByaXNlIFNlcnZpY2VzIEdtYkgxHzAdBgNVBAsMFlQtU3lzdGVtcyBU +cnVzdCBDZW50ZXIxJTAjBgNVBAMMHFQtVGVsZVNlYyBHbG9iYWxSb290IENsYXNzIDIwHhcNMDgx +MDAxMTA0MDE0WhcNMzMxMDAxMjM1OTU5WjCBgjELMAkGA1UEBhMCREUxKzApBgNVBAoMIlQtU3lz +dGVtcyBFbnRlcnByaXNlIFNlcnZpY2VzIEdtYkgxHzAdBgNVBAsMFlQtU3lzdGVtcyBUcnVzdCBD +ZW50ZXIxJTAjBgNVBAMMHFQtVGVsZVNlYyBHbG9iYWxSb290IENsYXNzIDIwggEiMA0GCSqGSIb3 +DQEBAQUAA4IBDwAwggEKAoIBAQCqX9obX+hzkeXaXPSi5kfl82hVYAUdAqSzm1nzHoqvNK38DcLZ +SBnuaY/JIPwhqgcZ7bBcrGXHX+0CfHt8LRvWurmAwhiCFoT6ZrAIxlQjgeTNuUk/9k9uN0goOA/F +vudocP05l03Sx5iRUKrERLMjfTlH6VJi1hKTXrcxlkIF+3anHqP1wvzpesVsqXFP6st4vGCvx970 +2cu+fjOlbpSD8DT6IavqjnKgP6TeMFvvhk1qlVtDRKgQFRzlAVfFmPHmBiiRqiDFt1MmUUOyCxGV +WOHAD3bZwI18gfNycJ5v/hqO2V81xrJvNHy+SE/iWjnX2J14np+GPgNeGYtEotXHAgMBAAGjQjBA +MA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBS/WSA2AHmgoCJrjNXy +YdK4LMuCSjANBgkqhkiG9w0BAQsFAAOCAQEAMQOiYQsfdOhyNsZt+U2e+iKo4YFWz827n+qrkRk4 +r6p8FU3ztqONpfSO9kSpp+ghla0+AGIWiPACuvxhI+YzmzB6azZie60EI4RYZeLbK4rnJVM3YlNf +vNoBYimipidx5joifsFvHZVwIEoHNN/q/xWA5brXethbdXwFeilHfkCoMRN3zUA7tFFHei4R40cR +3p1m0IvVVGb6g1XqfMIpiRvpb7PO4gWEyS8+eIVibslfwXhjdFjASBgMmTnrpMwatXlajRWc2BQN +9noHV8cigwUtPJslJj0Ys6lDfMjIq2SPDqO/nBudMNva0Bkuqjzx+zOAduTNrRlPBSeOE6Fuwg== +-----END CERTIFICATE----- + +Atos TrustedRoot 2011 +===================== +-----BEGIN CERTIFICATE----- +MIIDdzCCAl+gAwIBAgIIXDPLYixfszIwDQYJKoZIhvcNAQELBQAwPDEeMBwGA1UEAwwVQXRvcyBU +cnVzdGVkUm9vdCAyMDExMQ0wCwYDVQQKDARBdG9zMQswCQYDVQQGEwJERTAeFw0xMTA3MDcxNDU4 +MzBaFw0zMDEyMzEyMzU5NTlaMDwxHjAcBgNVBAMMFUF0b3MgVHJ1c3RlZFJvb3QgMjAxMTENMAsG +A1UECgwEQXRvczELMAkGA1UEBhMCREUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCV +hTuXbyo7LjvPpvMpNb7PGKw+qtn4TaA+Gke5vJrf8v7MPkfoepbCJI419KkM/IL9bcFyYie96mvr +54rMVD6QUM+A1JX76LWC1BTFtqlVJVfbsVD2sGBkWXppzwO3bw2+yj5vdHLqqjAqc2K+SZFhyBH+ +DgMq92og3AIVDV4VavzjgsG1xZ1kCWyjWZgHJ8cblithdHFsQ/H3NYkQ4J7sVaE3IqKHBAUsR320 +HLliKWYoyrfhk/WklAOZuXCFteZI6o1Q/NnezG8HDt0Lcp2AMBYHlT8oDv3FdU9T1nSatCQujgKR +z3bFmx5VdJx4IbHwLfELn8LVlhgf8FQieowHAgMBAAGjfTB7MB0GA1UdDgQWBBSnpQaxLKYJYO7R +l+lwrrw7GWzbITAPBgNVHRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFKelBrEspglg7tGX6XCuvDsZ +bNshMBgGA1UdIAQRMA8wDQYLKwYBBAGwLQMEAQEwDgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3DQEB +CwUAA4IBAQAmdzTblEiGKkGdLD4GkGDEjKwLVLgfuXvTBznk+j57sj1O7Z8jvZfza1zv7v1Apt+h +k6EKhqzvINB5Ab149xnYJDE0BAGmuhWawyfc2E8PzBhj/5kPDpFrdRbhIfzYJsdHt6bPWHJxfrrh +TZVHO8mvbaG0weyJ9rQPOLXiZNwlz6bb65pcmaHFCN795trV1lpFDMS3wrUU77QR/w4VtfX128a9 +61qn8FYiqTxlVMYVqL2Gns2Dlmh6cYGJ4Qvh6hEbaAjMaZ7snkGeRDImeuKHCnE96+RapNLbxc3G +3mB/ufNPRJLvKrcYPqcZ2Qt9sTdBQrC6YB3y/gkRsPCHe6ed +-----END CERTIFICATE----- + +QuoVadis Root CA 1 G3 +===================== +-----BEGIN CERTIFICATE----- +MIIFYDCCA0igAwIBAgIUeFhfLq0sGUvjNwc1NBMotZbUZZMwDQYJKoZIhvcNAQELBQAwSDELMAkG +A1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxHjAcBgNVBAMTFVF1b1ZhZGlzIFJv +b3QgQ0EgMSBHMzAeFw0xMjAxMTIxNzI3NDRaFw00MjAxMTIxNzI3NDRaMEgxCzAJBgNVBAYTAkJN +MRkwFwYDVQQKExBRdW9WYWRpcyBMaW1pdGVkMR4wHAYDVQQDExVRdW9WYWRpcyBSb290IENBIDEg +RzMwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCgvlAQjunybEC0BJyFuTHK3C3kEakE +PBtVwedYMB0ktMPvhd6MLOHBPd+C5k+tR4ds7FtJwUrVu4/sh6x/gpqG7D0DmVIB0jWerNrwU8lm +PNSsAgHaJNM7qAJGr6Qc4/hzWHa39g6QDbXwz8z6+cZM5cOGMAqNF34168Xfuw6cwI2H44g4hWf6 +Pser4BOcBRiYz5P1sZK0/CPTz9XEJ0ngnjybCKOLXSoh4Pw5qlPafX7PGglTvF0FBM+hSo+LdoIN +ofjSxxR3W5A2B4GbPgb6Ul5jxaYA/qXpUhtStZI5cgMJYr2wYBZupt0lwgNm3fME0UDiTouG9G/l +g6AnhF4EwfWQvTA9xO+oabw4m6SkltFi2mnAAZauy8RRNOoMqv8hjlmPSlzkYZqn0ukqeI1RPToV +7qJZjqlc3sX5kCLliEVx3ZGZbHqfPT2YfF72vhZooF6uCyP8Wg+qInYtyaEQHeTTRCOQiJ/GKubX +9ZqzWB4vMIkIG1SitZgj7Ah3HJVdYdHLiZxfokqRmu8hqkkWCKi9YSgxyXSthfbZxbGL0eUQMk1f +iyA6PEkfM4VZDdvLCXVDaXP7a3F98N/ETH3Goy7IlXnLc6KOTk0k+17kBL5yG6YnLUlamXrXXAkg +t3+UuU/xDRxeiEIbEbfnkduebPRq34wGmAOtzCjvpUfzUwIDAQABo0IwQDAPBgNVHRMBAf8EBTAD +AQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUo5fW816iEOGrRZ88F2Q87gFwnMwwDQYJKoZI +hvcNAQELBQADggIBABj6W3X8PnrHX3fHyt/PX8MSxEBd1DKquGrX1RUVRpgjpeaQWxiZTOOtQqOC +MTaIzen7xASWSIsBx40Bz1szBpZGZnQdT+3Btrm0DWHMY37XLneMlhwqI2hrhVd2cDMT/uFPpiN3 +GPoajOi9ZcnPP/TJF9zrx7zABC4tRi9pZsMbj/7sPtPKlL92CiUNqXsCHKnQO18LwIE6PWThv6ct +Tr1NxNgpxiIY0MWscgKCP6o6ojoilzHdCGPDdRS5YCgtW2jgFqlmgiNR9etT2DGbe+m3nUvriBbP ++V04ikkwj+3x6xn0dxoxGE1nVGwvb2X52z3sIexe9PSLymBlVNFxZPT5pqOBMzYzcfCkeF9OrYMh +3jRJjehZrJ3ydlo28hP0r+AJx2EqbPfgna67hkooby7utHnNkDPDs3b69fBsnQGQ+p6Q9pxyz0fa +wx/kNSBT8lTR32GDpgLiJTjehTItXnOQUl1CxM49S+H5GYQd1aJQzEH7QRTDvdbJWqNjZgKAvQU6 +O0ec7AAmTPWIUb+oI38YB7AL7YsmoWTTYUrrXJ/es69nA7Mf3W1daWhpq1467HxpvMc7hU6eFbm0 +FU/DlXpY18ls6Wy58yljXrQs8C097Vpl4KlbQMJImYFtnh8GKjwStIsPm6Ik8KaN1nrgS7ZklmOV +hMJKzRwuJIczYOXD +-----END CERTIFICATE----- + +QuoVadis Root CA 2 G3 +===================== +-----BEGIN CERTIFICATE----- +MIIFYDCCA0igAwIBAgIURFc0JFuBiZs18s64KztbpybwdSgwDQYJKoZIhvcNAQELBQAwSDELMAkG +A1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxHjAcBgNVBAMTFVF1b1ZhZGlzIFJv +b3QgQ0EgMiBHMzAeFw0xMjAxMTIxODU5MzJaFw00MjAxMTIxODU5MzJaMEgxCzAJBgNVBAYTAkJN +MRkwFwYDVQQKExBRdW9WYWRpcyBMaW1pdGVkMR4wHAYDVQQDExVRdW9WYWRpcyBSb290IENBIDIg +RzMwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQChriWyARjcV4g/Ruv5r+LrI3HimtFh +ZiFfqq8nUeVuGxbULX1QsFN3vXg6YOJkApt8hpvWGo6t/x8Vf9WVHhLL5hSEBMHfNrMWn4rjyduY +NM7YMxcoRvynyfDStNVNCXJJ+fKH46nafaF9a7I6JaltUkSs+L5u+9ymc5GQYaYDFCDy54ejiK2t +oIz/pgslUiXnFgHVy7g1gQyjO/Dh4fxaXc6AcW34Sas+O7q414AB+6XrW7PFXmAqMaCvN+ggOp+o +MiwMzAkd056OXbxMmO7FGmh77FOm6RQ1o9/NgJ8MSPsc9PG/Srj61YxxSscfrf5BmrODXfKEVu+l +V0POKa2Mq1W/xPtbAd0jIaFYAI7D0GoT7RPjEiuA3GfmlbLNHiJuKvhB1PLKFAeNilUSxmn1uIZo +L1NesNKqIcGY5jDjZ1XHm26sGahVpkUG0CM62+tlXSoREfA7T8pt9DTEceT/AFr2XK4jYIVz8eQQ +sSWu1ZK7E8EM4DnatDlXtas1qnIhO4M15zHfeiFuuDIIfR0ykRVKYnLP43ehvNURG3YBZwjgQQvD +6xVu+KQZ2aKrr+InUlYrAoosFCT5v0ICvybIxo/gbjh9Uy3l7ZizlWNof/k19N+IxWA1ksB8aRxh +lRbQ694Lrz4EEEVlWFA4r0jyWbYW8jwNkALGcC4BrTwV1wIDAQABo0IwQDAPBgNVHRMBAf8EBTAD +AQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQU7edvdlq/YOxJW8ald7tyFnGbxD0wDQYJKoZI +hvcNAQELBQADggIBAJHfgD9DCX5xwvfrs4iP4VGyvD11+ShdyLyZm3tdquXK4Qr36LLTn91nMX66 +AarHakE7kNQIXLJgapDwyM4DYvmL7ftuKtwGTTwpD4kWilhMSA/ohGHqPHKmd+RCroijQ1h5fq7K +pVMNqT1wvSAZYaRsOPxDMuHBR//47PERIjKWnML2W2mWeyAMQ0GaW/ZZGYjeVYg3UQt4XAoeo0L9 +x52ID8DyeAIkVJOviYeIyUqAHerQbj5hLja7NQ4nlv1mNDthcnPxFlxHBlRJAHpYErAK74X9sbgz +dWqTHBLmYF5vHX/JHyPLhGGfHoJE+V+tYlUkmlKY7VHnoX6XOuYvHxHaU4AshZ6rNRDbIl9qxV6X +U/IyAgkwo1jwDQHVcsaxfGl7w/U2Rcxhbl5MlMVerugOXou/983g7aEOGzPuVBj+D77vfoRrQ+Nw +mNtddbINWQeFFSM51vHfqSYP1kjHs6Yi9TM3WpVHn3u6GBVv/9YUZINJ0gpnIdsPNWNgKCLjsZWD +zYWm3S8P52dSbrsvhXz1SnPnxT7AvSESBT/8twNJAlvIJebiVDj1eYeMHVOyToV7BjjHLPj4sHKN +JeV3UvQDHEimUF+IIDBu8oJDqz2XhOdT+yHBTw8imoa4WSr2Rz0ZiC3oheGe7IUIarFsNMkd7Egr +O3jtZsSOeWmD3n+M +-----END CERTIFICATE----- + +QuoVadis Root CA 3 G3 +===================== +-----BEGIN CERTIFICATE----- +MIIFYDCCA0igAwIBAgIULvWbAiin23r/1aOp7r0DoM8Sah0wDQYJKoZIhvcNAQELBQAwSDELMAkG +A1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxHjAcBgNVBAMTFVF1b1ZhZGlzIFJv +b3QgQ0EgMyBHMzAeFw0xMjAxMTIyMDI2MzJaFw00MjAxMTIyMDI2MzJaMEgxCzAJBgNVBAYTAkJN +MRkwFwYDVQQKExBRdW9WYWRpcyBMaW1pdGVkMR4wHAYDVQQDExVRdW9WYWRpcyBSb290IENBIDMg +RzMwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCzyw4QZ47qFJenMioKVjZ/aEzHs286 +IxSR/xl/pcqs7rN2nXrpixurazHb+gtTTK/FpRp5PIpM/6zfJd5O2YIyC0TeytuMrKNuFoM7pmRL +Mon7FhY4futD4tN0SsJiCnMK3UmzV9KwCoWdcTzeo8vAMvMBOSBDGzXRU7Ox7sWTaYI+FrUoRqHe +6okJ7UO4BUaKhvVZR74bbwEhELn9qdIoyhA5CcoTNs+cra1AdHkrAj80//ogaX3T7mH1urPnMNA3 +I4ZyYUUpSFlob3emLoG+B01vr87ERRORFHAGjx+f+IdpsQ7vw4kZ6+ocYfx6bIrc1gMLnia6Et3U +VDmrJqMz6nWB2i3ND0/kA9HvFZcba5DFApCTZgIhsUfei5pKgLlVj7WiL8DWM2fafsSntARE60f7 +5li59wzweyuxwHApw0BiLTtIadwjPEjrewl5qW3aqDCYz4ByA4imW0aucnl8CAMhZa634RylsSqi +Md5mBPfAdOhx3v89WcyWJhKLhZVXGqtrdQtEPREoPHtht+KPZ0/l7DxMYIBpVzgeAVuNVejH38DM +dyM0SXV89pgR6y3e7UEuFAUCf+D+IOs15xGsIs5XPd7JMG0QA4XN8f+MFrXBsj6IbGB/kE+V9/Yt +rQE5BwT6dYB9v0lQ7e/JxHwc64B+27bQ3RP+ydOc17KXqQIDAQABo0IwQDAPBgNVHRMBAf8EBTAD +AQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUxhfQvKjqAkPyGwaZXSuQILnXnOQwDQYJKoZI +hvcNAQELBQADggIBADRh2Va1EodVTd2jNTFGu6QHcrxfYWLopfsLN7E8trP6KZ1/AvWkyaiTt3px +KGmPc+FSkNrVvjrlt3ZqVoAh313m6Tqe5T72omnHKgqwGEfcIHB9UqM+WXzBusnIFUBhynLWcKzS +t/Ac5IYp8M7vaGPQtSCKFWGafoaYtMnCdvvMujAWzKNhxnQT5WvvoxXqA/4Ti2Tk08HS6IT7SdEQ +TXlm66r99I0xHnAUrdzeZxNMgRVhvLfZkXdxGYFgu/BYpbWcC/ePIlUnwEsBbTuZDdQdm2NnL9Du +DcpmvJRPpq3t/O5jrFc/ZSXPsoaP0Aj/uHYUbt7lJ+yreLVTubY/6CD50qi+YUbKh4yE8/nxoGib +Ih6BJpsQBJFxwAYf3KDTuVan45gtf4Od34wrnDKOMpTwATwiKp9Dwi7DmDkHOHv8XgBCH/MyJnmD +hPbl8MFREsALHgQjDFSlTC9JxUrRtm5gDWv8a4uFJGS3iQ6rJUdbPM9+Sb3H6QrG2vd+DhcI00iX +0HGS8A85PjRqHH3Y8iKuu2n0M7SmSFXRDw4m6Oy2Cy2nhTXN/VnIn9HNPlopNLk9hM6xZdRZkZFW +dSHBd575euFgndOtBBj0fOtek49TSiIp+EgrPk2GrFt/ywaZWWDYWGWVjUTR939+J399roD1B0y2 +PpxxVJkES/1Y+Zj0 +-----END CERTIFICATE----- + +DigiCert Assured ID Root G2 +=========================== +-----BEGIN CERTIFICATE----- +MIIDljCCAn6gAwIBAgIQC5McOtY5Z+pnI7/Dr5r0SzANBgkqhkiG9w0BAQsFADBlMQswCQYDVQQG +EwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMSQw +IgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgRzIwHhcNMTMwODAxMTIwMDAwWhcNMzgw +MTE1MTIwMDAwWjBlMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQL +ExB3d3cuZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgRzIw +ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDZ5ygvUj82ckmIkzTz+GoeMVSAn61UQbVH +35ao1K+ALbkKz3X9iaV9JPrjIgwrvJUXCzO/GU1BBpAAvQxNEP4HteccbiJVMWWXvdMX0h5i89vq +bFCMP4QMls+3ywPgym2hFEwbid3tALBSfK+RbLE4E9HpEgjAALAcKxHad3A2m67OeYfcgnDmCXRw +VWmvo2ifv922ebPynXApVfSr/5Vh88lAbx3RvpO704gqu52/clpWcTs/1PPRCv4o76Pu2ZmvA9OP +YLfykqGxvYmJHzDNw6YuYjOuFgJ3RFrngQo8p0Quebg/BLxcoIfhG69Rjs3sLPr4/m3wOnyqi+Rn +lTGNAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMB0GA1UdDgQWBBTO +w0q5mVXyuNtgv6l+vVa1lzan1jANBgkqhkiG9w0BAQsFAAOCAQEAyqVVjOPIQW5pJ6d1Ee88hjZv +0p3GeDgdaZaikmkuOGybfQTUiaWxMTeKySHMq2zNixya1r9I0jJmwYrA8y8678Dj1JGG0VDjA9tz +d29KOVPt3ibHtX2vK0LRdWLjSisCx1BL4GnilmwORGYQRI+tBev4eaymG+g3NJ1TyWGqolKvSnAW +hsI6yLETcDbYz+70CjTVW0z9B5yiutkBclzzTcHdDrEcDcRjvq30FPuJ7KJBDkzMyFdA0G4Dqs0M +jomZmWzwPDCvON9vvKO+KSAnq3T/EyJ43pdSVR6DtVQgA+6uwE9W3jfMw3+qBCe703e4YtsXfJwo +IhNzbM8m9Yop5w== +-----END CERTIFICATE----- + +DigiCert Assured ID Root G3 +=========================== +-----BEGIN CERTIFICATE----- +MIICRjCCAc2gAwIBAgIQC6Fa+h3foLVJRK/NJKBs7DAKBggqhkjOPQQDAzBlMQswCQYDVQQGEwJV +UzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMSQwIgYD +VQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgRzMwHhcNMTMwODAxMTIwMDAwWhcNMzgwMTE1 +MTIwMDAwWjBlMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 +d3cuZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgRzMwdjAQ +BgcqhkjOPQIBBgUrgQQAIgNiAAQZ57ysRGXtzbg/WPuNsVepRC0FFfLvC/8QdJ+1YlJfZn4f5dwb +RXkLzMZTCp2NXQLZqVneAlr2lSoOjThKiknGvMYDOAdfVdp+CW7if17QRSAPWXYQ1qAk8C3eNvJs +KTmjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMB0GA1UdDgQWBBTL0L2p4ZgF +UaFNN6KDec6NHSrkhDAKBggqhkjOPQQDAwNnADBkAjAlpIFFAmsSS3V0T8gj43DydXLefInwz5Fy +YZ5eEJJZVrmDxxDnOOlYJjZ91eQ0hjkCMHw2U/Aw5WJjOpnitqM7mzT6HtoQknFekROn3aRukswy +1vUhZscv6pZjamVFkpUBtA== +-----END CERTIFICATE----- + +DigiCert Global Root G2 +======================= +-----BEGIN CERTIFICATE----- +MIIDjjCCAnagAwIBAgIQAzrx5qcRqaC7KGSxHQn65TANBgkqhkiG9w0BAQsFADBhMQswCQYDVQQG +EwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMSAw +HgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBHMjAeFw0xMzA4MDExMjAwMDBaFw0zODAxMTUx +MjAwMDBaMGExCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3 +dy5kaWdpY2VydC5jb20xIDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IEcyMIIBIjANBgkq +hkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuzfNNNx7a8myaJCtSnX/RrohCgiN9RlUyfuI2/Ou8jqJ +kTx65qsGGmvPrC3oXgkkRLpimn7Wo6h+4FR1IAWsULecYxpsMNzaHxmx1x7e/dfgy5SDN67sH0NO +3Xss0r0upS/kqbitOtSZpLYl6ZtrAGCSYP9PIUkY92eQq2EGnI/yuum06ZIya7XzV+hdG82MHauV +BJVJ8zUtluNJbd134/tJS7SsVQepj5WztCO7TG1F8PapspUwtP1MVYwnSlcUfIKdzXOS0xZKBgyM +UNGPHgm+F6HmIcr9g+UQvIOlCsRnKPZzFBQ9RnbDhxSJITRNrw9FDKZJobq7nMWxM4MphQIDAQAB +o0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBhjAdBgNVHQ4EFgQUTiJUIBiV5uNu +5g/6+rkS7QYXjzkwDQYJKoZIhvcNAQELBQADggEBAGBnKJRvDkhj6zHd6mcY1Yl9PMWLSn/pvtsr +F9+wX3N3KjITOYFnQoQj8kVnNeyIv/iPsGEMNKSuIEyExtv4NeF22d+mQrvHRAiGfzZ0JFrabA0U +WTW98kndth/Jsw1HKj2ZL7tcu7XUIOGZX1NGFdtom/DzMNU+MeKNhJ7jitralj41E6Vf8PlwUHBH +QRFXGU7Aj64GxJUTFy8bJZ918rGOmaFvE7FBcf6IKshPECBV1/MUReXgRPTqh5Uykw7+U0b6LJ3/ +iyK5S9kJRaTepLiaWN0bfVKfjllDiIGknibVb63dDcY3fe0Dkhvld1927jyNxF1WW6LZZm6zNTfl +MrY= +-----END CERTIFICATE----- + +DigiCert Global Root G3 +======================= +-----BEGIN CERTIFICATE----- +MIICPzCCAcWgAwIBAgIQBVVWvPJepDU1w6QP1atFcjAKBggqhkjOPQQDAzBhMQswCQYDVQQGEwJV +UzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMSAwHgYD +VQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBHMzAeFw0xMzA4MDExMjAwMDBaFw0zODAxMTUxMjAw +MDBaMGExCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5k +aWdpY2VydC5jb20xIDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IEczMHYwEAYHKoZIzj0C +AQYFK4EEACIDYgAE3afZu4q4C/sLfyHS8L6+c/MzXRq8NOrexpu80JX28MzQC7phW1FGfp4tn+6O +YwwX7Adw9c+ELkCDnOg/QW07rdOkFFk2eJ0DQ+4QE2xy3q6Ip6FrtUPOZ9wj/wMco+I+o0IwQDAP +BgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBhjAdBgNVHQ4EFgQUs9tIpPmhxdiuNkHMEWNp +Yim8S8YwCgYIKoZIzj0EAwMDaAAwZQIxAK288mw/EkrRLTnDCgmXc/SINoyIJ7vmiI1Qhadj+Z4y +3maTD/HMsQmP3Wyr+mt/oAIwOWZbwmSNuJ5Q3KjVSaLtx9zRSX8XAbjIho9OjIgrqJqpisXRAL34 +VOKa5Vt8sycX +-----END CERTIFICATE----- + +DigiCert Trusted Root G4 +======================== +-----BEGIN CERTIFICATE----- +MIIFkDCCA3igAwIBAgIQBZsbV56OITLiOQe9p3d1XDANBgkqhkiG9w0BAQwFADBiMQswCQYDVQQG +EwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMSEw +HwYDVQQDExhEaWdpQ2VydCBUcnVzdGVkIFJvb3QgRzQwHhcNMTMwODAxMTIwMDAwWhcNMzgwMTE1 +MTIwMDAwWjBiMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 +d3cuZGlnaWNlcnQuY29tMSEwHwYDVQQDExhEaWdpQ2VydCBUcnVzdGVkIFJvb3QgRzQwggIiMA0G +CSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC/5pBzaN675F1KPDAiMGkz7MKnJS7JIT3yithZwuEp +pz1Yq3aaza57G4QNxDAf8xukOBbrVsaXbR2rsnnyyhHS5F/WBTxSD1Ifxp4VpX6+n6lXFllVcq9o +k3DCsrp1mWpzMpTREEQQLt+C8weE5nQ7bXHiLQwb7iDVySAdYyktzuxeTsiT+CFhmzTrBcZe7Fsa +vOvJz82sNEBfsXpm7nfISKhmV1efVFiODCu3T6cw2Vbuyntd463JT17lNecxy9qTXtyOj4DatpGY +QJB5w3jHtrHEtWoYOAMQjdjUN6QuBX2I9YI+EJFwq1WCQTLX2wRzKm6RAXwhTNS8rhsDdV14Ztk6 +MUSaM0C/CNdaSaTC5qmgZ92kJ7yhTzm1EVgX9yRcRo9k98FpiHaYdj1ZXUJ2h4mXaXpI8OCiEhtm +mnTK3kse5w5jrubU75KSOp493ADkRSWJtppEGSt+wJS00mFt6zPZxd9LBADMfRyVw4/3IbKyEbe7 +f/LVjHAsQWCqsWMYRJUadmJ+9oCw++hkpjPRiQfhvbfmQ6QYuKZ3AeEPlAwhHbJUKSWJbOUOUlFH +dL4mrLZBdd56rF+NP8m800ERElvlEFDrMcXKchYiCd98THU/Y+whX8QgUWtvsauGi0/C1kVfnSD8 +oR7FwI+isX4KJpn15GkvmB0t9dmpsh3lGwIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1Ud +DwEB/wQEAwIBhjAdBgNVHQ4EFgQU7NfjgtJxXWRM3y5nP+e6mK4cD08wDQYJKoZIhvcNAQEMBQAD +ggIBALth2X2pbL4XxJEbw6GiAI3jZGgPVs93rnD5/ZpKmbnJeFwMDF/k5hQpVgs2SV1EY+CtnJYY +ZhsjDT156W1r1lT40jzBQ0CuHVD1UvyQO7uYmWlrx8GnqGikJ9yd+SeuMIW59mdNOj6PWTkiU0Tr +yF0Dyu1Qen1iIQqAyHNm0aAFYF/opbSnr6j3bTWcfFqK1qI4mfN4i/RN0iAL3gTujJtHgXINwBQy +7zBZLq7gcfJW5GqXb5JQbZaNaHqasjYUegbyJLkJEVDXCLG4iXqEI2FCKeWjzaIgQdfRnGTZ6iah +ixTXTBmyUEFxPT9NcCOGDErcgdLMMpSEDQgJlxxPwO5rIHQw0uA5NBCFIRUBCOhVMt5xSdkoF1BN +5r5N0XWs0Mr7QbhDparTwwVETyw2m+L64kW4I1NsBm9nVX9GtUw/bihaeSbSpKhil9Ie4u1Ki7wb +/UdKDd9nZn6yW0HQO+T0O/QEY+nvwlQAUaCKKsnOeMzV6ocEGLPOr0mIr/OSmbaz5mEP0oUA51Aa +5BuVnRmhuZyxm7EAHu/QD09CbMkKvO5D+jpxpchNJqU1/YldvIViHTLSoCtU7ZpXwdv6EM8Zt4tK +G48BtieVU+i2iW1bvGjUI+iLUaJW+fCmgKDWHrO8Dw9TdSmq6hN35N6MgSGtBxBHEa2HPQfRdbzP +82Z+ +-----END CERTIFICATE----- + +COMODO RSA Certification Authority +================================== +-----BEGIN CERTIFICATE----- +MIIF2DCCA8CgAwIBAgIQTKr5yttjb+Af907YWwOGnTANBgkqhkiG9w0BAQwFADCBhTELMAkGA1UE +BhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEaMBgG +A1UEChMRQ09NT0RPIENBIExpbWl0ZWQxKzApBgNVBAMTIkNPTU9ETyBSU0EgQ2VydGlmaWNhdGlv +biBBdXRob3JpdHkwHhcNMTAwMTE5MDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCBhTELMAkGA1UEBhMC +R0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEaMBgGA1UE +ChMRQ09NT0RPIENBIExpbWl0ZWQxKzApBgNVBAMTIkNPTU9ETyBSU0EgQ2VydGlmaWNhdGlvbiBB +dXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCR6FSS0gpWsawNJN3Fz0Rn +dJkrN6N9I3AAcbxT38T6KhKPS38QVr2fcHK3YX/JSw8Xpz3jsARh7v8Rl8f0hj4K+j5c+ZPmNHrZ +FGvnnLOFoIJ6dq9xkNfs/Q36nGz637CC9BR++b7Epi9Pf5l/tfxnQ3K9DADWietrLNPtj5gcFKt+ +5eNu/Nio5JIk2kNrYrhV/erBvGy2i/MOjZrkm2xpmfh4SDBF1a3hDTxFYPwyllEnvGfDyi62a+pG +x8cgoLEfZd5ICLqkTqnyg0Y3hOvozIFIQ2dOciqbXL1MGyiKXCJ7tKuY2e7gUYPDCUZObT6Z+pUX +2nwzV0E8jVHtC7ZcryxjGt9XyD+86V3Em69FmeKjWiS0uqlWPc9vqv9JWL7wqP/0uK3pN/u6uPQL +OvnoQ0IeidiEyxPx2bvhiWC4jChWrBQdnArncevPDt09qZahSL0896+1DSJMwBGB7FY79tOi4lu3 +sgQiUpWAk2nojkxl8ZEDLXB0AuqLZxUpaVICu9ffUGpVRr+goyhhf3DQw6KqLCGqR84onAZFdr+C +GCe01a60y1Dma/RMhnEw6abfFobg2P9A3fvQQoh/ozM6LlweQRGBY84YcWsr7KaKtzFcOmpH4MN5 +WdYgGq/yapiqcrxXStJLnbsQ/LBMQeXtHT1eKJ2czL+zUdqnR+WEUwIDAQABo0IwQDAdBgNVHQ4E +FgQUu69+Aj36pvE8hI6t7jiY7NkyMtQwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8w +DQYJKoZIhvcNAQEMBQADggIBAArx1UaEt65Ru2yyTUEUAJNMnMvlwFTPoCWOAvn9sKIN9SCYPBMt +rFaisNZ+EZLpLrqeLppysb0ZRGxhNaKatBYSaVqM4dc+pBroLwP0rmEdEBsqpIt6xf4FpuHA1sj+ +nq6PK7o9mfjYcwlYRm6mnPTXJ9OV2jeDchzTc+CiR5kDOF3VSXkAKRzH7JsgHAckaVd4sjn8OoSg +tZx8jb8uk2IntznaFxiuvTwJaP+EmzzV1gsD41eeFPfR60/IvYcjt7ZJQ3mFXLrrkguhxuhoqEwW +sRqZCuhTLJK7oQkYdQxlqHvLI7cawiiFwxv/0Cti76R7CZGYZ4wUAc1oBmpjIXUDgIiKboHGhfKp +pC3n9KUkEEeDys30jXlYsQab5xoq2Z0B15R97QNKyvDb6KkBPvVWmckejkk9u+UJueBPSZI9FoJA +zMxZxuY67RIuaTxslbH9qh17f4a+Hg4yRvv7E491f0yLS0Zj/gA0QHDBw7mh3aZw4gSzQbzpgJHq +ZJx64SIDqZxubw5lT2yHh17zbqD5daWbQOhTsiedSrnAdyGN/4fy3ryM7xfft0kL0fJuMAsaDk52 +7RH89elWsn2/x20Kk4yl0MC2Hb46TpSi125sC8KKfPog88Tk5c0NqMuRkrF8hey1FGlmDoLnzc7I +LaZRfyHBNVOFBkpdn627G190 +-----END CERTIFICATE----- + +USERTrust RSA Certification Authority +===================================== +-----BEGIN CERTIFICATE----- +MIIF3jCCA8agAwIBAgIQAf1tMPyjylGoG7xkDjUDLTANBgkqhkiG9w0BAQwFADCBiDELMAkGA1UE +BhMCVVMxEzARBgNVBAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0plcnNleSBDaXR5MR4wHAYDVQQK +ExVUaGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNVBAMTJVVTRVJUcnVzdCBSU0EgQ2VydGlmaWNh +dGlvbiBBdXRob3JpdHkwHhcNMTAwMjAxMDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCBiDELMAkGA1UE +BhMCVVMxEzARBgNVBAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0plcnNleSBDaXR5MR4wHAYDVQQK +ExVUaGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNVBAMTJVVTRVJUcnVzdCBSU0EgQ2VydGlmaWNh +dGlvbiBBdXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCAEmUXNg7D2wiz +0KxXDXbtzSfTTK1Qg2HiqiBNCS1kCdzOiZ/MPans9s/B3PHTsdZ7NygRK0faOca8Ohm0X6a9fZ2j +Y0K2dvKpOyuR+OJv0OwWIJAJPuLodMkYtJHUYmTbf6MG8YgYapAiPLz+E/CHFHv25B+O1ORRxhFn +RghRy4YUVD+8M/5+bJz/Fp0YvVGONaanZshyZ9shZrHUm3gDwFA66Mzw3LyeTP6vBZY1H1dat//O ++T23LLb2VN3I5xI6Ta5MirdcmrS3ID3KfyI0rn47aGYBROcBTkZTmzNg95S+UzeQc0PzMsNT79uq +/nROacdrjGCT3sTHDN/hMq7MkztReJVni+49Vv4M0GkPGw/zJSZrM233bkf6c0Plfg6lZrEpfDKE +Y1WJxA3Bk1QwGROs0303p+tdOmw1XNtB1xLaqUkL39iAigmTYo61Zs8liM2EuLE/pDkP2QKe6xJM +lXzzawWpXhaDzLhn4ugTncxbgtNMs+1b/97lc6wjOy0AvzVVdAlJ2ElYGn+SNuZRkg7zJn0cTRe8 +yexDJtC/QV9AqURE9JnnV4eeUB9XVKg+/XRjL7FQZQnmWEIuQxpMtPAlR1n6BB6T1CZGSlCBst6+ +eLf8ZxXhyVeEHg9j1uliutZfVS7qXMYoCAQlObgOK6nyTJccBz8NUvXt7y+CDwIDAQABo0IwQDAd +BgNVHQ4EFgQUU3m/WqorSs9UgOHYm8Cd8rIDZsswDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQF +MAMBAf8wDQYJKoZIhvcNAQEMBQADggIBAFzUfA3P9wF9QZllDHPFUp/L+M+ZBn8b2kMVn54CVVeW +FPFSPCeHlCjtHzoBN6J2/FNQwISbxmtOuowhT6KOVWKR82kV2LyI48SqC/3vqOlLVSoGIG1VeCkZ +7l8wXEskEVX/JJpuXior7gtNn3/3ATiUFJVDBwn7YKnuHKsSjKCaXqeYalltiz8I+8jRRa8YFWSQ +Eg9zKC7F4iRO/Fjs8PRF/iKz6y+O0tlFYQXBl2+odnKPi4w2r78NBc5xjeambx9spnFixdjQg3IM +8WcRiQycE0xyNN+81XHfqnHd4blsjDwSXWXavVcStkNr/+XeTWYRUc+ZruwXtuhxkYzeSf7dNXGi +FSeUHM9h4ya7b6NnJSFd5t0dCy5oGzuCr+yDZ4XUmFF0sbmZgIn/f3gZXHlKYC6SQK5MNyosycdi +yA5d9zZbyuAlJQG03RoHnHcAP9Dc1ew91Pq7P8yF1m9/qS3fuQL39ZeatTXaw2ewh0qpKJ4jjv9c +J2vhsE/zB+4ALtRZh8tSQZXq9EfX7mRBVXyNWQKV3WKdwrnuWih0hKWbt5DHDAff9Yk2dDLWKMGw +sAvgnEzDHNb842m1R0aBL6KCq9NjRHDEjf8tM7qtj3u1cIiuPhnPQCjY/MiQu12ZIvVS5ljFH4gx +Q+6IHdfGjjxDah2nGN59PRbxYvnKkKj9 +-----END CERTIFICATE----- + +USERTrust ECC Certification Authority +===================================== +-----BEGIN CERTIFICATE----- +MIICjzCCAhWgAwIBAgIQXIuZxVqUxdJxVt7NiYDMJjAKBggqhkjOPQQDAzCBiDELMAkGA1UEBhMC +VVMxEzARBgNVBAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0plcnNleSBDaXR5MR4wHAYDVQQKExVU +aGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNVBAMTJVVTRVJUcnVzdCBFQ0MgQ2VydGlmaWNhdGlv +biBBdXRob3JpdHkwHhcNMTAwMjAxMDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCBiDELMAkGA1UEBhMC +VVMxEzARBgNVBAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0plcnNleSBDaXR5MR4wHAYDVQQKExVU +aGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNVBAMTJVVTRVJUcnVzdCBFQ0MgQ2VydGlmaWNhdGlv +biBBdXRob3JpdHkwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQarFRaqfloI+d61SRvU8Za2EurxtW2 +0eZzca7dnNYMYf3boIkDuAUU7FfO7l0/4iGzzvfUinngo4N+LZfQYcTxmdwlkWOrfzCjtHDix6Ez +nPO/LlxTsV+zfTJ/ijTjeXmjQjBAMB0GA1UdDgQWBBQ64QmG1M8ZwpZ2dEl23OA1xmNjmjAOBgNV +HQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAKBggqhkjOPQQDAwNoADBlAjA2Z6EWCNzklwBB +HU6+4WMBzzuqQhFkoJ2UOQIReVx7Hfpkue4WQrO/isIJxOzksU0CMQDpKmFHjFJKS04YcPbWRNZu +9YO6bVi9JNlWSOrvxKJGgYhqOkbRqZtNyWHa0V1Xahg= +-----END CERTIFICATE----- + +GlobalSign ECC Root CA - R5 +=========================== +-----BEGIN CERTIFICATE----- +MIICHjCCAaSgAwIBAgIRYFlJ4CYuu1X5CneKcflK2GwwCgYIKoZIzj0EAwMwUDEkMCIGA1UECxMb +R2xvYmFsU2lnbiBFQ0MgUm9vdCBDQSAtIFI1MRMwEQYDVQQKEwpHbG9iYWxTaWduMRMwEQYDVQQD +EwpHbG9iYWxTaWduMB4XDTEyMTExMzAwMDAwMFoXDTM4MDExOTAzMTQwN1owUDEkMCIGA1UECxMb +R2xvYmFsU2lnbiBFQ0MgUm9vdCBDQSAtIFI1MRMwEQYDVQQKEwpHbG9iYWxTaWduMRMwEQYDVQQD +EwpHbG9iYWxTaWduMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAER0UOlvt9Xb/pOdEh+J8LttV7HpI6 +SFkc8GIxLcB6KP4ap1yztsyX50XUWPrRd21DosCHZTQKH3rd6zwzocWdTaRvQZU4f8kehOvRnkmS +h5SHDDqFSmafnVmTTZdhBoZKo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAd +BgNVHQ4EFgQUPeYpSJvqB8ohREom3m7e0oPQn1kwCgYIKoZIzj0EAwMDaAAwZQIxAOVpEslu28Yx +uglB4Zf4+/2a4n0Sye18ZNPLBSWLVtmg515dTguDnFt2KaAJJiFqYgIwcdK1j1zqO+F4CYWodZI7 +yFz9SO8NdCKoCOJuxUnOxwy8p2Fp8fc74SrL+SvzZpA3 +-----END CERTIFICATE----- + +IdenTrust Commercial Root CA 1 +============================== +-----BEGIN CERTIFICATE----- +MIIFYDCCA0igAwIBAgIQCgFCgAAAAUUjyES1AAAAAjANBgkqhkiG9w0BAQsFADBKMQswCQYDVQQG +EwJVUzESMBAGA1UEChMJSWRlblRydXN0MScwJQYDVQQDEx5JZGVuVHJ1c3QgQ29tbWVyY2lhbCBS +b290IENBIDEwHhcNMTQwMTE2MTgxMjIzWhcNMzQwMTE2MTgxMjIzWjBKMQswCQYDVQQGEwJVUzES +MBAGA1UEChMJSWRlblRydXN0MScwJQYDVQQDEx5JZGVuVHJ1c3QgQ29tbWVyY2lhbCBSb290IENB +IDEwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCnUBneP5k91DNG8W9RYYKyqU+PZ4ld +hNlT3Qwo2dfw/66VQ3KZ+bVdfIrBQuExUHTRgQ18zZshq0PirK1ehm7zCYofWjK9ouuU+ehcCuz/ +mNKvcbO0U59Oh++SvL3sTzIwiEsXXlfEU8L2ApeN2WIrvyQfYo3fw7gpS0l4PJNgiCL8mdo2yMKi +1CxUAGc1bnO/AljwpN3lsKImesrgNqUZFvX9t++uP0D1bVoE/c40yiTcdCMbXTMTEl3EASX2MN0C +XZ/g1Ue9tOsbobtJSdifWwLziuQkkORiT0/Br4sOdBeo0XKIanoBScy0RnnGF7HamB4HWfp1IYVl +3ZBWzvurpWCdxJ35UrCLvYf5jysjCiN2O/cz4ckA82n5S6LgTrx+kzmEB/dEcH7+B1rlsazRGMzy +NeVJSQjKVsk9+w8YfYs7wRPCTY/JTw436R+hDmrfYi7LNQZReSzIJTj0+kuniVyc0uMNOYZKdHzV +WYfCP04MXFL0PfdSgvHqo6z9STQaKPNBiDoT7uje/5kdX7rL6B7yuVBgwDHTc+XvvqDtMwt0viAg +xGds8AgDelWAf0ZOlqf0Hj7h9tgJ4TNkK2PXMl6f+cB7D3hvl7yTmvmcEpB4eoCHFddydJxVdHix +uuFucAS6T6C6aMN7/zHwcz09lCqxC0EOoP5NiGVreTO01wIDAQABo0IwQDAOBgNVHQ8BAf8EBAMC +AQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQU7UQZwNPwBovupHu+QucmVMiONnYwDQYJKoZI +hvcNAQELBQADggIBAA2ukDL2pkt8RHYZYR4nKM1eVO8lvOMIkPkp165oCOGUAFjvLi5+U1KMtlwH +6oi6mYtQlNeCgN9hCQCTrQ0U5s7B8jeUeLBfnLOic7iPBZM4zY0+sLj7wM+x8uwtLRvM7Kqas6pg +ghstO8OEPVeKlh6cdbjTMM1gCIOQ045U8U1mwF10A0Cj7oV+wh93nAbowacYXVKV7cndJZ5t+qnt +ozo00Fl72u1Q8zW/7esUTTHHYPTa8Yec4kjixsU3+wYQ+nVZZjFHKdp2mhzpgq7vmrlR94gjmmmV +YjzlVYA211QC//G5Xc7UI2/YRYRKW2XviQzdFKcgyxilJbQN+QHwotL0AMh0jqEqSI5l2xPE4iUX +feu+h1sXIFRRk0pTAwvsXcoz7WL9RccvW9xYoIA55vrX/hMUpu09lEpCdNTDd1lzzY9GvlU47/ro +kTLql1gEIt44w8y8bckzOmoKaT+gyOpyj4xjhiO9bTyWnpXgSUyqorkqG5w2gXjtw+hG4iZZRHUe +2XWJUc0QhJ1hYMtd+ZciTY6Y5uN/9lu7rs3KSoFrXgvzUeF0K+l+J6fZmUlO+KWA2yUPHGNiiskz +Z2s8EIPGrd6ozRaOjfAHN3Gf8qv8QfXBi+wAN10J5U6A7/qxXDgGpRtK4dw4LTzcqx+QGtVKnO7R +cGzM7vRX+Bi6hG6H +-----END CERTIFICATE----- + +IdenTrust Public Sector Root CA 1 +================================= +-----BEGIN CERTIFICATE----- +MIIFZjCCA06gAwIBAgIQCgFCgAAAAUUjz0Z8AAAAAjANBgkqhkiG9w0BAQsFADBNMQswCQYDVQQG +EwJVUzESMBAGA1UEChMJSWRlblRydXN0MSowKAYDVQQDEyFJZGVuVHJ1c3QgUHVibGljIFNlY3Rv +ciBSb290IENBIDEwHhcNMTQwMTE2MTc1MzMyWhcNMzQwMTE2MTc1MzMyWjBNMQswCQYDVQQGEwJV +UzESMBAGA1UEChMJSWRlblRydXN0MSowKAYDVQQDEyFJZGVuVHJ1c3QgUHVibGljIFNlY3RvciBS +b290IENBIDEwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC2IpT8pEiv6EdrCvsnduTy +P4o7ekosMSqMjbCpwzFrqHd2hCa2rIFCDQjrVVi7evi8ZX3yoG2LqEfpYnYeEe4IFNGyRBb06tD6 +Hi9e28tzQa68ALBKK0CyrOE7S8ItneShm+waOh7wCLPQ5CQ1B5+ctMlSbdsHyo+1W/CD80/HLaXI +rcuVIKQxKFdYWuSNG5qrng0M8gozOSI5Cpcu81N3uURF/YTLNiCBWS2ab21ISGHKTN9T0a9SvESf +qy9rg3LvdYDaBjMbXcjaY8ZNzaxmMc3R3j6HEDbhuaR672BQssvKplbgN6+rNBM5Jeg5ZuSYeqoS +mJxZZoY+rfGwyj4GD3vwEUs3oERte8uojHH01bWRNszwFcYr3lEXsZdMUD2xlVl8BX0tIdUAvwFn +ol57plzy9yLxkA2T26pEUWbMfXYD62qoKjgZl3YNa4ph+bz27nb9cCvdKTz4Ch5bQhyLVi9VGxyh +LrXHFub4qjySjmm2AcG1hp2JDws4lFTo6tyePSW8Uybt1as5qsVATFSrsrTZ2fjXctscvG29ZV/v +iDUqZi/u9rNl8DONfJhBaUYPQxxp+pu10GFqzcpL2UyQRqsVWaFHVCkugyhfHMKiq3IXAAaOReyL +4jM9f9oZRORicsPfIsbyVtTdX5Vy7W1f90gDW/3FKqD2cyOEEBsB5wIDAQABo0IwQDAOBgNVHQ8B +Af8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQU43HgntinQtnbcZFrlJPrw6PRFKMw +DQYJKoZIhvcNAQELBQADggIBAEf63QqwEZE4rU1d9+UOl1QZgkiHVIyqZJnYWv6IAcVYpZmxI1Qj +t2odIFflAWJBF9MJ23XLblSQdf4an4EKwt3X9wnQW3IV5B4Jaj0z8yGa5hV+rVHVDRDtfULAj+7A +mgjVQdZcDiFpboBhDhXAuM/FSRJSzL46zNQuOAXeNf0fb7iAaJg9TaDKQGXSc3z1i9kKlT/YPyNt +GtEqJBnZhbMX73huqVjRI9PHE+1yJX9dsXNw0H8GlwmEKYBhHfpe/3OsoOOJuBxxFcbeMX8S3OFt +m6/n6J91eEyrRjuazr8FGF1NFTwWmhlQBJqymm9li1JfPFgEKCXAZmExfrngdbkaqIHWchezxQMx +NRF4eKLg6TCMf4DfWN88uieW4oA0beOY02QnrEh+KHdcxiVhJfiFDGX6xDIvpZgF5PgLZxYWxoK4 +Mhn5+bl53B/N66+rDt0b20XkeucC4pVd/GnwU2lhlXV5C15V5jgclKlZM57IcXR5f1GJtshquDDI +ajjDbp7hNxbqBWJMWxJH7ae0s1hWx0nzfxJoCTFx8G34Tkf71oXuxVhAGaQdp/lLQzfcaFpPz+vC +ZHTetBXZ9FRUGi8c15dxVJCO2SCdUyt/q4/i6jC8UDfv8Ue1fXwsBOxonbRJRBD0ckscZOf85muQ +3Wl9af0AVqW3rLatt8o+Ae+c +-----END CERTIFICATE----- + +Entrust Root Certification Authority - G2 +========================================= +-----BEGIN CERTIFICATE----- +MIIEPjCCAyagAwIBAgIESlOMKDANBgkqhkiG9w0BAQsFADCBvjELMAkGA1UEBhMCVVMxFjAUBgNV +BAoTDUVudHJ1c3QsIEluYy4xKDAmBgNVBAsTH1NlZSB3d3cuZW50cnVzdC5uZXQvbGVnYWwtdGVy +bXMxOTA3BgNVBAsTMChjKSAyMDA5IEVudHJ1c3QsIEluYy4gLSBmb3IgYXV0aG9yaXplZCB1c2Ug +b25seTEyMDAGA1UEAxMpRW50cnVzdCBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IC0gRzIw +HhcNMDkwNzA3MTcyNTU0WhcNMzAxMjA3MTc1NTU0WjCBvjELMAkGA1UEBhMCVVMxFjAUBgNVBAoT +DUVudHJ1c3QsIEluYy4xKDAmBgNVBAsTH1NlZSB3d3cuZW50cnVzdC5uZXQvbGVnYWwtdGVybXMx +OTA3BgNVBAsTMChjKSAyMDA5IEVudHJ1c3QsIEluYy4gLSBmb3IgYXV0aG9yaXplZCB1c2Ugb25s +eTEyMDAGA1UEAxMpRW50cnVzdCBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IC0gRzIwggEi +MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC6hLZy254Ma+KZ6TABp3bqMriVQRrJ2mFOWHLP +/vaCeb9zYQYKpSfYs1/TRU4cctZOMvJyig/3gxnQaoCAAEUesMfnmr8SVycco2gvCoe9amsOXmXz +HHfV1IWNcCG0szLni6LVhjkCsbjSR87kyUnEO6fe+1R9V77w6G7CebI6C1XiUJgWMhNcL3hWwcKU +s/Ja5CeanyTXxuzQmyWC48zCxEXFjJd6BmsqEZ+pCm5IO2/b1BEZQvePB7/1U1+cPvQXLOZprE4y +TGJ36rfo5bs0vBmLrpxR57d+tVOxMyLlbc9wPBr64ptntoP0jaWvYkxN4FisZDQSA/i2jZRjJKRx +AgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBRqciZ6 +0B7vfec7aVHUbI2fkBJmqzANBgkqhkiG9w0BAQsFAAOCAQEAeZ8dlsa2eT8ijYfThwMEYGprmi5Z +iXMRrEPR9RP/jTkrwPK9T3CMqS/qF8QLVJ7UG5aYMzyorWKiAHarWWluBh1+xLlEjZivEtRh2woZ +Rkfz6/djwUAFQKXSt/S1mja/qYh2iARVBCuch38aNzx+LaUa2NSJXsq9rD1s2G2v1fN2D807iDgi +nWyTmsQ9v4IbZT+mD12q/OWyFcq1rca8PdCE6OoGcrBNOTJ4vz4RnAuknZoh8/CbCzB428Hch0P+ +vGOaysXCHMnHjf87ElgI5rY97HosTvuDls4MPGmHVHOkc8KT/1EQrBVUAdj8BbGJoX90g5pJ19xO +e4pIb4tF9g== +-----END CERTIFICATE----- + +Entrust Root Certification Authority - EC1 +========================================== +-----BEGIN CERTIFICATE----- +MIIC+TCCAoCgAwIBAgINAKaLeSkAAAAAUNCR+TAKBggqhkjOPQQDAzCBvzELMAkGA1UEBhMCVVMx +FjAUBgNVBAoTDUVudHJ1c3QsIEluYy4xKDAmBgNVBAsTH1NlZSB3d3cuZW50cnVzdC5uZXQvbGVn +YWwtdGVybXMxOTA3BgNVBAsTMChjKSAyMDEyIEVudHJ1c3QsIEluYy4gLSBmb3IgYXV0aG9yaXpl +ZCB1c2Ugb25seTEzMDEGA1UEAxMqRW50cnVzdCBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5 +IC0gRUMxMB4XDTEyMTIxODE1MjUzNloXDTM3MTIxODE1NTUzNlowgb8xCzAJBgNVBAYTAlVTMRYw +FAYDVQQKEw1FbnRydXN0LCBJbmMuMSgwJgYDVQQLEx9TZWUgd3d3LmVudHJ1c3QubmV0L2xlZ2Fs +LXRlcm1zMTkwNwYDVQQLEzAoYykgMjAxMiBFbnRydXN0LCBJbmMuIC0gZm9yIGF1dGhvcml6ZWQg +dXNlIG9ubHkxMzAxBgNVBAMTKkVudHJ1c3QgUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAt +IEVDMTB2MBAGByqGSM49AgEGBSuBBAAiA2IABIQTydC6bUF74mzQ61VfZgIaJPRbiWlH47jCffHy +AsWfoPZb1YsGGYZPUxBtByQnoaD41UcZYUx9ypMn6nQM72+WCf5j7HBdNq1nd67JnXxVRDqiY1Ef +9eNi1KlHBz7MIKNCMEAwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYE +FLdj5xrdjekIplWDpOBqUEFlEUJJMAoGCCqGSM49BAMDA2cAMGQCMGF52OVCR98crlOZF7ZvHH3h +vxGU0QOIdeSNiaSKd0bebWHvAvX7td/M/k7//qnmpwIwW5nXhTcGtXsI/esni0qU+eH6p44mCOh8 +kmhtc9hvJqwhAriZtyZBWyVgrtBIGu4G +-----END CERTIFICATE----- + +CFCA EV ROOT +============ +-----BEGIN CERTIFICATE----- +MIIFjTCCA3WgAwIBAgIEGErM1jANBgkqhkiG9w0BAQsFADBWMQswCQYDVQQGEwJDTjEwMC4GA1UE +CgwnQ2hpbmEgRmluYW5jaWFsIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MRUwEwYDVQQDDAxDRkNB +IEVWIFJPT1QwHhcNMTIwODA4MDMwNzAxWhcNMjkxMjMxMDMwNzAxWjBWMQswCQYDVQQGEwJDTjEw +MC4GA1UECgwnQ2hpbmEgRmluYW5jaWFsIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MRUwEwYDVQQD +DAxDRkNBIEVWIFJPT1QwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDXXWvNED8fBVnV +BU03sQ7smCuOFR36k0sXgiFxEFLXUWRwFsJVaU2OFW2fvwwbwuCjZ9YMrM8irq93VCpLTIpTUnrD +7i7es3ElweldPe6hL6P3KjzJIx1qqx2hp/Hz7KDVRM8Vz3IvHWOX6Jn5/ZOkVIBMUtRSqy5J35DN +uF++P96hyk0g1CXohClTt7GIH//62pCfCqktQT+x8Rgp7hZZLDRJGqgG16iI0gNyejLi6mhNbiyW +ZXvKWfry4t3uMCz7zEasxGPrb382KzRzEpR/38wmnvFyXVBlWY9ps4deMm/DGIq1lY+wejfeWkU7 +xzbh72fROdOXW3NiGUgthxwG+3SYIElz8AXSG7Ggo7cbcNOIabla1jj0Ytwli3i/+Oh+uFzJlU9f +py25IGvPa931DfSCt/SyZi4QKPaXWnuWFo8BGS1sbn85WAZkgwGDg8NNkt0yxoekN+kWzqotaK8K +gWU6cMGbrU1tVMoqLUuFG7OA5nBFDWteNfB/O7ic5ARwiRIlk9oKmSJgamNgTnYGmE69g60dWIol +hdLHZR4tjsbftsbhf4oEIRUpdPA+nJCdDC7xij5aqgwJHsfVPKPtl8MeNPo4+QgO48BdK4PRVmrJ +tqhUUy54Mmc9gn900PvhtgVguXDbjgv5E1hvcWAQUhC5wUEJ73IfZzF4/5YFjQIDAQABo2MwYTAf +BgNVHSMEGDAWgBTj/i39KNALtbq2osS/BqoFjJP7LzAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB +/wQEAwIBBjAdBgNVHQ4EFgQU4/4t/SjQC7W6tqLEvwaqBYyT+y8wDQYJKoZIhvcNAQELBQADggIB +ACXGumvrh8vegjmWPfBEp2uEcwPenStPuiB/vHiyz5ewG5zz13ku9Ui20vsXiObTej/tUxPQ4i9q +ecsAIyjmHjdXNYmEwnZPNDatZ8POQQaIxffu2Bq41gt/UP+TqhdLjOztUmCypAbqTuv0axn96/Ua +4CUqmtzHQTb3yHQFhDmVOdYLO6Qn+gjYXB74BGBSESgoA//vU2YApUo0FmZ8/Qmkrp5nGm9BC2sG +E5uPhnEFtC+NiWYzKXZUmhH4J/qyP5Hgzg0b8zAarb8iXRvTvyUFTeGSGn+ZnzxEk8rUQElsgIfX +BDrDMlI1Dlb4pd19xIsNER9Tyx6yF7Zod1rg1MvIB671Oi6ON7fQAUtDKXeMOZePglr4UeWJoBjn +aH9dCi77o0cOPaYjesYBx4/IXr9tgFa+iiS6M+qf4TIRnvHST4D2G0CvOJ4RUHlzEhLN5mydLIhy +PDCBBpEi6lmt2hkuIsKNuYyH4Ga8cyNfIWRjgEj1oDwYPZTISEEdQLpe/v5WOaHIz16eGWRGENoX +kbcFgKyLmZJ956LYBws2J+dIeWCKw9cTXPhyQN9Ky8+ZAAoACxGV2lZFA4gKn2fQ1XmxqI1AbQ3C +ekD6819kR5LLU7m7Wc5P/dAVUwHY3+vZ5nbv0CO7O6l5s9UCKc2Jo5YPSjXnTkLAdc0Hz+Ys63su +-----END CERTIFICATE----- + +OISTE WISeKey Global Root GB CA +=============================== +-----BEGIN CERTIFICATE----- +MIIDtTCCAp2gAwIBAgIQdrEgUnTwhYdGs/gjGvbCwDANBgkqhkiG9w0BAQsFADBtMQswCQYDVQQG +EwJDSDEQMA4GA1UEChMHV0lTZUtleTEiMCAGA1UECxMZT0lTVEUgRm91bmRhdGlvbiBFbmRvcnNl +ZDEoMCYGA1UEAxMfT0lTVEUgV0lTZUtleSBHbG9iYWwgUm9vdCBHQiBDQTAeFw0xNDEyMDExNTAw +MzJaFw0zOTEyMDExNTEwMzFaMG0xCzAJBgNVBAYTAkNIMRAwDgYDVQQKEwdXSVNlS2V5MSIwIAYD +VQQLExlPSVNURSBGb3VuZGF0aW9uIEVuZG9yc2VkMSgwJgYDVQQDEx9PSVNURSBXSVNlS2V5IEds +b2JhbCBSb290IEdCIENBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA2Be3HEokKtaX +scriHvt9OO+Y9bI5mE4nuBFde9IllIiCFSZqGzG7qFshISvYD06fWvGxWuR51jIjK+FTzJlFXHtP +rby/h0oLS5daqPZI7H17Dc0hBt+eFf1Biki3IPShehtX1F1Q/7pn2COZH8g/497/b1t3sWtuuMlk +9+HKQUYOKXHQuSP8yYFfTvdv37+ErXNku7dCjmn21HYdfp2nuFeKUWdy19SouJVUQHMD9ur06/4o +Qnc/nSMbsrY9gBQHTC5P99UKFg29ZkM3fiNDecNAhvVMKdqOmq0NpQSHiB6F4+lT1ZvIiwNjeOvg +GUpuuy9rM2RYk61pv48b74JIxwIDAQABo1EwTzALBgNVHQ8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB +/zAdBgNVHQ4EFgQUNQ/INmNe4qPs+TtmFc5RUuORmj0wEAYJKwYBBAGCNxUBBAMCAQAwDQYJKoZI +hvcNAQELBQADggEBAEBM+4eymYGQfp3FsLAmzYh7KzKNbrghcViXfa43FK8+5/ea4n32cZiZBKpD +dHij40lhPnOMTZTg+XHEthYOU3gf1qKHLwI5gSk8rxWYITD+KJAAjNHhy/peyP34EEY7onhCkRd0 +VQreUGdNZtGn//3ZwLWoo4rOZvUPQ82nK1d7Y0Zqqi5S2PTt4W2tKZB4SLrhI6qjiey1q5bAtEui +HZeeevJuQHHfaPFlTc58Bd9TZaml8LGXBHAVRgOY1NK/VLSgWH1Sb9pWJmLU2NuJMW8c8CLC02Ic +Nc1MaRVUGpCY3useX8p3x8uOPUNpnJpY0CQ73xtAln41rYHHTnG6iBM= +-----END CERTIFICATE----- + +SZAFIR ROOT CA2 +=============== +-----BEGIN CERTIFICATE----- +MIIDcjCCAlqgAwIBAgIUPopdB+xV0jLVt+O2XwHrLdzk1uQwDQYJKoZIhvcNAQELBQAwUTELMAkG +A1UEBhMCUEwxKDAmBgNVBAoMH0tyYWpvd2EgSXpiYSBSb3psaWN6ZW5pb3dhIFMuQS4xGDAWBgNV +BAMMD1NaQUZJUiBST09UIENBMjAeFw0xNTEwMTkwNzQzMzBaFw0zNTEwMTkwNzQzMzBaMFExCzAJ +BgNVBAYTAlBMMSgwJgYDVQQKDB9LcmFqb3dhIEl6YmEgUm96bGljemVuaW93YSBTLkEuMRgwFgYD +VQQDDA9TWkFGSVIgUk9PVCBDQTIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC3vD5Q +qEvNQLXOYeeWyrSh2gwisPq1e3YAd4wLz32ohswmUeQgPYUM1ljj5/QqGJ3a0a4m7utT3PSQ1hNK +DJA8w/Ta0o4NkjrcsbH/ON7Dui1fgLkCvUqdGw+0w8LBZwPd3BucPbOw3gAeqDRHu5rr/gsUvTaE +2g0gv/pby6kWIK05YO4vdbbnl5z5Pv1+TW9NL++IDWr63fE9biCloBK0TXC5ztdyO4mTp4CEHCdJ +ckm1/zuVnsHMyAHs6A6KCpbns6aH5db5BSsNl0BwPLqsdVqc1U2dAgrSS5tmS0YHF2Wtn2yIANwi +ieDhZNRnvDF5YTy7ykHNXGoAyDw4jlivAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0P +AQH/BAQDAgEGMB0GA1UdDgQWBBQuFqlKGLXLzPVvUPMjX/hd56zwyDANBgkqhkiG9w0BAQsFAAOC +AQEAtXP4A9xZWx126aMqe5Aosk3AM0+qmrHUuOQn/6mWmc5G4G18TKI4pAZw8PRBEew/R40/cof5 +O/2kbytTAOD/OblqBw7rHRz2onKQy4I9EYKL0rufKq8h5mOGnXkZ7/e7DDWQw4rtTw/1zBLZpD67 +oPwglV9PJi8RI4NOdQcPv5vRtB3pEAT+ymCPoky4rc/hkA/NrgrHXXu3UNLUYfrVFdvXn4dRVOul +4+vJhaAlIDf7js4MNIThPIGyd05DpYhfhmehPea0XGG2Ptv+tyjFogeutcrKjSoS75ftwjCkySp6 ++/NNIxuZMzSgLvWpCz/UXeHPhJ/iGcJfitYgHuNztw== +-----END CERTIFICATE----- + +Certum Trusted Network CA 2 +=========================== +-----BEGIN CERTIFICATE----- +MIIF0jCCA7qgAwIBAgIQIdbQSk8lD8kyN/yqXhKN6TANBgkqhkiG9w0BAQ0FADCBgDELMAkGA1UE +BhMCUEwxIjAgBgNVBAoTGVVuaXpldG8gVGVjaG5vbG9naWVzIFMuQS4xJzAlBgNVBAsTHkNlcnR1 +bSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTEkMCIGA1UEAxMbQ2VydHVtIFRydXN0ZWQgTmV0d29y +ayBDQSAyMCIYDzIwMTExMDA2MDgzOTU2WhgPMjA0NjEwMDYwODM5NTZaMIGAMQswCQYDVQQGEwJQ +TDEiMCAGA1UEChMZVW5pemV0byBUZWNobm9sb2dpZXMgUy5BLjEnMCUGA1UECxMeQ2VydHVtIENl +cnRpZmljYXRpb24gQXV0aG9yaXR5MSQwIgYDVQQDExtDZXJ0dW0gVHJ1c3RlZCBOZXR3b3JrIENB +IDIwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC9+Xj45tWADGSdhhuWZGc/IjoedQF9 +7/tcZ4zJzFxrqZHmuULlIEub2pt7uZld2ZuAS9eEQCsn0+i6MLs+CRqnSZXvK0AkwpfHp+6bJe+o +CgCXhVqqndwpyeI1B+twTUrWwbNWuKFBOJvR+zF/j+Bf4bE/D44WSWDXBo0Y+aomEKsq09DRZ40b +Rr5HMNUuctHFY9rnY3lEfktjJImGLjQ/KUxSiyqnwOKRKIm5wFv5HdnnJ63/mgKXwcZQkpsCLL2p +uTRZCr+ESv/f/rOf69me4Jgj7KZrdxYq28ytOxykh9xGc14ZYmhFV+SQgkK7QtbwYeDBoz1mo130 +GO6IyY0XRSmZMnUCMe4pJshrAua1YkV/NxVaI2iJ1D7eTiew8EAMvE0Xy02isx7QBlrd9pPPV3WZ +9fqGGmd4s7+W/jTcvedSVuWz5XV710GRBdxdaeOVDUO5/IOWOZV7bIBaTxNyxtd9KXpEulKkKtVB +Rgkg/iKgtlswjbyJDNXXcPiHUv3a76xRLgezTv7QCdpw75j6VuZt27VXS9zlLCUVyJ4ueE742pye +hizKV/Ma5ciSixqClnrDvFASadgOWkaLOusm+iPJtrCBvkIApPjW/jAux9JG9uWOdf3yzLnQh1vM +BhBgu4M1t15n3kfsmUjxpKEV/q2MYo45VU85FrmxY53/twIDAQABo0IwQDAPBgNVHRMBAf8EBTAD +AQH/MB0GA1UdDgQWBBS2oVQ5AsOgP46KvPrU+Bym0ToO/TAOBgNVHQ8BAf8EBAMCAQYwDQYJKoZI +hvcNAQENBQADggIBAHGlDs7k6b8/ONWJWsQCYftMxRQXLYtPU2sQF/xlhMcQSZDe28cmk4gmb3DW +Al45oPePq5a1pRNcgRRtDoGCERuKTsZPpd1iHkTfCVn0W3cLN+mLIMb4Ck4uWBzrM9DPhmDJ2vuA +L55MYIR4PSFk1vtBHxgP58l1cb29XN40hz5BsA72udY/CROWFC/emh1auVbONTqwX3BNXuMp8SMo +clm2q8KMZiYcdywmdjWLKKdpoPk79SPdhRB0yZADVpHnr7pH1BKXESLjokmUbOe3lEu6LaTaM4tM +pkT/WjzGHWTYtTHkpjx6qFcL2+1hGsvxznN3Y6SHb0xRONbkX8eftoEq5IVIeVheO/jbAoJnwTnb +w3RLPTYe+SmTiGhbqEQZIfCn6IENLOiTNrQ3ssqwGyZ6miUfmpqAnksqP/ujmv5zMnHCnsZy4Ypo +J/HkD7TETKVhk/iXEAcqMCWpuchxuO9ozC1+9eB+D4Kob7a6bINDd82Kkhehnlt4Fj1F4jNy3eFm +ypnTycUm/Q1oBEauttmbjL4ZvrHG8hnjXALKLNhvSgfZyTXaQHXyxKcZb55CEJh15pWLYLztxRLX +is7VmFxWlgPF7ncGNf/P5O4/E2Hu29othfDNrp2yGAlFw5Khchf8R7agCyzxxN5DaAhqXzvwdmP7 +zAYspsbiDrW5viSP +-----END CERTIFICATE----- + +Hellenic Academic and Research Institutions RootCA 2015 +======================================================= +-----BEGIN CERTIFICATE----- +MIIGCzCCA/OgAwIBAgIBADANBgkqhkiG9w0BAQsFADCBpjELMAkGA1UEBhMCR1IxDzANBgNVBAcT +BkF0aGVuczFEMEIGA1UEChM7SGVsbGVuaWMgQWNhZGVtaWMgYW5kIFJlc2VhcmNoIEluc3RpdHV0 +aW9ucyBDZXJ0LiBBdXRob3JpdHkxQDA+BgNVBAMTN0hlbGxlbmljIEFjYWRlbWljIGFuZCBSZXNl +YXJjaCBJbnN0aXR1dGlvbnMgUm9vdENBIDIwMTUwHhcNMTUwNzA3MTAxMTIxWhcNNDAwNjMwMTAx +MTIxWjCBpjELMAkGA1UEBhMCR1IxDzANBgNVBAcTBkF0aGVuczFEMEIGA1UEChM7SGVsbGVuaWMg +QWNhZGVtaWMgYW5kIFJlc2VhcmNoIEluc3RpdHV0aW9ucyBDZXJ0LiBBdXRob3JpdHkxQDA+BgNV +BAMTN0hlbGxlbmljIEFjYWRlbWljIGFuZCBSZXNlYXJjaCBJbnN0aXR1dGlvbnMgUm9vdENBIDIw +MTUwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDC+Kk/G4n8PDwEXT2QNrCROnk8Zlrv +bTkBSRq0t89/TSNTt5AA4xMqKKYx8ZEA4yjsriFBzh/a/X0SWwGDD7mwX5nh8hKDgE0GPt+sr+eh +iGsxr/CL0BgzuNtFajT0AoAkKAoCFZVedioNmToUW/bLy1O8E00BiDeUJRtCvCLYjqOWXjrZMts+ +6PAQZe104S+nfK8nNLspfZu2zwnI5dMK/IhlZXQK3HMcXM1AsRzUtoSMTFDPaI6oWa7CJ06CojXd +FPQf/7J31Ycvqm59JCfnxssm5uX+Zwdj2EUN3TpZZTlYepKZcj2chF6IIbjV9Cz82XBST3i4vTwr +i5WY9bPRaM8gFH5MXF/ni+X1NYEZN9cRCLdmvtNKzoNXADrDgfgXy5I2XdGj2HUb4Ysn6npIQf1F +GQatJ5lOwXBH3bWfgVMS5bGMSF0xQxfjjMZ6Y5ZLKTBOhE5iGV48zpeQpX8B653g+IuJ3SWYPZK2 +fu/Z8VFRfS0myGlZYeCsargqNhEEelC9MoS+L9xy1dcdFkfkR2YgP/SWxa+OAXqlD3pk9Q0Yh9mu +iNX6hME6wGkoLfINaFGq46V3xqSQDqE3izEjR8EJCOtu93ib14L8hCCZSRm2Ekax+0VVFqmjZayc +Bw/qa9wfLgZy7IaIEuQt218FL+TwA9MmM+eAws1CoRc0CwIDAQABo0IwQDAPBgNVHRMBAf8EBTAD +AQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUcRVnyMjJvXVdctA4GGqd83EkVAswDQYJKoZI +hvcNAQELBQADggIBAHW7bVRLqhBYRjTyYtcWNl0IXtVsyIe9tC5G8jH4fOpCtZMWVdyhDBKg2mF+ +D1hYc2Ryx+hFjtyp8iY/xnmMsVMIM4GwVhO+5lFc2JsKT0ucVlMC6U/2DWDqTUJV6HwbISHTGzrM +d/K4kPFox/la/vot9L/J9UUbzjgQKjeKeaO04wlshYaT/4mWJ3iBj2fjRnRUjtkNaeJK9E10A/+y +d+2VZ5fkscWrv2oj6NSU4kQoYsRL4vDY4ilrGnB+JGGTe08DMiUNRSQrlrRGar9KC/eaj8GsGsVn +82800vpzY4zvFrCopEYq+OsS7HK07/grfoxSwIuEVPkvPuNVqNxmsdnhX9izjFk0WaSrT2y7Hxjb +davYy5LNlDhhDgcGH0tGEPEVvo2FXDtKK4F5D7Rpn0lQl033DlZdwJVqwjbDG2jJ9SrcR5q+ss7F +Jej6A7na+RZukYT1HCjI/CbM1xyQVqdfbzoEvM14iQuODy+jqk+iGxI9FghAD/FGTNeqewjBCvVt +J94Cj8rDtSvK6evIIVM4pcw72Hc3MKJP2W/R8kCtQXoXxdZKNYm3QdV8hn9VTYNKpXMgwDqvkPGa +JI7ZjnHKe7iG2rKPmT4dEw0SEe7Uq/DpFXYC5ODfqiAeW2GFZECpkJcNrVPSWh2HagCXZWK0vm9q +p/UsQu0yrbYhnr68 +-----END CERTIFICATE----- + +Hellenic Academic and Research Institutions ECC RootCA 2015 +=========================================================== +-----BEGIN CERTIFICATE----- +MIICwzCCAkqgAwIBAgIBADAKBggqhkjOPQQDAjCBqjELMAkGA1UEBhMCR1IxDzANBgNVBAcTBkF0 +aGVuczFEMEIGA1UEChM7SGVsbGVuaWMgQWNhZGVtaWMgYW5kIFJlc2VhcmNoIEluc3RpdHV0aW9u +cyBDZXJ0LiBBdXRob3JpdHkxRDBCBgNVBAMTO0hlbGxlbmljIEFjYWRlbWljIGFuZCBSZXNlYXJj +aCBJbnN0aXR1dGlvbnMgRUNDIFJvb3RDQSAyMDE1MB4XDTE1MDcwNzEwMzcxMloXDTQwMDYzMDEw +MzcxMlowgaoxCzAJBgNVBAYTAkdSMQ8wDQYDVQQHEwZBdGhlbnMxRDBCBgNVBAoTO0hlbGxlbmlj +IEFjYWRlbWljIGFuZCBSZXNlYXJjaCBJbnN0aXR1dGlvbnMgQ2VydC4gQXV0aG9yaXR5MUQwQgYD +VQQDEztIZWxsZW5pYyBBY2FkZW1pYyBhbmQgUmVzZWFyY2ggSW5zdGl0dXRpb25zIEVDQyBSb290 +Q0EgMjAxNTB2MBAGByqGSM49AgEGBSuBBAAiA2IABJKgQehLgoRc4vgxEZmGZE4JJS+dQS8KrjVP +dJWyUWRrjWvmP3CV8AVER6ZyOFB2lQJajq4onvktTpnvLEhvTCUp6NFxW98dwXU3tNf6e3pCnGoK +Vlp8aQuqgAkkbH7BRqNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0O +BBYEFLQiC4KZJAEOnLvkDv2/+5cgk5kqMAoGCCqGSM49BAMCA2cAMGQCMGfOFmI4oqxiRaeplSTA +GiecMjvAwNW6qef4BENThe5SId6d9SWDPp5YSy/XZxMOIQIwBeF1Ad5o7SofTUwJCA3sS61kFyjn +dc5FZXIhF8siQQ6ME5g4mlRtm8rifOoCWCKR +-----END CERTIFICATE----- + +ISRG Root X1 +============ +-----BEGIN CERTIFICATE----- +MIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAwTzELMAkGA1UE +BhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2VhcmNoIEdyb3VwMRUwEwYDVQQD +EwxJU1JHIFJvb3QgWDEwHhcNMTUwNjA0MTEwNDM4WhcNMzUwNjA0MTEwNDM4WjBPMQswCQYDVQQG +EwJVUzEpMCcGA1UEChMgSW50ZXJuZXQgU2VjdXJpdHkgUmVzZWFyY2ggR3JvdXAxFTATBgNVBAMT +DElTUkcgUm9vdCBYMTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK3oJHP0FDfzm54r +Vygch77ct984kIxuPOZXoHj3dcKi/vVqbvYATyjb3miGbESTtrFj/RQSa78f0uoxmyF+0TM8ukj1 +3Xnfs7j/EvEhmkvBioZxaUpmZmyPfjxwv60pIgbz5MDmgK7iS4+3mX6UA5/TR5d8mUgjU+g4rk8K +b4Mu0UlXjIB0ttov0DiNewNwIRt18jA8+o+u3dpjq+sWT8KOEUt+zwvo/7V3LvSye0rgTBIlDHCN +Aymg4VMk7BPZ7hm/ELNKjD+Jo2FR3qyHB5T0Y3HsLuJvW5iB4YlcNHlsdu87kGJ55tukmi8mxdAQ +4Q7e2RCOFvu396j3x+UCB5iPNgiV5+I3lg02dZ77DnKxHZu8A/lJBdiB3QW0KtZB6awBdpUKD9jf +1b0SHzUvKBds0pjBqAlkd25HN7rOrFleaJ1/ctaJxQZBKT5ZPt0m9STJEadao0xAH0ahmbWnOlFu +hjuefXKnEgV4We0+UXgVCwOPjdAvBbI+e0ocS3MFEvzG6uBQE3xDk3SzynTnjh8BCNAw1FtxNrQH +usEwMFxIt4I7mKZ9YIqioymCzLq9gwQbooMDQaHWBfEbwrbwqHyGO0aoSCqI3Haadr8faqU9GY/r +OPNk3sgrDQoo//fb4hVC1CLQJ13hef4Y53CIrU7m2Ys6xt0nUW7/vGT1M0NPAgMBAAGjQjBAMA4G +A1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBR5tFnme7bl5AFzgAiIyBpY +9umbbjANBgkqhkiG9w0BAQsFAAOCAgEAVR9YqbyyqFDQDLHYGmkgJykIrGF1XIpu+ILlaS/V9lZL +ubhzEFnTIZd+50xx+7LSYK05qAvqFyFWhfFQDlnrzuBZ6brJFe+GnY+EgPbk6ZGQ3BebYhtF8GaV +0nxvwuo77x/Py9auJ/GpsMiu/X1+mvoiBOv/2X/qkSsisRcOj/KKNFtY2PwByVS5uCbMiogziUwt +hDyC3+6WVwW6LLv3xLfHTjuCvjHIInNzktHCgKQ5ORAzI4JMPJ+GslWYHb4phowim57iaztXOoJw +TdwJx4nLCgdNbOhdjsnvzqvHu7UrTkXWStAmzOVyyghqpZXjFaH3pO3JLF+l+/+sKAIuvtd7u+Nx +e5AW0wdeRlN8NwdCjNPElpzVmbUq4JUagEiuTDkHzsxHpFKVK7q4+63SM1N95R1NbdWhscdCb+ZA +JzVcoyi3B43njTOQ5yOf+1CceWxG1bQVs5ZufpsMljq4Ui0/1lvh+wjChP4kqKOJ2qxq4RgqsahD +YVvTH9w7jXbyLeiNdd8XM2w9U/t7y0Ff/9yi0GE44Za4rF2LN9d11TPAmRGunUHBcnWEvgJBQl9n +JEiU0Zsnvgc/ubhPgXRR4Xq37Z0j4r7g1SgEEzwxA57demyPxgcYxn/eR44/KJ4EBs+lVDR3veyJ +m+kXQ99b21/+jh5Xos1AnX5iItreGCc= +-----END CERTIFICATE----- + +AC RAIZ FNMT-RCM +================ +-----BEGIN CERTIFICATE----- +MIIFgzCCA2ugAwIBAgIPXZONMGc2yAYdGsdUhGkHMA0GCSqGSIb3DQEBCwUAMDsxCzAJBgNVBAYT +AkVTMREwDwYDVQQKDAhGTk1ULVJDTTEZMBcGA1UECwwQQUMgUkFJWiBGTk1ULVJDTTAeFw0wODEw +MjkxNTU5NTZaFw0zMDAxMDEwMDAwMDBaMDsxCzAJBgNVBAYTAkVTMREwDwYDVQQKDAhGTk1ULVJD +TTEZMBcGA1UECwwQQUMgUkFJWiBGTk1ULVJDTTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoC +ggIBALpxgHpMhm5/yBNtwMZ9HACXjywMI7sQmkCpGreHiPibVmr75nuOi5KOpyVdWRHbNi63URcf +qQgfBBckWKo3Shjf5TnUV/3XwSyRAZHiItQDwFj8d0fsjz50Q7qsNI1NOHZnjrDIbzAzWHFctPVr +btQBULgTfmxKo0nRIBnuvMApGGWn3v7v3QqQIecaZ5JCEJhfTzC8PhxFtBDXaEAUwED653cXeuYL +j2VbPNmaUtu1vZ5Gzz3rkQUCwJaydkxNEJY7kvqcfw+Z374jNUUeAlz+taibmSXaXvMiwzn15Cou +08YfxGyqxRxqAQVKL9LFwag0Jl1mpdICIfkYtwb1TplvqKtMUejPUBjFd8g5CSxJkjKZqLsXF3mw +WsXmo8RZZUc1g16p6DULmbvkzSDGm0oGObVo/CK67lWMK07q87Hj/LaZmtVC+nFNCM+HHmpxffnT +tOmlcYF7wk5HlqX2doWjKI/pgG6BU6VtX7hI+cL5NqYuSf+4lsKMB7ObiFj86xsc3i1w4peSMKGJ +47xVqCfWS+2QrYv6YyVZLag13cqXM7zlzced0ezvXg5KkAYmY6252TUtB7p2ZSysV4999AeU14EC +ll2jB0nVetBX+RvnU0Z1qrB5QstocQjpYL05ac70r8NWQMetUqIJ5G+GR4of6ygnXYMgrwTJbFaa +i0b1AgMBAAGjgYMwgYAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYE +FPd9xf3E6Jobd2Sn9R2gzL+HYJptMD4GA1UdIAQ3MDUwMwYEVR0gADArMCkGCCsGAQUFBwIBFh1o +dHRwOi8vd3d3LmNlcnQuZm5tdC5lcy9kcGNzLzANBgkqhkiG9w0BAQsFAAOCAgEAB5BK3/MjTvDD +nFFlm5wioooMhfNzKWtN/gHiqQxjAb8EZ6WdmF/9ARP67Jpi6Yb+tmLSbkyU+8B1RXxlDPiyN8+s +D8+Nb/kZ94/sHvJwnvDKuO+3/3Y3dlv2bojzr2IyIpMNOmqOFGYMLVN0V2Ue1bLdI4E7pWYjJ2cJ +j+F3qkPNZVEI7VFY/uY5+ctHhKQV8Xa7pO6kO8Rf77IzlhEYt8llvhjho6Tc+hj507wTmzl6NLrT +Qfv6MooqtyuGC2mDOL7Nii4LcK2NJpLuHvUBKwrZ1pebbuCoGRw6IYsMHkCtA+fdZn71uSANA+iW ++YJF1DngoABd15jmfZ5nc8OaKveri6E6FO80vFIOiZiaBECEHX5FaZNXzuvO+FB8TxxuBEOb+dY7 +Ixjp6o7RTUaN8Tvkasq6+yO3m/qZASlaWFot4/nUbQ4mrcFuNLwy+AwF+mWj2zs3gyLp1txyM/1d +8iC9djwj2ij3+RvrWWTV3F9yfiD8zYm1kGdNYno/Tq0dwzn+evQoFt9B9kiABdcPUXmsEKvU7ANm +5mqwujGSQkBqvjrTcuFqN1W8rB2Vt2lh8kORdOag0wokRqEIr9baRRmW1FMdW4R58MD3R++Lj8UG +rp1MYp3/RgT408m2ECVAdf4WqslKYIYvuu8wd+RU4riEmViAqhOLUTpPSPaLtrM= +-----END CERTIFICATE----- + +Amazon Root CA 1 +================ +-----BEGIN CERTIFICATE----- +MIIDQTCCAimgAwIBAgITBmyfz5m/jAo54vB4ikPmljZbyjANBgkqhkiG9w0BAQsFADA5MQswCQYD +VQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6b24gUm9vdCBDQSAxMB4XDTE1 +MDUyNjAwMDAwMFoXDTM4MDExNzAwMDAwMFowOTELMAkGA1UEBhMCVVMxDzANBgNVBAoTBkFtYXpv +bjEZMBcGA1UEAxMQQW1hem9uIFJvb3QgQ0EgMTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC +ggEBALJ4gHHKeNXjca9HgFB0fW7Y14h29Jlo91ghYPl0hAEvrAIthtOgQ3pOsqTQNroBvo3bSMgH +FzZM9O6II8c+6zf1tRn4SWiw3te5djgdYZ6k/oI2peVKVuRF4fn9tBb6dNqcmzU5L/qwIFAGbHrQ +gLKm+a/sRxmPUDgH3KKHOVj4utWp+UhnMJbulHheb4mjUcAwhmahRWa6VOujw5H5SNz/0egwLX0t +dHA114gk957EWW67c4cX8jJGKLhD+rcdqsq08p8kDi1L93FcXmn/6pUCyziKrlA4b9v7LWIbxcce +VOF34GfID5yHI9Y/QCB/IIDEgEw+OyQmjgSubJrIqg0CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB +/zAOBgNVHQ8BAf8EBAMCAYYwHQYDVR0OBBYEFIQYzIU07LwMlJQuCFmcx7IQTgoIMA0GCSqGSIb3 +DQEBCwUAA4IBAQCY8jdaQZChGsV2USggNiMOruYou6r4lK5IpDB/G/wkjUu0yKGX9rbxenDIU5PM +CCjjmCXPI6T53iHTfIUJrU6adTrCC2qJeHZERxhlbI1Bjjt/msv0tadQ1wUsN+gDS63pYaACbvXy +8MWy7Vu33PqUXHeeE6V/Uq2V8viTO96LXFvKWlJbYK8U90vvo/ufQJVtMVT8QtPHRh8jrdkPSHCa +2XV4cdFyQzR1bldZwgJcJmApzyMZFo6IQ6XU5MsI+yMRQ+hDKXJioaldXgjUkK642M4UwtBV8ob2 +xJNDd2ZhwLnoQdeXeGADbkpyrqXRfboQnoZsG4q5WTP468SQvvG5 +-----END CERTIFICATE----- + +Amazon Root CA 2 +================ +-----BEGIN CERTIFICATE----- +MIIFQTCCAymgAwIBAgITBmyf0pY1hp8KD+WGePhbJruKNzANBgkqhkiG9w0BAQwFADA5MQswCQYD +VQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6b24gUm9vdCBDQSAyMB4XDTE1 +MDUyNjAwMDAwMFoXDTQwMDUyNjAwMDAwMFowOTELMAkGA1UEBhMCVVMxDzANBgNVBAoTBkFtYXpv +bjEZMBcGA1UEAxMQQW1hem9uIFJvb3QgQ0EgMjCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoC +ggIBAK2Wny2cSkxKgXlRmeyKy2tgURO8TW0G/LAIjd0ZEGrHJgw12MBvIITplLGbhQPDW9tK6Mj4 +kHbZW0/jTOgGNk3Mmqw9DJArktQGGWCsN0R5hYGCrVo34A3MnaZMUnbqQ523BNFQ9lXg1dKmSYXp +N+nKfq5clU1Imj+uIFptiJXZNLhSGkOQsL9sBbm2eLfq0OQ6PBJTYv9K8nu+NQWpEjTj82R0Yiw9 +AElaKP4yRLuH3WUnAnE72kr3H9rN9yFVkE8P7K6C4Z9r2UXTu/Bfh+08LDmG2j/e7HJV63mjrdvd +fLC6HM783k81ds8P+HgfajZRRidhW+mez/CiVX18JYpvL7TFz4QuK/0NURBs+18bvBt+xa47mAEx +kv8LV/SasrlX6avvDXbR8O70zoan4G7ptGmh32n2M8ZpLpcTnqWHsFcQgTfJU7O7f/aS0ZzQGPSS +btqDT6ZjmUyl+17vIWR6IF9sZIUVyzfpYgwLKhbcAS4y2j5L9Z469hdAlO+ekQiG+r5jqFoz7Mt0 +Q5X5bGlSNscpb/xVA1wf+5+9R+vnSUeVC06JIglJ4PVhHvG/LopyboBZ/1c6+XUyo05f7O0oYtlN +c/LMgRdg7c3r3NunysV+Ar3yVAhU/bQtCSwXVEqY0VThUWcI0u1ufm8/0i2BWSlmy5A5lREedCf+ +3euvAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMB0GA1UdDgQWBBSw +DPBMMPQFWAJI/TPlUq9LhONmUjANBgkqhkiG9w0BAQwFAAOCAgEAqqiAjw54o+Ci1M3m9Zh6O+oA +A7CXDpO8Wqj2LIxyh6mx/H9z/WNxeKWHWc8w4Q0QshNabYL1auaAn6AFC2jkR2vHat+2/XcycuUY ++gn0oJMsXdKMdYV2ZZAMA3m3MSNjrXiDCYZohMr/+c8mmpJ5581LxedhpxfL86kSk5Nrp+gvU5LE +YFiwzAJRGFuFjWJZY7attN6a+yb3ACfAXVU3dJnJUH/jWS5E4ywl7uxMMne0nxrpS10gxdr9HIcW +xkPo1LsmmkVwXqkLN1PiRnsn/eBG8om3zEK2yygmbtmlyTrIQRNg91CMFa6ybRoVGld45pIq2WWQ +gj9sAq+uEjonljYE1x2igGOpm/HlurR8FLBOybEfdF849lHqm/osohHUqS0nGkWxr7JOcQ3AWEbW +aQbLU8uz/mtBzUF+fUwPfHJ5elnNXkoOrJupmHN5fLT0zLm4BwyydFy4x2+IoZCn9Kr5v2c69BoV +Yh63n749sSmvZ6ES8lgQGVMDMBu4Gon2nL2XA46jCfMdiyHxtN/kHNGfZQIG6lzWE7OE76KlXIx3 +KadowGuuQNKotOrN8I1LOJwZmhsoVLiJkO/KdYE+HvJkJMcYr07/R54H9jVlpNMKVv/1F2Rs76gi +JUmTtt8AF9pYfl3uxRuw0dFfIRDH+fO6AgonB8Xx1sfT4PsJYGw= +-----END CERTIFICATE----- + +Amazon Root CA 3 +================ +-----BEGIN CERTIFICATE----- +MIIBtjCCAVugAwIBAgITBmyf1XSXNmY/Owua2eiedgPySjAKBggqhkjOPQQDAjA5MQswCQYDVQQG +EwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6b24gUm9vdCBDQSAzMB4XDTE1MDUy +NjAwMDAwMFoXDTQwMDUyNjAwMDAwMFowOTELMAkGA1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZ +MBcGA1UEAxMQQW1hem9uIFJvb3QgQ0EgMzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABCmXp8ZB +f8ANm+gBG1bG8lKlui2yEujSLtf6ycXYqm0fc4E7O5hrOXwzpcVOho6AF2hiRVd9RFgdszflZwjr +Zt6jQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMB0GA1UdDgQWBBSrttvXBp43 +rDCGB5Fwx5zEGbF4wDAKBggqhkjOPQQDAgNJADBGAiEA4IWSoxe3jfkrBqWTrBqYaGFy+uGh0Psc +eGCmQ5nFuMQCIQCcAu/xlJyzlvnrxir4tiz+OpAUFteMYyRIHN8wfdVoOw== +-----END CERTIFICATE----- + +Amazon Root CA 4 +================ +-----BEGIN CERTIFICATE----- +MIIB8jCCAXigAwIBAgITBmyf18G7EEwpQ+Vxe3ssyBrBDjAKBggqhkjOPQQDAzA5MQswCQYDVQQG +EwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6b24gUm9vdCBDQSA0MB4XDTE1MDUy +NjAwMDAwMFoXDTQwMDUyNjAwMDAwMFowOTELMAkGA1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZ +MBcGA1UEAxMQQW1hem9uIFJvb3QgQ0EgNDB2MBAGByqGSM49AgEGBSuBBAAiA2IABNKrijdPo1MN +/sGKe0uoe0ZLY7Bi9i0b2whxIdIA6GO9mif78DluXeo9pcmBqqNbIJhFXRbb/egQbeOc4OO9X4Ri +83BkM6DLJC9wuoihKqB1+IGuYgbEgds5bimwHvouXKNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNV +HQ8BAf8EBAMCAYYwHQYDVR0OBBYEFNPsxzplbszh2naaVvuc84ZtV+WBMAoGCCqGSM49BAMDA2gA +MGUCMDqLIfG9fhGt0O9Yli/W651+kI0rz2ZVwyzjKKlwCkcO8DdZEv8tmZQoTipPNU0zWgIxAOp1 +AE47xDqUEpHJWEadIRNyp4iciuRMStuW1KyLa2tJElMzrdfkviT8tQp21KW8EA== +-----END CERTIFICATE----- + +TUBITAK Kamu SM SSL Kok Sertifikasi - Surum 1 +============================================= +-----BEGIN CERTIFICATE----- +MIIEYzCCA0ugAwIBAgIBATANBgkqhkiG9w0BAQsFADCB0jELMAkGA1UEBhMCVFIxGDAWBgNVBAcT +D0dlYnplIC0gS29jYWVsaTFCMEAGA1UEChM5VHVya2l5ZSBCaWxpbXNlbCB2ZSBUZWtub2xvamlr +IEFyYXN0aXJtYSBLdXJ1bXUgLSBUVUJJVEFLMS0wKwYDVQQLEyRLYW11IFNlcnRpZmlrYXN5b24g +TWVya2V6aSAtIEthbXUgU00xNjA0BgNVBAMTLVRVQklUQUsgS2FtdSBTTSBTU0wgS29rIFNlcnRp +ZmlrYXNpIC0gU3VydW0gMTAeFw0xMzExMjUwODI1NTVaFw00MzEwMjUwODI1NTVaMIHSMQswCQYD +VQQGEwJUUjEYMBYGA1UEBxMPR2ViemUgLSBLb2NhZWxpMUIwQAYDVQQKEzlUdXJraXllIEJpbGlt +c2VsIHZlIFRla25vbG9qaWsgQXJhc3Rpcm1hIEt1cnVtdSAtIFRVQklUQUsxLTArBgNVBAsTJEth +bXUgU2VydGlmaWthc3lvbiBNZXJrZXppIC0gS2FtdSBTTTE2MDQGA1UEAxMtVFVCSVRBSyBLYW11 +IFNNIFNTTCBLb2sgU2VydGlmaWthc2kgLSBTdXJ1bSAxMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A +MIIBCgKCAQEAr3UwM6q7a9OZLBI3hNmNe5eA027n/5tQlT6QlVZC1xl8JoSNkvoBHToP4mQ4t4y8 +6Ij5iySrLqP1N+RAjhgleYN1Hzv/bKjFxlb4tO2KRKOrbEz8HdDc72i9z+SqzvBV96I01INrN3wc +wv61A+xXzry0tcXtAA9TNypN9E8Mg/uGz8v+jE69h/mniyFXnHrfA2eJLJ2XYacQuFWQfw4tJzh0 +3+f92k4S400VIgLI4OD8D62K18lUUMw7D8oWgITQUVbDjlZ/iSIzL+aFCr2lqBs23tPcLG07xxO9 +WSMs5uWk99gL7eqQQESolbuT1dCANLZGeA4fAJNG4e7p+exPFwIDAQABo0IwQDAdBgNVHQ4EFgQU +ZT/HiobGPN08VFw1+DrtUgxHV8gwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wDQYJ +KoZIhvcNAQELBQADggEBACo/4fEyjq7hmFxLXs9rHmoJ0iKpEsdeV31zVmSAhHqT5Am5EM2fKifh +AHe+SMg1qIGf5LgsyX8OsNJLN13qudULXjS99HMpw+0mFZx+CFOKWI3QSyjfwbPfIPP54+M638yc +lNhOT8NrF7f3cuitZjO1JVOr4PhMqZ398g26rrnZqsZr+ZO7rqu4lzwDGrpDxpa5RXI4s6ehlj2R +e37AIVNMh+3yC1SVUZPVIqUNivGTDj5UDrDYyU7c8jEyVupk+eq1nRZmQnLzf9OxMUP8pI4X8W0j +q5Rm+K37DwhuJi1/FwcJsoz7UMCflo3Ptv0AnVoUmr8CRPXBwp8iXqIPoeM= +-----END CERTIFICATE----- + +GDCA TrustAUTH R5 ROOT +====================== +-----BEGIN CERTIFICATE----- +MIIFiDCCA3CgAwIBAgIIfQmX/vBH6nowDQYJKoZIhvcNAQELBQAwYjELMAkGA1UEBhMCQ04xMjAw +BgNVBAoMKUdVQU5HIERPTkcgQ0VSVElGSUNBVEUgQVVUSE9SSVRZIENPLixMVEQuMR8wHQYDVQQD +DBZHRENBIFRydXN0QVVUSCBSNSBST09UMB4XDTE0MTEyNjA1MTMxNVoXDTQwMTIzMTE1NTk1OVow +YjELMAkGA1UEBhMCQ04xMjAwBgNVBAoMKUdVQU5HIERPTkcgQ0VSVElGSUNBVEUgQVVUSE9SSVRZ +IENPLixMVEQuMR8wHQYDVQQDDBZHRENBIFRydXN0QVVUSCBSNSBST09UMIICIjANBgkqhkiG9w0B +AQEFAAOCAg8AMIICCgKCAgEA2aMW8Mh0dHeb7zMNOwZ+Vfy1YI92hhJCfVZmPoiC7XJjDp6L3TQs +AlFRwxn9WVSEyfFrs0yw6ehGXTjGoqcuEVe6ghWinI9tsJlKCvLriXBjTnnEt1u9ol2x8kECK62p +OqPseQrsXzrj/e+APK00mxqriCZ7VqKChh/rNYmDf1+uKU49tm7srsHwJ5uu4/Ts765/94Y9cnrr +pftZTqfrlYwiOXnhLQiPzLyRuEH3FMEjqcOtmkVEs7LXLM3GKeJQEK5cy4KOFxg2fZfmiJqwTTQJ +9Cy5WmYqsBebnh52nUpmMUHfP/vFBu8btn4aRjb3ZGM74zkYI+dndRTVdVeSN72+ahsmUPI2JgaQ +xXABZG12ZuGR224HwGGALrIuL4xwp9E7PLOR5G62xDtw8mySlwnNR30YwPO7ng/Wi64HtloPzgsM +R6flPri9fcebNaBhlzpBdRfMK5Z3KpIhHtmVdiBnaM8Nvd/WHwlqmuLMc3GkL30SgLdTMEZeS1SZ +D2fJpcjyIMGC7J0R38IC+xo70e0gmu9lZJIQDSri3nDxGGeCjGHeuLzRL5z7D9Ar7Rt2ueQ5Vfj4 +oR24qoAATILnsn8JuLwwoC8N9VKejveSswoAHQBUlwbgsQfZxw9cZX08bVlX5O2ljelAU58VS6Bx +9hoh49pwBiFYFIeFd3mqgnkCAwEAAaNCMEAwHQYDVR0OBBYEFOLJQJ9NzuiaoXzPDj9lxSmIahlR +MA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3DQEBCwUAA4ICAQDRSVfg +p8xoWLoBDysZzY2wYUWsEe1jUGn4H3++Fo/9nesLqjJHdtJnJO29fDMylyrHBYZmDRd9FBUb1Ov9 +H5r2XpdptxolpAqzkT9fNqyL7FeoPueBihhXOYV0GkLH6VsTX4/5COmSdI31R9KrO9b7eGZONn35 +6ZLpBN79SWP8bfsUcZNnL0dKt7n/HipzcEYwv1ryL3ml4Y0M2fmyYzeMN2WFcGpcWwlyua1jPLHd ++PwyvzeG5LuOmCd+uh8W4XAR8gPfJWIyJyYYMoSf/wA6E7qaTfRPuBRwIrHKK5DOKcFw9C+df/KQ +HtZa37dG/OaG+svgIHZ6uqbL9XzeYqWxi+7egmaKTjowHz+Ay60nugxe19CxVsp3cbK1daFQqUBD +F8Io2c9Si1vIY9RCPqAzekYu9wogRlR+ak8x8YF+QnQ4ZXMn7sZ8uI7XpTrXmKGcjBBV09tL7ECQ +8s1uV9JiDnxXk7Gnbc2dg7sq5+W2O3FYrf3RRbxake5TFW/TRQl1brqQXR4EzzffHqhmsYzmIGrv +/EhOdJhCrylvLmrH+33RZjEizIYAfmaDDEL0vTSSwxrqT8p+ck0LcIymSLumoRT2+1hEmRSuqguT +aaApJUqlyyvdimYHFngVV3Eb7PVHhPOeMTd61X8kreS8/f3MboPoDKi3QWwH3b08hpcv0g== +-----END CERTIFICATE----- + +SSL.com Root Certification Authority RSA +======================================== +-----BEGIN CERTIFICATE----- +MIIF3TCCA8WgAwIBAgIIeyyb0xaAMpkwDQYJKoZIhvcNAQELBQAwfDELMAkGA1UEBhMCVVMxDjAM +BgNVBAgMBVRleGFzMRAwDgYDVQQHDAdIb3VzdG9uMRgwFgYDVQQKDA9TU0wgQ29ycG9yYXRpb24x +MTAvBgNVBAMMKFNTTC5jb20gUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSBSU0EwHhcNMTYw +MjEyMTczOTM5WhcNNDEwMjEyMTczOTM5WjB8MQswCQYDVQQGEwJVUzEOMAwGA1UECAwFVGV4YXMx +EDAOBgNVBAcMB0hvdXN0b24xGDAWBgNVBAoMD1NTTCBDb3Jwb3JhdGlvbjExMC8GA1UEAwwoU1NM +LmNvbSBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IFJTQTCCAiIwDQYJKoZIhvcNAQEBBQAD +ggIPADCCAgoCggIBAPkP3aMrfcvQKv7sZ4Wm5y4bunfh4/WvpOz6Sl2RxFdHaxh3a3by/ZPkPQ/C +Fp4LZsNWlJ4Xg4XOVu/yFv0AYvUiCVToZRdOQbngT0aXqhvIuG5iXmmxX9sqAn78bMrzQdjt0Oj8 +P2FI7bADFB0QDksZ4LtO7IZl/zbzXmcCC52GVWH9ejjt/uIZALdvoVBidXQ8oPrIJZK0bnoix/ge +oeOy3ZExqysdBP+lSgQ36YWkMyv94tZVNHwZpEpox7Ko07fKoZOI68GXvIz5HdkihCR0xwQ9aqkp +k8zruFvh/l8lqjRYyMEjVJ0bmBHDOJx+PYZspQ9AhnwC9FwCTyjLrnGfDzrIM/4RJTXq/LrFYD3Z +fBjVsqnTdXgDciLKOsMf7yzlLqn6niy2UUb9rwPW6mBo6oUWNmuF6R7As93EJNyAKoFBbZQ+yODJ +gUEAnl6/f8UImKIYLEJAs/lvOCdLToD0PYFH4Ih86hzOtXVcUS4cK38acijnALXRdMbX5J+tB5O2 +UzU1/Dfkw/ZdFr4hc96SCvigY2q8lpJqPvi8ZVWb3vUNiSYE/CUapiVpy8JtynziWV+XrOvvLsi8 +1xtZPCvM8hnIk2snYxnP/Okm+Mpxm3+T/jRnhE6Z6/yzeAkzcLpmpnbtG3PrGqUNxCITIJRWCk4s +bE6x/c+cCbqiM+2HAgMBAAGjYzBhMB0GA1UdDgQWBBTdBAkHovV6fVJTEpKV7jiAJQ2mWTAPBgNV +HRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFN0ECQei9Xp9UlMSkpXuOIAlDaZZMA4GA1UdDwEB/wQE +AwIBhjANBgkqhkiG9w0BAQsFAAOCAgEAIBgRlCn7Jp0cHh5wYfGVcpNxJK1ok1iOMq8bs3AD/CUr +dIWQPXhq9LmLpZc7tRiRux6n+UBbkflVma8eEdBcHadm47GUBwwyOabqG7B52B2ccETjit3E+ZUf +ijhDPwGFpUenPUayvOUiaPd7nNgsPgohyC0zrL/FgZkxdMF1ccW+sfAjRfSda/wZY52jvATGGAsl +u1OJD7OAUN5F7kR/q5R4ZJjT9ijdh9hwZXT7DrkT66cPYakylszeu+1jTBi7qUD3oFRuIIhxdRjq +erQ0cuAjJ3dctpDqhiVAq+8zD8ufgr6iIPv2tS0a5sKFsXQP+8hlAqRSAUfdSSLBv9jra6x+3uxj +MxW3IwiPxg+NQVrdjsW5j+VFP3jbutIbQLH+cU0/4IGiul607BXgk90IH37hVZkLId6Tngr75qNJ +vTYw/ud3sqB1l7UtgYgXZSD32pAAn8lSzDLKNXz1PQ/YK9f1JmzJBjSWFupwWRoyeXkLtoh/D1JI +Pb9s2KJELtFOt3JY04kTlf5Eq/jXixtunLwsoFvVagCvXzfh1foQC5ichucmj87w7G6KVwuA406y +wKBjYZC6VWg3dGq2ktufoYYitmUnDuy2n0Jg5GfCtdpBC8TTi2EbvPofkSvXRAdeuims2cXp71NI +WuuA8ShYIc2wBlX7Jz9TkHCpBB5XJ7k= +-----END CERTIFICATE----- + +SSL.com Root Certification Authority ECC +======================================== +-----BEGIN CERTIFICATE----- +MIICjTCCAhSgAwIBAgIIdebfy8FoW6gwCgYIKoZIzj0EAwIwfDELMAkGA1UEBhMCVVMxDjAMBgNV +BAgMBVRleGFzMRAwDgYDVQQHDAdIb3VzdG9uMRgwFgYDVQQKDA9TU0wgQ29ycG9yYXRpb24xMTAv +BgNVBAMMKFNTTC5jb20gUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSBFQ0MwHhcNMTYwMjEy +MTgxNDAzWhcNNDEwMjEyMTgxNDAzWjB8MQswCQYDVQQGEwJVUzEOMAwGA1UECAwFVGV4YXMxEDAO +BgNVBAcMB0hvdXN0b24xGDAWBgNVBAoMD1NTTCBDb3Jwb3JhdGlvbjExMC8GA1UEAwwoU1NMLmNv +bSBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IEVDQzB2MBAGByqGSM49AgEGBSuBBAAiA2IA +BEVuqVDEpiM2nl8ojRfLliJkP9x6jh3MCLOicSS6jkm5BBtHllirLZXI7Z4INcgn64mMU1jrYor+ +8FsPazFSY0E7ic3s7LaNGdM0B9y7xgZ/wkWV7Mt/qCPgCemB+vNH06NjMGEwHQYDVR0OBBYEFILR +hXMw5zUE044CkvvlpNHEIejNMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAUgtGFczDnNQTT +jgKS++Wk0cQh6M0wDgYDVR0PAQH/BAQDAgGGMAoGCCqGSM49BAMCA2cAMGQCMG/n61kRpGDPYbCW +e+0F+S8Tkdzt5fxQaxFGRrMcIQBiu77D5+jNB5n5DQtdcj7EqgIwH7y6C+IwJPt8bYBVCpk+gA0z +5Wajs6O7pdWLjwkspl1+4vAHCGht0nxpbl/f5Wpl +-----END CERTIFICATE----- + +SSL.com EV Root Certification Authority RSA R2 +============================================== +-----BEGIN CERTIFICATE----- +MIIF6zCCA9OgAwIBAgIIVrYpzTS8ePYwDQYJKoZIhvcNAQELBQAwgYIxCzAJBgNVBAYTAlVTMQ4w +DAYDVQQIDAVUZXhhczEQMA4GA1UEBwwHSG91c3RvbjEYMBYGA1UECgwPU1NMIENvcnBvcmF0aW9u +MTcwNQYDVQQDDC5TU0wuY29tIEVWIFJvb3QgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkgUlNBIFIy +MB4XDTE3MDUzMTE4MTQzN1oXDTQyMDUzMDE4MTQzN1owgYIxCzAJBgNVBAYTAlVTMQ4wDAYDVQQI +DAVUZXhhczEQMA4GA1UEBwwHSG91c3RvbjEYMBYGA1UECgwPU1NMIENvcnBvcmF0aW9uMTcwNQYD +VQQDDC5TU0wuY29tIEVWIFJvb3QgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkgUlNBIFIyMIICIjAN +BgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAjzZlQOHWTcDXtOlG2mvqM0fNTPl9fb69LT3w23jh +hqXZuglXaO1XPqDQCEGD5yhBJB/jchXQARr7XnAjssufOePPxU7Gkm0mxnu7s9onnQqG6YE3Bf7w +cXHswxzpY6IXFJ3vG2fThVUCAtZJycxa4bH3bzKfydQ7iEGonL3Lq9ttewkfokxykNorCPzPPFTO +Zw+oz12WGQvE43LrrdF9HSfvkusQv1vrO6/PgN3B0pYEW3p+pKk8OHakYo6gOV7qd89dAFmPZiw+ +B6KjBSYRaZfqhbcPlgtLyEDhULouisv3D5oi53+aNxPN8k0TayHRwMwi8qFG9kRpnMphNQcAb9Zh +CBHqurj26bNg5U257J8UZslXWNvNh2n4ioYSA0e/ZhN2rHd9NCSFg83XqpyQGp8hLH94t2S42Oim +9HizVcuE0jLEeK6jj2HdzghTreyI/BXkmg3mnxp3zkyPuBQVPWKchjgGAGYS5Fl2WlPAApiiECto +RHuOec4zSnaqW4EWG7WK2NAAe15itAnWhmMOpgWVSbooi4iTsjQc2KRVbrcc0N6ZVTsj9CLg+Slm +JuwgUHfbSguPvuUCYHBBXtSuUDkiFCbLsjtzdFVHB3mBOagwE0TlBIqulhMlQg+5U8Sb/M3kHN48 ++qvWBkofZ6aYMBzdLNvcGJVXZsb/XItW9XcCAwEAAaNjMGEwDwYDVR0TAQH/BAUwAwEB/zAfBgNV +HSMEGDAWgBT5YLvU49U09rj1BoAlp3PbRmmonjAdBgNVHQ4EFgQU+WC71OPVNPa49QaAJadz20Zp +qJ4wDgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3DQEBCwUAA4ICAQBWs47LCp1Jjr+kxJG7ZhcFUZh1 +++VQLHqe8RT6q9OKPv+RKY9ji9i0qVQBDb6Thi/5Sm3HXvVX+cpVHBK+Rw82xd9qt9t1wkclf7nx +Y/hoLVUE0fKNsKTPvDxeH3jnpaAgcLAExbf3cqfeIg29MyVGjGSSJuM+LmOW2puMPfgYCdcDzH2G +guDKBAdRUNf/ktUM79qGn5nX67evaOI5JpS6aLe/g9Pqemc9YmeuJeVy6OLk7K4S9ksrPJ/psEDz +OFSz/bdoyNrGj1E8svuR3Bznm53htw1yj+KkxKl4+esUrMZDBcJlOSgYAsOCsp0FvmXtll9ldDz7 +CTUue5wT/RsPXcdtgTpWD8w74a8CLyKsRspGPKAcTNZEtF4uXBVmCeEmKf7GUmG6sXP/wwyc5Wxq +lD8UykAWlYTzWamsX0xhk23RO8yilQwipmdnRC652dKKQbNmC1r7fSOl8hqw/96bg5Qu0T/fkreR +rwU7ZcegbLHNYhLDkBvjJc40vG93drEQw/cFGsDWr3RiSBd3kmmQYRzelYB0VI8YHMPzA9C/pEN1 +hlMYegouCRw2n5H9gooiS9EOUCXdywMMF8mDAAhONU2Ki+3wApRmLER/y5UnlhetCTCstnEXbosX +9hwJ1C07mKVx01QT2WDz9UtmT/rx7iASjbSsV7FFY6GsdqnC+w== +-----END CERTIFICATE----- + +SSL.com EV Root Certification Authority ECC +=========================================== +-----BEGIN CERTIFICATE----- +MIIClDCCAhqgAwIBAgIILCmcWxbtBZUwCgYIKoZIzj0EAwIwfzELMAkGA1UEBhMCVVMxDjAMBgNV +BAgMBVRleGFzMRAwDgYDVQQHDAdIb3VzdG9uMRgwFgYDVQQKDA9TU0wgQ29ycG9yYXRpb24xNDAy +BgNVBAMMK1NTTC5jb20gRVYgUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSBFQ0MwHhcNMTYw +MjEyMTgxNTIzWhcNNDEwMjEyMTgxNTIzWjB/MQswCQYDVQQGEwJVUzEOMAwGA1UECAwFVGV4YXMx +EDAOBgNVBAcMB0hvdXN0b24xGDAWBgNVBAoMD1NTTCBDb3Jwb3JhdGlvbjE0MDIGA1UEAwwrU1NM +LmNvbSBFViBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IEVDQzB2MBAGByqGSM49AgEGBSuB +BAAiA2IABKoSR5CYG/vvw0AHgyBO8TCCogbR8pKGYfL2IWjKAMTH6kMAVIbc/R/fALhBYlzccBYy +3h+Z1MzFB8gIH2EWB1E9fVwHU+M1OIzfzZ/ZLg1KthkuWnBaBu2+8KGwytAJKaNjMGEwHQYDVR0O +BBYEFFvKXuXe0oGqzagtZFG22XKbl+ZPMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAUW8pe +5d7SgarNqC1kUbbZcpuX5k8wDgYDVR0PAQH/BAQDAgGGMAoGCCqGSM49BAMCA2gAMGUCMQCK5kCJ +N+vp1RPZytRrJPOwPYdGWBrssd9v+1a6cGvHOMzosYxPD/fxZ3YOg9AeUY8CMD32IygmTMZgh5Mm +m7I1HrrW9zzRHM76JTymGoEVW/MSD2zuZYrJh6j5B+BimoxcSg== +-----END CERTIFICATE----- + +GlobalSign Root CA - R6 +======================= +-----BEGIN CERTIFICATE----- +MIIFgzCCA2ugAwIBAgIORea7A4Mzw4VlSOb/RVEwDQYJKoZIhvcNAQEMBQAwTDEgMB4GA1UECxMX +R2xvYmFsU2lnbiBSb290IENBIC0gUjYxEzARBgNVBAoTCkdsb2JhbFNpZ24xEzARBgNVBAMTCkds +b2JhbFNpZ24wHhcNMTQxMjEwMDAwMDAwWhcNMzQxMjEwMDAwMDAwWjBMMSAwHgYDVQQLExdHbG9i +YWxTaWduIFJvb3QgQ0EgLSBSNjETMBEGA1UEChMKR2xvYmFsU2lnbjETMBEGA1UEAxMKR2xvYmFs +U2lnbjCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAJUH6HPKZvnsFMp7PPcNCPG0RQss +grRIxutbPK6DuEGSMxSkb3/pKszGsIhrxbaJ0cay/xTOURQh7ErdG1rG1ofuTToVBu1kZguSgMpE +3nOUTvOniX9PeGMIyBJQbUJmL025eShNUhqKGoC3GYEOfsSKvGRMIRxDaNc9PIrFsmbVkJq3MQbF +vuJtMgamHvm566qjuL++gmNQ0PAYid/kD3n16qIfKtJwLnvnvJO7bVPiSHyMEAc4/2ayd2F+4OqM +PKq0pPbzlUoSB239jLKJz9CgYXfIWHSw1CM69106yqLbnQneXUQtkPGBzVeS+n68UARjNN9rkxi+ +azayOeSsJDa38O+2HBNXk7besvjihbdzorg1qkXy4J02oW9UivFyVm4uiMVRQkQVlO6jxTiWm05O +WgtH8wY2SXcwvHE35absIQh1/OZhFj931dmRl4QKbNQCTXTAFO39OfuD8l4UoQSwC+n+7o/hbguy +CLNhZglqsQY6ZZZZwPA1/cnaKI0aEYdwgQqomnUdnjqGBQCe24DWJfncBZ4nWUx2OVvq+aWh2IMP +0f/fMBH5hc8zSPXKbWQULHpYT9NLCEnFlWQaYw55PfWzjMpYrZxCRXluDocZXFSxZba/jJvcE+kN +b7gu3GduyYsRtYQUigAZcIN5kZeR1BonvzceMgfYFGM8KEyvAgMBAAGjYzBhMA4GA1UdDwEB/wQE +AwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBSubAWjkxPioufi1xzWx/B/yGdToDAfBgNV +HSMEGDAWgBSubAWjkxPioufi1xzWx/B/yGdToDANBgkqhkiG9w0BAQwFAAOCAgEAgyXt6NH9lVLN +nsAEoJFp5lzQhN7craJP6Ed41mWYqVuoPId8AorRbrcWc+ZfwFSY1XS+wc3iEZGtIxg93eFyRJa0 +lV7Ae46ZeBZDE1ZXs6KzO7V33EByrKPrmzU+sQghoefEQzd5Mr6155wsTLxDKZmOMNOsIeDjHfrY +BzN2VAAiKrlNIC5waNrlU/yDXNOd8v9EDERm8tLjvUYAGm0CuiVdjaExUd1URhxN25mW7xocBFym +Fe944Hn+Xds+qkxV/ZoVqW/hpvvfcDDpw+5CRu3CkwWJ+n1jez/QcYF8AOiYrg54NMMl+68KnyBr +3TsTjxKM4kEaSHpzoHdpx7Zcf4LIHv5YGygrqGytXm3ABdJ7t+uA/iU3/gKbaKxCXcPu9czc8FB1 +0jZpnOZ7BN9uBmm23goJSFmH63sUYHpkqmlD75HHTOwY3WzvUy2MmeFe8nI+z1TIvWfspA9MRf/T +uTAjB0yPEL+GltmZWrSZVxykzLsViVO6LAUP5MSeGbEYNNVMnbrt9x+vJJUEeKgDu+6B5dpffItK +oZB0JaezPkvILFa9x8jvOOJckvB595yEunQtYQEgfn7R8k8HWV+LLUNS60YMlOH1Zkd5d9VUWx+t +JDfLRVpOoERIyNiwmcUVhAn21klJwGW45hpxbqCo8YLoRT5s1gLXCmeDBVrJpBA= +-----END CERTIFICATE----- + +OISTE WISeKey Global Root GC CA +=============================== +-----BEGIN CERTIFICATE----- +MIICaTCCAe+gAwIBAgIQISpWDK7aDKtARb8roi066jAKBggqhkjOPQQDAzBtMQswCQYDVQQGEwJD +SDEQMA4GA1UEChMHV0lTZUtleTEiMCAGA1UECxMZT0lTVEUgRm91bmRhdGlvbiBFbmRvcnNlZDEo +MCYGA1UEAxMfT0lTVEUgV0lTZUtleSBHbG9iYWwgUm9vdCBHQyBDQTAeFw0xNzA1MDkwOTQ4MzRa +Fw00MjA1MDkwOTU4MzNaMG0xCzAJBgNVBAYTAkNIMRAwDgYDVQQKEwdXSVNlS2V5MSIwIAYDVQQL +ExlPSVNURSBGb3VuZGF0aW9uIEVuZG9yc2VkMSgwJgYDVQQDEx9PSVNURSBXSVNlS2V5IEdsb2Jh +bCBSb290IEdDIENBMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAETOlQwMYPchi82PG6s4nieUqjFqdr +VCTbUf/q9Akkwwsin8tqJ4KBDdLArzHkdIJuyiXZjHWd8dvQmqJLIX4Wp2OQ0jnUsYd4XxiWD1Ab +NTcPasbc2RNNpI6QN+a9WzGRo1QwUjAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAd +BgNVHQ4EFgQUSIcUrOPDnpBgOtfKie7TrYy0UGYwEAYJKwYBBAGCNxUBBAMCAQAwCgYIKoZIzj0E +AwMDaAAwZQIwJsdpW9zV57LnyAyMjMPdeYwbY9XJUpROTYJKcx6ygISpJcBMWm1JKWB4E+J+SOtk +AjEA2zQgMgj/mkkCtojeFK9dbJlxjRo/i9fgojaGHAeCOnZT/cKi7e97sIBPWA9LUzm9 +-----END CERTIFICATE----- + +UCA Global G2 Root +================== +-----BEGIN CERTIFICATE----- +MIIFRjCCAy6gAwIBAgIQXd+x2lqj7V2+WmUgZQOQ7zANBgkqhkiG9w0BAQsFADA9MQswCQYDVQQG +EwJDTjERMA8GA1UECgwIVW5pVHJ1c3QxGzAZBgNVBAMMElVDQSBHbG9iYWwgRzIgUm9vdDAeFw0x +NjAzMTEwMDAwMDBaFw00MDEyMzEwMDAwMDBaMD0xCzAJBgNVBAYTAkNOMREwDwYDVQQKDAhVbmlU +cnVzdDEbMBkGA1UEAwwSVUNBIEdsb2JhbCBHMiBSb290MIICIjANBgkqhkiG9w0BAQEFAAOCAg8A +MIICCgKCAgEAxeYrb3zvJgUno4Ek2m/LAfmZmqkywiKHYUGRO8vDaBsGxUypK8FnFyIdK+35KYmT +oni9kmugow2ifsqTs6bRjDXVdfkX9s9FxeV67HeToI8jrg4aA3++1NDtLnurRiNb/yzmVHqUwCoV +8MmNsHo7JOHXaOIxPAYzRrZUEaalLyJUKlgNAQLx+hVRZ2zA+te2G3/RVogvGjqNO7uCEeBHANBS +h6v7hn4PJGtAnTRnvI3HLYZveT6OqTwXS3+wmeOwcWDcC/Vkw85DvG1xudLeJ1uK6NjGruFZfc8o +LTW4lVYa8bJYS7cSN8h8s+1LgOGN+jIjtm+3SJUIsUROhYw6AlQgL9+/V087OpAh18EmNVQg7Mc/ +R+zvWr9LesGtOxdQXGLYD0tK3Cv6brxzks3sx1DoQZbXqX5t2Okdj4q1uViSukqSKwxW/YDrCPBe +KW4bHAyvj5OJrdu9o54hyokZ7N+1wxrrFv54NkzWbtA+FxyQF2smuvt6L78RHBgOLXMDj6DlNaBa +4kx1HXHhOThTeEDMg5PXCp6dW4+K5OXgSORIskfNTip1KnvyIvbJvgmRlld6iIis7nCs+dwp4wwc +OxJORNanTrAmyPPZGpeRaOrvjUYG0lZFWJo8DA+DuAUlwznPO6Q0ibd5Ei9Hxeepl2n8pndntd97 +8XplFeRhVmUCAwEAAaNCMEAwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0O +BBYEFIHEjMz15DD/pQwIX4wVZyF0Ad/fMA0GCSqGSIb3DQEBCwUAA4ICAQATZSL1jiutROTL/7lo +5sOASD0Ee/ojL3rtNtqyzm325p7lX1iPyzcyochltq44PTUbPrw7tgTQvPlJ9Zv3hcU2tsu8+Mg5 +1eRfB70VVJd0ysrtT7q6ZHafgbiERUlMjW+i67HM0cOU2kTC5uLqGOiiHycFutfl1qnN3e92mI0A +Ds0b+gO3joBYDic/UvuUospeZcnWhNq5NXHzJsBPd+aBJ9J3O5oUb3n09tDh05S60FdRvScFDcH9 +yBIw7m+NESsIndTUv4BFFJqIRNow6rSn4+7vW4LVPtateJLbXDzz2K36uGt/xDYotgIVilQsnLAX +c47QN6MUPJiVAAwpBVueSUmxX8fjy88nZY41F7dXyDDZQVu5FLbowg+UMaeUmMxq67XhJ/UQqAHo +jhJi6IjMtX9Gl8CbEGY4GjZGXyJoPd/JxhMnq1MGrKI8hgZlb7F+sSlEmqO6SWkoaY/X5V+tBIZk +bxqgDMUIYs6Ao9Dz7GjevjPHF1t/gMRMTLGmhIrDO7gJzRSBuhjjVFc2/tsvfEehOjPI+Vg7RE+x +ygKJBJYoaMVLuCaJu9YzL1DV/pqJuhgyklTGW+Cd+V7lDSKb9triyCGyYiGqhkCyLmTTX8jjfhFn +RR8F/uOi77Oos/N9j/gMHyIfLXC0uAE0djAA5SN4p1bXUB+K+wb1whnw0A== +-----END CERTIFICATE----- + +UCA Extended Validation Root +============================ +-----BEGIN CERTIFICATE----- +MIIFWjCCA0KgAwIBAgIQT9Irj/VkyDOeTzRYZiNwYDANBgkqhkiG9w0BAQsFADBHMQswCQYDVQQG +EwJDTjERMA8GA1UECgwIVW5pVHJ1c3QxJTAjBgNVBAMMHFVDQSBFeHRlbmRlZCBWYWxpZGF0aW9u +IFJvb3QwHhcNMTUwMzEzMDAwMDAwWhcNMzgxMjMxMDAwMDAwWjBHMQswCQYDVQQGEwJDTjERMA8G +A1UECgwIVW5pVHJ1c3QxJTAjBgNVBAMMHFVDQSBFeHRlbmRlZCBWYWxpZGF0aW9uIFJvb3QwggIi +MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCpCQcoEwKwmeBkqh5DFnpzsZGgdT6o+uM4AHrs +iWogD4vFsJszA1qGxliG1cGFu0/GnEBNyr7uaZa4rYEwmnySBesFK5pI0Lh2PpbIILvSsPGP2KxF +Rv+qZ2C0d35qHzwaUnoEPQc8hQ2E0B92CvdqFN9y4zR8V05WAT558aopO2z6+I9tTcg1367r3CTu +eUWnhbYFiN6IXSV8l2RnCdm/WhUFhvMJHuxYMjMR83dksHYf5BA1FxvyDrFspCqjc/wJHx4yGVMR +59mzLC52LqGj3n5qiAno8geK+LLNEOfic0CTuwjRP+H8C5SzJe98ptfRr5//lpr1kXuYC3fUfugH +0mK1lTnj8/FtDw5lhIpjVMWAtuCeS31HJqcBCF3RiJ7XwzJE+oJKCmhUfzhTA8ykADNkUVkLo4KR +el7sFsLzKuZi2irbWWIQJUoqgQtHB0MGcIfS+pMRKXpITeuUx3BNr2fVUbGAIAEBtHoIppB/TuDv +B0GHr2qlXov7z1CymlSvw4m6WC31MJixNnI5fkkE/SmnTHnkBVfblLkWU41Gsx2VYVdWf6/wFlth +WG82UBEL2KwrlRYaDh8IzTY0ZRBiZtWAXxQgXy0MoHgKaNYs1+lvK9JKBZP8nm9rZ/+I8U6laUpS +NwXqxhaN0sSZ0YIrO7o1dfdRUVjzyAfd5LQDfwIDAQABo0IwQDAdBgNVHQ4EFgQU2XQ65DA9DfcS +3H5aBZ8eNJr34RQwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAYYwDQYJKoZIhvcNAQEL +BQADggIBADaNl8xCFWQpN5smLNb7rhVpLGsaGvdftvkHTFnq88nIua7Mui563MD1sC3AO6+fcAUR +ap8lTwEpcOPlDOHqWnzcSbvBHiqB9RZLcpHIojG5qtr8nR/zXUACE/xOHAbKsxSQVBcZEhrxH9cM +aVr2cXj0lH2RC47skFSOvG+hTKv8dGT9cZr4QQehzZHkPJrgmzI5c6sq1WnIeJEmMX3ixzDx/BR4 +dxIOE/TdFpS/S2d7cFOFyrC78zhNLJA5wA3CXWvp4uXViI3WLL+rG761KIcSF3Ru/H38j9CHJrAb ++7lsq+KePRXBOy5nAliRn+/4Qh8st2j1da3Ptfb/EX3C8CSlrdP6oDyp+l3cpaDvRKS+1ujl5BOW +F3sGPjLtx7dCvHaj2GU4Kzg1USEODm8uNBNA4StnDG1KQTAYI1oyVZnJF+A83vbsea0rWBmirSwi +GpWOvpaQXUJXxPkUAzUrHC1RVwinOt4/5Mi0A3PCwSaAuwtCH60NryZy2sy+s6ODWA2CxR9GUeOc +GMyNm43sSet1UNWMKFnKdDTajAshqx7qG+XH/RU+wBeq+yNuJkbL+vmxcmtpzyKEC2IPrNkZAJSi +djzULZrtBJ4tBmIQN1IchXIbJ+XMxjHsN+xjWZsLHXbMfjKaiJUINlK73nZfdklJrX+9ZSCyycEr +dhh2n1ax +-----END CERTIFICATE----- + +Certigna Root CA +================ +-----BEGIN CERTIFICATE----- +MIIGWzCCBEOgAwIBAgIRAMrpG4nxVQMNo+ZBbcTjpuEwDQYJKoZIhvcNAQELBQAwWjELMAkGA1UE +BhMCRlIxEjAQBgNVBAoMCURoaW15b3RpczEcMBoGA1UECwwTMDAwMiA0ODE0NjMwODEwMDAzNjEZ +MBcGA1UEAwwQQ2VydGlnbmEgUm9vdCBDQTAeFw0xMzEwMDEwODMyMjdaFw0zMzEwMDEwODMyMjda +MFoxCzAJBgNVBAYTAkZSMRIwEAYDVQQKDAlEaGlteW90aXMxHDAaBgNVBAsMEzAwMDIgNDgxNDYz +MDgxMDAwMzYxGTAXBgNVBAMMEENlcnRpZ25hIFJvb3QgQ0EwggIiMA0GCSqGSIb3DQEBAQUAA4IC +DwAwggIKAoICAQDNGDllGlmx6mQWDoyUJJV8g9PFOSbcDO8WV43X2KyjQn+Cyu3NW9sOty3tRQgX +stmzy9YXUnIo245Onoq2C/mehJpNdt4iKVzSs9IGPjA5qXSjklYcoW9MCiBtnyN6tMbaLOQdLNyz +KNAT8kxOAkmhVECe5uUFoC2EyP+YbNDrihqECB63aCPuI9Vwzm1RaRDuoXrC0SIxwoKF0vJVdlB8 +JXrJhFwLrN1CTivngqIkicuQstDuI7pmTLtipPlTWmR7fJj6o0ieD5Wupxj0auwuA0Wv8HT4Ks16 +XdG+RCYyKfHx9WzMfgIhC59vpD++nVPiz32pLHxYGpfhPTc3GGYo0kDFUYqMwy3OU4gkWGQwFsWq +4NYKpkDfePb1BHxpE4S80dGnBs8B92jAqFe7OmGtBIyT46388NtEbVncSVmurJqZNjBBe3YzIoej +wpKGbvlw7q6Hh5UbxHq9MfPU0uWZ/75I7HX1eBYdpnDBfzwboZL7z8g81sWTCo/1VTp2lc5ZmIoJ +lXcymoO6LAQ6l73UL77XbJuiyn1tJslV1c/DeVIICZkHJC1kJWumIWmbat10TWuXekG9qxf5kBdI +jzb5LdXF2+6qhUVB+s06RbFo5jZMm5BX7CO5hwjCxAnxl4YqKE3idMDaxIzb3+KhF1nOJFl0Mdp/ +/TBt2dzhauH8XwIDAQABo4IBGjCCARYwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYw +HQYDVR0OBBYEFBiHVuBud+4kNTxOc5of1uHieX4rMB8GA1UdIwQYMBaAFBiHVuBud+4kNTxOc5of +1uHieX4rMEQGA1UdIAQ9MDswOQYEVR0gADAxMC8GCCsGAQUFBwIBFiNodHRwczovL3d3d3cuY2Vy +dGlnbmEuZnIvYXV0b3JpdGVzLzBtBgNVHR8EZjBkMC+gLaArhilodHRwOi8vY3JsLmNlcnRpZ25h +LmZyL2NlcnRpZ25hcm9vdGNhLmNybDAxoC+gLYYraHR0cDovL2NybC5kaGlteW90aXMuY29tL2Nl +cnRpZ25hcm9vdGNhLmNybDANBgkqhkiG9w0BAQsFAAOCAgEAlLieT/DjlQgi581oQfccVdV8AOIt +OoldaDgvUSILSo3L6btdPrtcPbEo/uRTVRPPoZAbAh1fZkYJMyjhDSSXcNMQH+pkV5a7XdrnxIxP +TGRGHVyH41neQtGbqH6mid2PHMkwgu07nM3A6RngatgCdTer9zQoKJHyBApPNeNgJgH60BGM+RFq +7q89w1DTj18zeTyGqHNFkIwgtnJzFyO+B2XleJINugHA64wcZr+shncBlA2c5uk5jR+mUYyZDDl3 +4bSb+hxnV29qao6pK0xXeXpXIs/NX2NGjVxZOob4Mkdio2cNGJHc+6Zr9UhhcyNZjgKnvETq9Emd +8VRY+WCv2hikLyhF3HqgiIZd8zvn/yk1gPxkQ5Tm4xxvvq0OKmOZK8l+hfZx6AYDlf7ej0gcWtSS +6Cvu5zHbugRqh5jnxV/vfaci9wHYTfmJ0A6aBVmknpjZbyvKcL5kwlWj9Omvw5Ip3IgWJJk8jSaY +tlu3zM63Nwf9JtmYhST/WSMDmu2dnajkXjjO11INb9I/bbEFa0nOipFGc/T2L/Coc3cOZayhjWZS +aX5LaAzHHjcng6WMxwLkFM1JAbBzs/3GkDpv0mztO+7skb6iQ12LAEpmJURw3kAP+HwV96LOPNde +E4yBFxgX0b3xdxA61GU5wSesVywlVP+i2k+KYTlerj1KjL0= +-----END CERTIFICATE----- + +emSign Root CA - G1 +=================== +-----BEGIN CERTIFICATE----- +MIIDlDCCAnygAwIBAgIKMfXkYgxsWO3W2DANBgkqhkiG9w0BAQsFADBnMQswCQYDVQQGEwJJTjET +MBEGA1UECxMKZW1TaWduIFBLSTElMCMGA1UEChMcZU11ZGhyYSBUZWNobm9sb2dpZXMgTGltaXRl +ZDEcMBoGA1UEAxMTZW1TaWduIFJvb3QgQ0EgLSBHMTAeFw0xODAyMTgxODMwMDBaFw00MzAyMTgx +ODMwMDBaMGcxCzAJBgNVBAYTAklOMRMwEQYDVQQLEwplbVNpZ24gUEtJMSUwIwYDVQQKExxlTXVk +aHJhIFRlY2hub2xvZ2llcyBMaW1pdGVkMRwwGgYDVQQDExNlbVNpZ24gUm9vdCBDQSAtIEcxMIIB +IjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAk0u76WaK7p1b1TST0Bsew+eeuGQzf2N4aLTN +LnF115sgxk0pvLZoYIr3IZpWNVrzdr3YzZr/k1ZLpVkGoZM0Kd0WNHVO8oG0x5ZOrRkVUkr+PHB1 +cM2vK6sVmjM8qrOLqs1D/fXqcP/tzxE7lM5OMhbTI0Aqd7OvPAEsbO2ZLIvZTmmYsvePQbAyeGHW +DV/D+qJAkh1cF+ZwPjXnorfCYuKrpDhMtTk1b+oDafo6VGiFbdbyL0NVHpENDtjVaqSW0RM8LHhQ +6DqS0hdW5TUaQBw+jSztOd9C4INBdN+jzcKGYEho42kLVACL5HZpIQ15TjQIXhTCzLG3rdd8cIrH +hQIDAQABo0IwQDAdBgNVHQ4EFgQU++8Nhp6w492pufEhF38+/PB3KxowDgYDVR0PAQH/BAQDAgEG +MA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAFn/8oz1h31xPaOfG1vR2vjTnGs2 +vZupYeveFix0PZ7mddrXuqe8QhfnPZHr5X3dPpzxz5KsbEjMwiI/aTvFthUvozXGaCocV685743Q +NcMYDHsAVhzNixl03r4PEuDQqqE/AjSxcM6dGNYIAwlG7mDgfrbESQRRfXBgvKqy/3lyeqYdPV8q ++Mri/Tm3R7nrft8EI6/6nAYH6ftjk4BAtcZsCjEozgyfz7MjNYBBjWzEN3uBL4ChQEKF6dk4jeih +U80Bv2noWgbyRQuQ+q7hv53yrlc8pa6yVvSLZUDp/TGBLPQ5Cdjua6e0ph0VpZj3AYHYhX3zUVxx +iN66zB+Afko= +-----END CERTIFICATE----- + +emSign ECC Root CA - G3 +======================= +-----BEGIN CERTIFICATE----- +MIICTjCCAdOgAwIBAgIKPPYHqWhwDtqLhDAKBggqhkjOPQQDAzBrMQswCQYDVQQGEwJJTjETMBEG +A1UECxMKZW1TaWduIFBLSTElMCMGA1UEChMcZU11ZGhyYSBUZWNobm9sb2dpZXMgTGltaXRlZDEg +MB4GA1UEAxMXZW1TaWduIEVDQyBSb290IENBIC0gRzMwHhcNMTgwMjE4MTgzMDAwWhcNNDMwMjE4 +MTgzMDAwWjBrMQswCQYDVQQGEwJJTjETMBEGA1UECxMKZW1TaWduIFBLSTElMCMGA1UEChMcZU11 +ZGhyYSBUZWNobm9sb2dpZXMgTGltaXRlZDEgMB4GA1UEAxMXZW1TaWduIEVDQyBSb290IENBIC0g +RzMwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQjpQy4LRL1KPOxst3iAhKAnjlfSU2fySU0WXTsuwYc +58Byr+iuL+FBVIcUqEqy6HyC5ltqtdyzdc6LBtCGI79G1Y4PPwT01xySfvalY8L1X44uT6EYGQIr +MgqCZH0Wk9GjQjBAMB0GA1UdDgQWBBR8XQKEE9TMipuBzhccLikenEhjQjAOBgNVHQ8BAf8EBAMC +AQYwDwYDVR0TAQH/BAUwAwEB/zAKBggqhkjOPQQDAwNpADBmAjEAvvNhzwIQHWSVB7gYboiFBS+D +CBeQyh+KTOgNG3qxrdWBCUfvO6wIBHxcmbHtRwfSAjEAnbpV/KlK6O3t5nYBQnvI+GDZjVGLVTv7 +jHvrZQnD+JbNR6iC8hZVdyR+EhCVBCyj +-----END CERTIFICATE----- + +emSign Root CA - C1 +=================== +-----BEGIN CERTIFICATE----- +MIIDczCCAlugAwIBAgILAK7PALrEzzL4Q7IwDQYJKoZIhvcNAQELBQAwVjELMAkGA1UEBhMCVVMx +EzARBgNVBAsTCmVtU2lnbiBQS0kxFDASBgNVBAoTC2VNdWRocmEgSW5jMRwwGgYDVQQDExNlbVNp +Z24gUm9vdCBDQSAtIEMxMB4XDTE4MDIxODE4MzAwMFoXDTQzMDIxODE4MzAwMFowVjELMAkGA1UE +BhMCVVMxEzARBgNVBAsTCmVtU2lnbiBQS0kxFDASBgNVBAoTC2VNdWRocmEgSW5jMRwwGgYDVQQD +ExNlbVNpZ24gUm9vdCBDQSAtIEMxMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAz+up +ufGZBczYKCFK83M0UYRWEPWgTywS4/oTmifQz/l5GnRfHXk5/Fv4cI7gklL35CX5VIPZHdPIWoU/ +Xse2B+4+wM6ar6xWQio5JXDWv7V7Nq2s9nPczdcdioOl+yuQFTdrHCZH3DspVpNqs8FqOp099cGX +OFgFixwR4+S0uF2FHYP+eF8LRWgYSKVGczQ7/g/IdrvHGPMF0Ybzhe3nudkyrVWIzqa2kbBPrH4V +I5b2P/AgNBbeCsbEBEV5f6f9vtKppa+cxSMq9zwhbL2vj07FOrLzNBL834AaSaTUqZX3noleooms +lMuoaJuvimUnzYnu3Yy1aylwQ6BpC+S5DwIDAQABo0IwQDAdBgNVHQ4EFgQU/qHgcB4qAzlSWkK+ +XJGFehiqTbUwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQAD +ggEBAMJKVvoVIXsoounlHfv4LcQ5lkFMOycsxGwYFYDGrK9HWS8mC+M2sO87/kOXSTKZEhVb3xEp +/6tT+LvBeA+snFOvV71ojD1pM/CjoCNjO2RnIkSt1XHLVip4kqNPEjE2NuLe/gDEo2APJ62gsIq1 +NnpSob0n9CAnYuhNlCQT5AoE6TyrLshDCUrGYQTlSTR+08TI9Q/Aqum6VF7zYytPT1DU/rl7mYw9 +wC68AivTxEDkigcxHpvOJpkT+xHqmiIMERnHXhuBUDDIlhJu58tBf5E7oke3VIAb3ADMmpDqw8NQ +BmIMMMAVSKeoWXzhriKi4gp6D/piq1JM4fHfyr6DDUI= +-----END CERTIFICATE----- + +emSign ECC Root CA - C3 +======================= +-----BEGIN CERTIFICATE----- +MIICKzCCAbGgAwIBAgIKe3G2gla4EnycqDAKBggqhkjOPQQDAzBaMQswCQYDVQQGEwJVUzETMBEG +A1UECxMKZW1TaWduIFBLSTEUMBIGA1UEChMLZU11ZGhyYSBJbmMxIDAeBgNVBAMTF2VtU2lnbiBF +Q0MgUm9vdCBDQSAtIEMzMB4XDTE4MDIxODE4MzAwMFoXDTQzMDIxODE4MzAwMFowWjELMAkGA1UE +BhMCVVMxEzARBgNVBAsTCmVtU2lnbiBQS0kxFDASBgNVBAoTC2VNdWRocmEgSW5jMSAwHgYDVQQD +ExdlbVNpZ24gRUNDIFJvb3QgQ0EgLSBDMzB2MBAGByqGSM49AgEGBSuBBAAiA2IABP2lYa57JhAd +6bciMK4G9IGzsUJxlTm801Ljr6/58pc1kjZGDoeVjbk5Wum739D+yAdBPLtVb4OjavtisIGJAnB9 +SMVK4+kiVCJNk7tCDK93nCOmfddhEc5lx/h//vXyqaNCMEAwHQYDVR0OBBYEFPtaSNCAIEDyqOkA +B2kZd6fmw/TPMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MAoGCCqGSM49BAMDA2gA +MGUCMQC02C8Cif22TGK6Q04ThHK1rt0c3ta13FaPWEBaLd4gTCKDypOofu4SQMfWh0/434UCMBwU +ZOR8loMRnLDRWmFLpg9J0wD8ofzkpf9/rdcw0Md3f76BB1UwUCAU9Vc4CqgxUQ== +-----END CERTIFICATE----- + +Hongkong Post Root CA 3 +======================= +-----BEGIN CERTIFICATE----- +MIIFzzCCA7egAwIBAgIUCBZfikyl7ADJk0DfxMauI7gcWqQwDQYJKoZIhvcNAQELBQAwbzELMAkG +A1UEBhMCSEsxEjAQBgNVBAgTCUhvbmcgS29uZzESMBAGA1UEBxMJSG9uZyBLb25nMRYwFAYDVQQK +Ew1Ib25na29uZyBQb3N0MSAwHgYDVQQDExdIb25na29uZyBQb3N0IFJvb3QgQ0EgMzAeFw0xNzA2 +MDMwMjI5NDZaFw00MjA2MDMwMjI5NDZaMG8xCzAJBgNVBAYTAkhLMRIwEAYDVQQIEwlIb25nIEtv +bmcxEjAQBgNVBAcTCUhvbmcgS29uZzEWMBQGA1UEChMNSG9uZ2tvbmcgUG9zdDEgMB4GA1UEAxMX +SG9uZ2tvbmcgUG9zdCBSb290IENBIDMwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCz +iNfqzg8gTr7m1gNt7ln8wlffKWihgw4+aMdoWJwcYEuJQwy51BWy7sFOdem1p+/l6TWZ5Mwc50tf +jTMwIDNT2aa71T4Tjukfh0mtUC1Qyhi+AViiE3CWu4mIVoBc+L0sPOFMV4i707mV78vH9toxdCim +5lSJ9UExyuUmGs2C4HDaOym71QP1mbpV9WTRYA6ziUm4ii8F0oRFKHyPaFASePwLtVPLwpgchKOe +sL4jpNrcyCse2m5FHomY2vkALgbpDDtw1VAliJnLzXNg99X/NWfFobxeq81KuEXryGgeDQ0URhLj +0mRiikKYvLTGCAj4/ahMZJx2Ab0vqWwzD9g/KLg8aQFChn5pwckGyuV6RmXpwtZQQS4/t+TtbNe/ +JgERohYpSms0BpDsE9K2+2p20jzt8NYt3eEV7KObLyzJPivkaTv/ciWxNoZbx39ri1UbSsUgYT2u +y1DhCDq+sI9jQVMwCFk8mB13umOResoQUGC/8Ne8lYePl8X+l2oBlKN8W4UdKjk60FSh0Tlxnf0h ++bV78OLgAo9uliQlLKAeLKjEiafv7ZkGL7YKTE/bosw3Gq9HhS2KX8Q0NEwA/RiTZxPRN+ZItIsG +xVd7GYYKecsAyVKvQv83j+GjHno9UKtjBucVtT+2RTeUN7F+8kjDf8V1/peNRY8apxpyKBpADwID +AQABo2MwYTAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAfBgNVHSMEGDAWgBQXnc0e +i9Y5K3DTXNSguB+wAPzFYTAdBgNVHQ4EFgQUF53NHovWOStw01zUoLgfsAD8xWEwDQYJKoZIhvcN +AQELBQADggIBAFbVe27mIgHSQpsY1Q7XZiNc4/6gx5LS6ZStS6LG7BJ8dNVI0lkUmcDrudHr9Egw +W62nV3OZqdPlt9EuWSRY3GguLmLYauRwCy0gUCCkMpXRAJi70/33MvJJrsZ64Ee+bs7Lo3I6LWld +y8joRTnU+kLBEUx3XZL7av9YROXrgZ6voJmtvqkBZss4HTzfQx/0TW60uhdG/H39h4F5ag0zD/ov ++BS5gLNdTaqX4fnkGMX41TiMJjz98iji7lpJiCzfeT2OnpA8vUFKOt1b9pq0zj8lMH8yfaIDlNDc +eqFS3m6TjRgm/VWsvY+b0s+v54Ysyx8Jb6NvqYTUc79NoXQbTiNg8swOqn+knEwlqLJmOzj/2ZQw +9nKEvmhVEA/GcywWaZMH/rFF7buiVWqw2rVKAiUnhde3t4ZEFolsgCs+l6mc1X5VTMbeRRAc6uk7 +nwNT7u56AQIWeNTowr5GdogTPyK7SBIdUgC0An4hGh6cJfTzPV4e0hz5sy229zdcxsshTrD3mUcY +hcErulWuBurQB7Lcq9CClnXO0lD+mefPL5/ndtFhKvshuzHQqp9HpLIiyhY6UFfEW0NnxWViA0kB +60PZ2Pierc+xYw5F9KBaLJstxabArahH9CdMOA0uG0k7UvToiIMrVCjU8jVStDKDYmlkDJGcn5fq +dBb9HxEGmpv0 +-----END CERTIFICATE----- + +Entrust Root Certification Authority - G4 +========================================= +-----BEGIN CERTIFICATE----- +MIIGSzCCBDOgAwIBAgIRANm1Q3+vqTkPAAAAAFVlrVgwDQYJKoZIhvcNAQELBQAwgb4xCzAJBgNV +BAYTAlVTMRYwFAYDVQQKEw1FbnRydXN0LCBJbmMuMSgwJgYDVQQLEx9TZWUgd3d3LmVudHJ1c3Qu +bmV0L2xlZ2FsLXRlcm1zMTkwNwYDVQQLEzAoYykgMjAxNSBFbnRydXN0LCBJbmMuIC0gZm9yIGF1 +dGhvcml6ZWQgdXNlIG9ubHkxMjAwBgNVBAMTKUVudHJ1c3QgUm9vdCBDZXJ0aWZpY2F0aW9uIEF1 +dGhvcml0eSAtIEc0MB4XDTE1MDUyNzExMTExNloXDTM3MTIyNzExNDExNlowgb4xCzAJBgNVBAYT +AlVTMRYwFAYDVQQKEw1FbnRydXN0LCBJbmMuMSgwJgYDVQQLEx9TZWUgd3d3LmVudHJ1c3QubmV0 +L2xlZ2FsLXRlcm1zMTkwNwYDVQQLEzAoYykgMjAxNSBFbnRydXN0LCBJbmMuIC0gZm9yIGF1dGhv +cml6ZWQgdXNlIG9ubHkxMjAwBgNVBAMTKUVudHJ1c3QgUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhv +cml0eSAtIEc0MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAsewsQu7i0TD/pZJH4i3D +umSXbcr3DbVZwbPLqGgZ2K+EbTBwXX7zLtJTmeH+H17ZSK9dE43b/2MzTdMAArzE+NEGCJR5WIoV +3imz/f3ET+iq4qA7ec2/a0My3dl0ELn39GjUu9CH1apLiipvKgS1sqbHoHrmSKvS0VnM1n4j5pds +8ELl3FFLFUHtSUrJ3hCX1nbB76W1NhSXNdh4IjVS70O92yfbYVaCNNzLiGAMC1rlLAHGVK/XqsEQ +e9IFWrhAnoanw5CGAlZSCXqc0ieCU0plUmr1POeo8pyvi73TDtTUXm6Hnmo9RR3RXRv06QqsYJn7 +ibT/mCzPfB3pAqoEmh643IhuJbNsZvc8kPNXwbMv9W3y+8qh+CmdRouzavbmZwe+LGcKKh9asj5X +xNMhIWNlUpEbsZmOeX7m640A2Vqq6nPopIICR5b+W45UYaPrL0swsIsjdXJ8ITzI9vF01Bx7owVV +7rtNOzK+mndmnqxpkCIHH2E6lr7lmk/MBTwoWdPBDFSoWWG9yHJM6Nyfh3+9nEg2XpWjDrk4JFX8 +dWbrAuMINClKxuMrLzOg2qOGpRKX/YAr2hRC45K9PvJdXmd0LhyIRyk0X+IyqJwlN4y6mACXi0mW +Hv0liqzc2thddG5msP9E36EYxr5ILzeUePiVSj9/E15dWf10hkNjc0kCAwEAAaNCMEAwDwYDVR0T +AQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFJ84xFYjwznooHFs6FRM5Og6sb9n +MA0GCSqGSIb3DQEBCwUAA4ICAQAS5UKme4sPDORGpbZgQIeMJX6tuGguW8ZAdjwD+MlZ9POrYs4Q +jbRaZIxowLByQzTSGwv2LFPSypBLhmb8qoMi9IsabyZIrHZ3CL/FmFz0Jomee8O5ZDIBf9PD3Vht +7LGrhFV0d4QEJ1JrhkzO3bll/9bGXp+aEJlLdWr+aumXIOTkdnrG0CSqkM0gkLpHZPt/B7NTeLUK +YvJzQ85BK4FqLoUWlFPUa19yIqtRLULVAJyZv967lDtX/Zr1hstWO1uIAeV8KEsD+UmDfLJ/fOPt +jqF/YFOOVZ1QNBIPt5d7bIdKROf1beyAN/BYGW5KaHbwH5Lk6rWS02FREAutp9lfx1/cH6NcjKF+ +m7ee01ZvZl4HliDtC3T7Zk6LERXpgUl+b7DUUH8i119lAg2m9IUe2K4GS0qn0jFmwvjO5QimpAKW +RGhXxNUzzxkvFMSUHHuk2fCfDrGA4tGeEWSpiBE6doLlYsKA2KSD7ZPvfC+QsDJMlhVoSFLUmQjA +JOgc47OlIQ6SwJAfzyBfyjs4x7dtOvPmRLgOMWuIjnDrnBdSqEGULoe256YSxXXfW8AKbnuk5F6G ++TaU33fD6Q3AOfF5u0aOq0NZJ7cguyPpVkAh7DE9ZapD8j3fcEThuk0mEDuYn/PIjhs4ViFqUZPT +kcpG2om3PVODLAgfi49T3f+sHw== +-----END CERTIFICATE----- + +Microsoft ECC Root Certificate Authority 2017 +============================================= +-----BEGIN CERTIFICATE----- +MIICWTCCAd+gAwIBAgIQZvI9r4fei7FK6gxXMQHC7DAKBggqhkjOPQQDAzBlMQswCQYDVQQGEwJV +UzEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMTYwNAYDVQQDEy1NaWNyb3NvZnQgRUND +IFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5IDIwMTcwHhcNMTkxMjE4MjMwNjQ1WhcNNDIwNzE4 +MjMxNjA0WjBlMQswCQYDVQQGEwJVUzEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMTYw +NAYDVQQDEy1NaWNyb3NvZnQgRUNDIFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5IDIwMTcwdjAQ +BgcqhkjOPQIBBgUrgQQAIgNiAATUvD0CQnVBEyPNgASGAlEvaqiBYgtlzPbKnR5vSmZRogPZnZH6 +thaxjG7efM3beaYvzrvOcS/lpaso7GMEZpn4+vKTEAXhgShC48Zo9OYbhGBKia/teQ87zvH2RPUB +eMCjVDBSMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBTIy5lycFIM ++Oa+sgRXKSrPQhDtNTAQBgkrBgEEAYI3FQEEAwIBADAKBggqhkjOPQQDAwNoADBlAjBY8k3qDPlf +Xu5gKcs68tvWMoQZP3zVL8KxzJOuULsJMsbG7X7JNpQS5GiFBqIb0C8CMQCZ6Ra0DvpWSNSkMBaR +eNtUjGUBiudQZsIxtzm6uBoiB078a1QWIP8rtedMDE2mT3M= +-----END CERTIFICATE----- + +Microsoft RSA Root Certificate Authority 2017 +============================================= +-----BEGIN CERTIFICATE----- +MIIFqDCCA5CgAwIBAgIQHtOXCV/YtLNHcB6qvn9FszANBgkqhkiG9w0BAQwFADBlMQswCQYDVQQG +EwJVUzEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMTYwNAYDVQQDEy1NaWNyb3NvZnQg +UlNBIFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5IDIwMTcwHhcNMTkxMjE4MjI1MTIyWhcNNDIw +NzE4MjMwMDIzWjBlMQswCQYDVQQGEwJVUzEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9u +MTYwNAYDVQQDEy1NaWNyb3NvZnQgUlNBIFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5IDIwMTcw +ggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDKW76UM4wplZEWCpW9R2LBifOZNt9GkMml +7Xhqb0eRaPgnZ1AzHaGm++DlQ6OEAlcBXZxIQIJTELy/xztokLaCLeX0ZdDMbRnMlfl7rEqUrQ7e +S0MdhweSE5CAg2Q1OQT85elss7YfUJQ4ZVBcF0a5toW1HLUX6NZFndiyJrDKxHBKrmCk3bPZ7Pw7 +1VdyvD/IybLeS2v4I2wDwAW9lcfNcztmgGTjGqwu+UcF8ga2m3P1eDNbx6H7JyqhtJqRjJHTOoI+ +dkC0zVJhUXAoP8XFWvLJjEm7FFtNyP9nTUwSlq31/niol4fX/V4ggNyhSyL71Imtus5Hl0dVe49F +yGcohJUcaDDv70ngNXtk55iwlNpNhTs+VcQor1fznhPbRiefHqJeRIOkpcrVE7NLP8TjwuaGYaRS +MLl6IE9vDzhTyzMMEyuP1pq9KsgtsRx9S1HKR9FIJ3Jdh+vVReZIZZ2vUpC6W6IYZVcSn2i51BVr +lMRpIpj0M+Dt+VGOQVDJNE92kKz8OMHY4Xu54+OU4UZpyw4KUGsTuqwPN1q3ErWQgR5WrlcihtnJ +0tHXUeOrO8ZV/R4O03QK0dqq6mm4lyiPSMQH+FJDOvTKVTUssKZqwJz58oHhEmrARdlns87/I6KJ +ClTUFLkqqNfs+avNJVgyeY+QW5g5xAgGwax/Dj0ApQIDAQABo1QwUjAOBgNVHQ8BAf8EBAMCAYYw +DwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUCctZf4aycI8awznjwNnpv7tNsiMwEAYJKwYBBAGC +NxUBBAMCAQAwDQYJKoZIhvcNAQEMBQADggIBAKyvPl3CEZaJjqPnktaXFbgToqZCLgLNFgVZJ8og +6Lq46BrsTaiXVq5lQ7GPAJtSzVXNUzltYkyLDVt8LkS/gxCP81OCgMNPOsduET/m4xaRhPtthH80 +dK2Jp86519efhGSSvpWhrQlTM93uCupKUY5vVau6tZRGrox/2KJQJWVggEbbMwSubLWYdFQl3JPk ++ONVFT24bcMKpBLBaYVu32TxU5nhSnUgnZUP5NbcA/FZGOhHibJXWpS2qdgXKxdJ5XbLwVaZOjex +/2kskZGT4d9Mozd2TaGf+G0eHdP67Pv0RR0Tbc/3WeUiJ3IrhvNXuzDtJE3cfVa7o7P4NHmJweDy +AmH3pvwPuxwXC65B2Xy9J6P9LjrRk5Sxcx0ki69bIImtt2dmefU6xqaWM/5TkshGsRGRxpl/j8nW +ZjEgQRCHLQzWwa80mMpkg/sTV9HB8Dx6jKXB/ZUhoHHBk2dxEuqPiAppGWSZI1b7rCoucL5mxAyE +7+WL85MB+GqQk2dLsmijtWKP6T+MejteD+eMuMZ87zf9dOLITzNy4ZQ5bb0Sr74MTnB8G2+NszKT +c0QWbej09+CVgI+WXTik9KveCjCHk9hNAHFiRSdLOkKEW39lt2c0Ui2cFmuqqNh7o0JMcccMyj6D +5KbvtwEwXlGjefVwaaZBRA+GsCyRxj3qrg+E +-----END CERTIFICATE----- + +e-Szigno Root CA 2017 +===================== +-----BEGIN CERTIFICATE----- +MIICQDCCAeWgAwIBAgIMAVRI7yH9l1kN9QQKMAoGCCqGSM49BAMCMHExCzAJBgNVBAYTAkhVMREw +DwYDVQQHDAhCdWRhcGVzdDEWMBQGA1UECgwNTWljcm9zZWMgTHRkLjEXMBUGA1UEYQwOVkFUSFUt +MjM1ODQ0OTcxHjAcBgNVBAMMFWUtU3ppZ25vIFJvb3QgQ0EgMjAxNzAeFw0xNzA4MjIxMjA3MDZa +Fw00MjA4MjIxMjA3MDZaMHExCzAJBgNVBAYTAkhVMREwDwYDVQQHDAhCdWRhcGVzdDEWMBQGA1UE +CgwNTWljcm9zZWMgTHRkLjEXMBUGA1UEYQwOVkFUSFUtMjM1ODQ0OTcxHjAcBgNVBAMMFWUtU3pp +Z25vIFJvb3QgQ0EgMjAxNzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABJbcPYrYsHtvxie+RJCx +s1YVe45DJH0ahFnuY2iyxl6H0BVIHqiQrb1TotreOpCmYF9oMrWGQd+HWyx7xf58etqjYzBhMA8G +A1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBSHERUI0arBeAyxr87GyZDv +vzAEwDAfBgNVHSMEGDAWgBSHERUI0arBeAyxr87GyZDvvzAEwDAKBggqhkjOPQQDAgNJADBGAiEA +tVfd14pVCzbhhkT61NlojbjcI4qKDdQvfepz7L9NbKgCIQDLpbQS+ue16M9+k/zzNY9vTlp8tLxO +svxyqltZ+efcMQ== +-----END CERTIFICATE----- + +certSIGN Root CA G2 +=================== +-----BEGIN CERTIFICATE----- +MIIFRzCCAy+gAwIBAgIJEQA0tk7GNi02MA0GCSqGSIb3DQEBCwUAMEExCzAJBgNVBAYTAlJPMRQw +EgYDVQQKEwtDRVJUU0lHTiBTQTEcMBoGA1UECxMTY2VydFNJR04gUk9PVCBDQSBHMjAeFw0xNzAy +MDYwOTI3MzVaFw00MjAyMDYwOTI3MzVaMEExCzAJBgNVBAYTAlJPMRQwEgYDVQQKEwtDRVJUU0lH +TiBTQTEcMBoGA1UECxMTY2VydFNJR04gUk9PVCBDQSBHMjCCAiIwDQYJKoZIhvcNAQEBBQADggIP +ADCCAgoCggIBAMDFdRmRfUR0dIf+DjuW3NgBFszuY5HnC2/OOwppGnzC46+CjobXXo9X69MhWf05 +N0IwvlDqtg+piNguLWkh59E3GE59kdUWX2tbAMI5Qw02hVK5U2UPHULlj88F0+7cDBrZuIt4Imfk +abBoxTzkbFpG583H+u/E7Eu9aqSs/cwoUe+StCmrqzWaTOTECMYmzPhpn+Sc8CnTXPnGFiWeI8Mg +wT0PPzhAsP6CRDiqWhqKa2NYOLQV07YRaXseVO6MGiKscpc/I1mbySKEwQdPzH/iV8oScLumZfNp +dWO9lfsbl83kqK/20U6o2YpxJM02PbyWxPFsqa7lzw1uKA2wDrXKUXt4FMMgL3/7FFXhEZn91Qqh +ngLjYl/rNUssuHLoPj1PrCy7Lobio3aP5ZMqz6WryFyNSwb/EkaseMsUBzXgqd+L6a8VTxaJW732 +jcZZroiFDsGJ6x9nxUWO/203Nit4ZoORUSs9/1F3dmKh7Gc+PoGD4FapUB8fepmrY7+EF3fxDTvf +95xhszWYijqy7DwaNz9+j5LP2RIUZNoQAhVB/0/E6xyjyfqZ90bp4RjZsbgyLcsUDFDYg2WD7rlc +z8sFWkz6GZdr1l0T08JcVLwyc6B49fFtHsufpaafItzRUZ6CeWRgKRM+o/1Pcmqr4tTluCRVLERL +iohEnMqE0yo7AgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1Ud +DgQWBBSCIS1mxteg4BXrzkwJd8RgnlRuAzANBgkqhkiG9w0BAQsFAAOCAgEAYN4auOfyYILVAzOB +ywaK8SJJ6ejqkX/GM15oGQOGO0MBzwdw5AgeZYWR5hEit/UCI46uuR59H35s5r0l1ZUa8gWmr4UC +b6741jH/JclKyMeKqdmfS0mbEVeZkkMR3rYzpMzXjWR91M08KCy0mpbqTfXERMQlqiCA2ClV9+BB +/AYm/7k29UMUA2Z44RGx2iBfRgB4ACGlHgAoYXhvqAEBj500mv/0OJD7uNGzcgbJceaBxXntC6Z5 +8hMLnPddDnskk7RI24Zf3lCGeOdA5jGokHZwYa+cNywRtYK3qq4kNFtyDGkNzVmf9nGvnAvRCjj5 +BiKDUyUM/FHE5r7iOZULJK2v0ZXkltd0ZGtxTgI8qoXzIKNDOXZbbFD+mpwUHmUUihW9o4JFWklW +atKcsWMy5WHgUyIOpwpJ6st+H6jiYoD2EEVSmAYY3qXNL3+q1Ok+CHLsIwMCPKaq2LxndD0UF/tU +Sxfj03k9bWtJySgOLnRQvwzZRjoQhsmnP+mg7H/rpXdYaXHmgwo38oZJar55CJD2AhZkPuXaTH4M +NMn5X7azKFGnpyuqSfqNZSlO42sTp5SjLVFteAxEy9/eCG/Oo2Sr05WE1LlSVHJ7liXMvGnjSG4N +0MedJ5qq+BOS3R7fY581qRY27Iy4g/Q9iY/NtBde17MXQRBdJ3NghVdJIgc= +-----END CERTIFICATE----- + +Trustwave Global Certification Authority +======================================== +-----BEGIN CERTIFICATE----- +MIIF2jCCA8KgAwIBAgIMBfcOhtpJ80Y1LrqyMA0GCSqGSIb3DQEBCwUAMIGIMQswCQYDVQQGEwJV +UzERMA8GA1UECAwISWxsaW5vaXMxEDAOBgNVBAcMB0NoaWNhZ28xITAfBgNVBAoMGFRydXN0d2F2 +ZSBIb2xkaW5ncywgSW5jLjExMC8GA1UEAwwoVHJ1c3R3YXZlIEdsb2JhbCBDZXJ0aWZpY2F0aW9u +IEF1dGhvcml0eTAeFw0xNzA4MjMxOTM0MTJaFw00MjA4MjMxOTM0MTJaMIGIMQswCQYDVQQGEwJV +UzERMA8GA1UECAwISWxsaW5vaXMxEDAOBgNVBAcMB0NoaWNhZ28xITAfBgNVBAoMGFRydXN0d2F2 +ZSBIb2xkaW5ncywgSW5jLjExMC8GA1UEAwwoVHJ1c3R3YXZlIEdsb2JhbCBDZXJ0aWZpY2F0aW9u +IEF1dGhvcml0eTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBALldUShLPDeS0YLOvR29 +zd24q88KPuFd5dyqCblXAj7mY2Hf8g+CY66j96xz0XznswuvCAAJWX/NKSqIk4cXGIDtiLK0thAf +LdZfVaITXdHG6wZWiYj+rDKd/VzDBcdu7oaJuogDnXIhhpCujwOl3J+IKMujkkkP7NAP4m1ET4Bq +stTnoApTAbqOl5F2brz81Ws25kCI1nsvXwXoLG0R8+eyvpJETNKXpP7ScoFDB5zpET71ixpZfR9o +WN0EACyW80OzfpgZdNmcc9kYvkHHNHnZ9GLCQ7mzJ7Aiy/k9UscwR7PJPrhq4ufogXBeQotPJqX+ +OsIgbrv4Fo7NDKm0G2x2EOFYeUY+VM6AqFcJNykbmROPDMjWLBz7BegIlT1lRtzuzWniTY+HKE40 +Cz7PFNm73bZQmq131BnW2hqIyE4bJ3XYsgjxroMwuREOzYfwhI0Vcnyh78zyiGG69Gm7DIwLdVcE +uE4qFC49DxweMqZiNu5m4iK4BUBjECLzMx10coos9TkpoNPnG4CELcU9402x/RpvumUHO1jsQkUm ++9jaJXLE9gCxInm943xZYkqcBW89zubWR2OZxiRvchLIrH+QtAuRcOi35hYQcRfO3gZPSEF9NUqj +ifLJS3tBEW1ntwiYTOURGa5CgNz7kAXU+FDKvuStx8KU1xad5hePrzb7AgMBAAGjQjBAMA8GA1Ud +EwEB/wQFMAMBAf8wHQYDVR0OBBYEFJngGWcNYtt2s9o9uFvo/ULSMQ6HMA4GA1UdDwEB/wQEAwIB +BjANBgkqhkiG9w0BAQsFAAOCAgEAmHNw4rDT7TnsTGDZqRKGFx6W0OhUKDtkLSGm+J1WE2pIPU/H +PinbbViDVD2HfSMF1OQc3Og4ZYbFdada2zUFvXfeuyk3QAUHw5RSn8pk3fEbK9xGChACMf1KaA0H +ZJDmHvUqoai7PF35owgLEQzxPy0QlG/+4jSHg9bP5Rs1bdID4bANqKCqRieCNqcVtgimQlRXtpla +4gt5kNdXElE1GYhBaCXUNxeEFfsBctyV3lImIJgm4nb1J2/6ADtKYdkNy1GTKv0WBpanI5ojSP5R +vbbEsLFUzt5sQa0WZ37b/TjNuThOssFgy50X31ieemKyJo90lZvkWx3SD92YHJtZuSPTMaCm/zjd +zyBP6VhWOmfD0faZmZ26NraAL4hHT4a/RDqA5Dccprrql5gR0IRiR2Qequ5AvzSxnI9O4fKSTx+O +856X3vOmeWqJcU9LJxdI/uz0UA9PSX3MReO9ekDFQdxhVicGaeVyQYHTtgGJoC86cnn+OjC/QezH +Yj6RS8fZMXZC+fc8Y+wmjHMMfRod6qh8h6jCJ3zhM0EPz8/8AKAigJ5Kp28AsEFFtyLKaEjFQqKu +3R3y4G5OBVixwJAWKqQ9EEC+j2Jjg6mcgn0tAumDMHzLJ8n9HmYAsC7TIS+OMxZsmO0QqAfWzJPP +29FpHOTKyeC2nOnOcXHebD8WpHk= +-----END CERTIFICATE----- + +Trustwave Global ECC P256 Certification Authority +================================================= +-----BEGIN CERTIFICATE----- +MIICYDCCAgegAwIBAgIMDWpfCD8oXD5Rld9dMAoGCCqGSM49BAMCMIGRMQswCQYDVQQGEwJVUzER +MA8GA1UECBMISWxsaW5vaXMxEDAOBgNVBAcTB0NoaWNhZ28xITAfBgNVBAoTGFRydXN0d2F2ZSBI +b2xkaW5ncywgSW5jLjE6MDgGA1UEAxMxVHJ1c3R3YXZlIEdsb2JhbCBFQ0MgUDI1NiBDZXJ0aWZp +Y2F0aW9uIEF1dGhvcml0eTAeFw0xNzA4MjMxOTM1MTBaFw00MjA4MjMxOTM1MTBaMIGRMQswCQYD +VQQGEwJVUzERMA8GA1UECBMISWxsaW5vaXMxEDAOBgNVBAcTB0NoaWNhZ28xITAfBgNVBAoTGFRy +dXN0d2F2ZSBIb2xkaW5ncywgSW5jLjE6MDgGA1UEAxMxVHJ1c3R3YXZlIEdsb2JhbCBFQ0MgUDI1 +NiBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABH77bOYj +43MyCMpg5lOcunSNGLB4kFKA3TjASh3RqMyTpJcGOMoNFWLGjgEqZZ2q3zSRLoHB5DOSMcT9CTqm +P62jQzBBMA8GA1UdEwEB/wQFMAMBAf8wDwYDVR0PAQH/BAUDAwcGADAdBgNVHQ4EFgQUo0EGrJBt +0UrrdaVKEJmzsaGLSvcwCgYIKoZIzj0EAwIDRwAwRAIgB+ZU2g6gWrKuEZ+Hxbb/ad4lvvigtwjz +RM4q3wghDDcCIC0mA6AFvWvR9lz4ZcyGbbOcNEhjhAnFjXca4syc4XR7 +-----END CERTIFICATE----- + +Trustwave Global ECC P384 Certification Authority +================================================= +-----BEGIN CERTIFICATE----- +MIICnTCCAiSgAwIBAgIMCL2Fl2yZJ6SAaEc7MAoGCCqGSM49BAMDMIGRMQswCQYDVQQGEwJVUzER +MA8GA1UECBMISWxsaW5vaXMxEDAOBgNVBAcTB0NoaWNhZ28xITAfBgNVBAoTGFRydXN0d2F2ZSBI +b2xkaW5ncywgSW5jLjE6MDgGA1UEAxMxVHJ1c3R3YXZlIEdsb2JhbCBFQ0MgUDM4NCBDZXJ0aWZp +Y2F0aW9uIEF1dGhvcml0eTAeFw0xNzA4MjMxOTM2NDNaFw00MjA4MjMxOTM2NDNaMIGRMQswCQYD +VQQGEwJVUzERMA8GA1UECBMISWxsaW5vaXMxEDAOBgNVBAcTB0NoaWNhZ28xITAfBgNVBAoTGFRy +dXN0d2F2ZSBIb2xkaW5ncywgSW5jLjE6MDgGA1UEAxMxVHJ1c3R3YXZlIEdsb2JhbCBFQ0MgUDM4 +NCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTB2MBAGByqGSM49AgEGBSuBBAAiA2IABGvaDXU1CDFH +Ba5FmVXxERMuSvgQMSOjfoPTfygIOiYaOs+Xgh+AtycJj9GOMMQKmw6sWASr9zZ9lCOkmwqKi6vr +/TklZvFe/oyujUF5nQlgziip04pt89ZF1PKYhDhloKNDMEEwDwYDVR0TAQH/BAUwAwEB/zAPBgNV +HQ8BAf8EBQMDBwYAMB0GA1UdDgQWBBRVqYSJ0sEyvRjLbKYHTsjnnb6CkDAKBggqhkjOPQQDAwNn +ADBkAjA3AZKXRRJ+oPM+rRk6ct30UJMDEr5E0k9BpIycnR+j9sKS50gU/k6bpZFXrsY3crsCMGcl +CrEMXu6pY5Jv5ZAL/mYiykf9ijH3g/56vxC+GCsej/YpHpRZ744hN8tRmKVuSw== +-----END CERTIFICATE----- + +NAVER Global Root Certification Authority +========================================= +-----BEGIN CERTIFICATE----- +MIIFojCCA4qgAwIBAgIUAZQwHqIL3fXFMyqxQ0Rx+NZQTQ0wDQYJKoZIhvcNAQEMBQAwaTELMAkG +A1UEBhMCS1IxJjAkBgNVBAoMHU5BVkVSIEJVU0lORVNTIFBMQVRGT1JNIENvcnAuMTIwMAYDVQQD +DClOQVZFUiBHbG9iYWwgUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw0xNzA4MTgwODU4 +NDJaFw0zNzA4MTgyMzU5NTlaMGkxCzAJBgNVBAYTAktSMSYwJAYDVQQKDB1OQVZFUiBCVVNJTkVT +UyBQTEFURk9STSBDb3JwLjEyMDAGA1UEAwwpTkFWRVIgR2xvYmFsIFJvb3QgQ2VydGlmaWNhdGlv +biBBdXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC21PGTXLVAiQqrDZBb +UGOukJR0F0Vy1ntlWilLp1agS7gvQnXp2XskWjFlqxcX0TM62RHcQDaH38dq6SZeWYp34+hInDEW ++j6RscrJo+KfziFTowI2MMtSAuXaMl3Dxeb57hHHi8lEHoSTGEq0n+USZGnQJoViAbbJAh2+g1G7 +XNr4rRVqmfeSVPc0W+m/6imBEtRTkZazkVrd/pBzKPswRrXKCAfHcXLJZtM0l/aM9BhK4dA9WkW2 +aacp+yPOiNgSnABIqKYPszuSjXEOdMWLyEz59JuOuDxp7W87UC9Y7cSw0BwbagzivESq2M0UXZR4 +Yb8ObtoqvC8MC3GmsxY/nOb5zJ9TNeIDoKAYv7vxvvTWjIcNQvcGufFt7QSUqP620wbGQGHfnZ3z +VHbOUzoBppJB7ASjjw2i1QnK1sua8e9DXcCrpUHPXFNwcMmIpi3Ua2FzUCaGYQ5fG8Ir4ozVu53B +A0K6lNpfqbDKzE0K70dpAy8i+/Eozr9dUGWokG2zdLAIx6yo0es+nPxdGoMuK8u180SdOqcXYZai +cdNwlhVNt0xz7hlcxVs+Qf6sdWA7G2POAN3aCJBitOUt7kinaxeZVL6HSuOpXgRM6xBtVNbv8ejy +YhbLgGvtPe31HzClrkvJE+2KAQHJuFFYwGY6sWZLxNUxAmLpdIQM201GLQIDAQABo0IwQDAdBgNV +HQ4EFgQU0p+I36HNLL3s9TsBAZMzJ7LrYEswDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMB +Af8wDQYJKoZIhvcNAQEMBQADggIBADLKgLOdPVQG3dLSLvCkASELZ0jKbY7gyKoNqo0hV4/GPnrK +21HUUrPUloSlWGB/5QuOH/XcChWB5Tu2tyIvCZwTFrFsDDUIbatjcu3cvuzHV+YwIHHW1xDBE1UB +jCpD5EHxzzp6U5LOogMFDTjfArsQLtk70pt6wKGm+LUx5vR1yblTmXVHIloUFcd4G7ad6Qz4G3bx +hYTeodoS76TiEJd6eN4MUZeoIUCLhr0N8F5OSza7OyAfikJW4Qsav3vQIkMsRIz75Sq0bBwcupTg +E34h5prCy8VCZLQelHsIJchxzIdFV4XTnyliIoNRlwAYl3dqmJLJfGBs32x9SuRwTMKeuB330DTH +D8z7p/8Dvq1wkNoL3chtl1+afwkyQf3NosxabUzyqkn+Zvjp2DXrDige7kgvOtB5CTh8piKCk5XQ +A76+AqAF3SAi428diDRgxuYKuQl1C/AH6GmWNcf7I4GOODm4RStDeKLRLBT/DShycpWbXgnbiUSY +qqFJu3FS8r/2/yehNq+4tneI3TqkbZs0kNwUXTC/t+sX5Ie3cdCh13cV1ELX8vMxmV2b3RZtP+oG +I/hGoiLtk/bdmuYqh7GYVPEi92tF4+KOdh2ajcQGjTa3FPOdVGm3jjzVpG2Tgbet9r1ke8LJaDmg +kpzNNIaRkPpkUZ3+/uul9XXeifdy +-----END CERTIFICATE----- + +AC RAIZ FNMT-RCM SERVIDORES SEGUROS +=================================== +-----BEGIN CERTIFICATE----- +MIICbjCCAfOgAwIBAgIQYvYybOXE42hcG2LdnC6dlTAKBggqhkjOPQQDAzB4MQswCQYDVQQGEwJF +UzERMA8GA1UECgwIRk5NVC1SQ00xDjAMBgNVBAsMBUNlcmVzMRgwFgYDVQRhDA9WQVRFUy1RMjgy +NjAwNEoxLDAqBgNVBAMMI0FDIFJBSVogRk5NVC1SQ00gU0VSVklET1JFUyBTRUdVUk9TMB4XDTE4 +MTIyMDA5MzczM1oXDTQzMTIyMDA5MzczM1oweDELMAkGA1UEBhMCRVMxETAPBgNVBAoMCEZOTVQt +UkNNMQ4wDAYDVQQLDAVDZXJlczEYMBYGA1UEYQwPVkFURVMtUTI4MjYwMDRKMSwwKgYDVQQDDCNB +QyBSQUlaIEZOTVQtUkNNIFNFUlZJRE9SRVMgU0VHVVJPUzB2MBAGByqGSM49AgEGBSuBBAAiA2IA +BPa6V1PIyqvfNkpSIeSX0oNnnvBlUdBeh8dHsVnyV0ebAAKTRBdp20LHsbI6GA60XYyzZl2hNPk2 +LEnb80b8s0RpRBNm/dfF/a82Tc4DTQdxz69qBdKiQ1oKUm8BA06Oi6NCMEAwDwYDVR0TAQH/BAUw +AwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFAG5L++/EYZg8k/QQW6rcx/n0m5JMAoGCCqG +SM49BAMDA2kAMGYCMQCuSuMrQMN0EfKVrRYj3k4MGuZdpSRea0R7/DjiT8ucRRcRTBQnJlU5dUoD +zBOQn5ICMQD6SmxgiHPz7riYYqnOK8LZiqZwMR2vsJRM60/G49HzYqc8/5MuB1xJAWdpEgJyv+c= +-----END CERTIFICATE----- + +GlobalSign Root R46 +=================== +-----BEGIN CERTIFICATE----- +MIIFWjCCA0KgAwIBAgISEdK7udcjGJ5AXwqdLdDfJWfRMA0GCSqGSIb3DQEBDAUAMEYxCzAJBgNV +BAYTAkJFMRkwFwYDVQQKExBHbG9iYWxTaWduIG52LXNhMRwwGgYDVQQDExNHbG9iYWxTaWduIFJv +b3QgUjQ2MB4XDTE5MDMyMDAwMDAwMFoXDTQ2MDMyMDAwMDAwMFowRjELMAkGA1UEBhMCQkUxGTAX +BgNVBAoTEEdsb2JhbFNpZ24gbnYtc2ExHDAaBgNVBAMTE0dsb2JhbFNpZ24gUm9vdCBSNDYwggIi +MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCsrHQy6LNl5brtQyYdpokNRbopiLKkHWPd08Es +CVeJOaFV6Wc0dwxu5FUdUiXSE2te4R2pt32JMl8Nnp8semNgQB+msLZ4j5lUlghYruQGvGIFAha/ +r6gjA7aUD7xubMLL1aa7DOn2wQL7Id5m3RerdELv8HQvJfTqa1VbkNud316HCkD7rRlr+/fKYIje +2sGP1q7Vf9Q8g+7XFkyDRTNrJ9CG0Bwta/OrffGFqfUo0q3v84RLHIf8E6M6cqJaESvWJ3En7YEt +bWaBkoe0G1h6zD8K+kZPTXhc+CtI4wSEy132tGqzZfxCnlEmIyDLPRT5ge1lFgBPGmSXZgjPjHvj +K8Cd+RTyG/FWaha/LIWFzXg4mutCagI0GIMXTpRW+LaCtfOW3T3zvn8gdz57GSNrLNRyc0NXfeD4 +12lPFzYE+cCQYDdF3uYM2HSNrpyibXRdQr4G9dlkbgIQrImwTDsHTUB+JMWKmIJ5jqSngiCNI/on +ccnfxkF0oE32kRbcRoxfKWMxWXEM2G/CtjJ9++ZdU6Z+Ffy7dXxd7Pj2Fxzsx2sZy/N78CsHpdls +eVR2bJ0cpm4O6XkMqCNqo98bMDGfsVR7/mrLZqrcZdCinkqaByFrgY/bxFn63iLABJzjqls2k+g9 +vXqhnQt2sQvHnf3PmKgGwvgqo6GDoLclcqUC4wIDAQABo0IwQDAOBgNVHQ8BAf8EBAMCAYYwDwYD +VR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUA1yrc4GHqMywptWU4jaWSf8FmSwwDQYJKoZIhvcNAQEM +BQADggIBAHx47PYCLLtbfpIrXTncvtgdokIzTfnvpCo7RGkerNlFo048p9gkUbJUHJNOxO97k4Vg +JuoJSOD1u8fpaNK7ajFxzHmuEajwmf3lH7wvqMxX63bEIaZHU1VNaL8FpO7XJqti2kM3S+LGteWy +gxk6x9PbTZ4IevPuzz5i+6zoYMzRx6Fcg0XERczzF2sUyQQCPtIkpnnpHs6i58FZFZ8d4kuaPp92 +CC1r2LpXFNqD6v6MVenQTqnMdzGxRBF6XLE+0xRFFRhiJBPSy03OXIPBNvIQtQ6IbbjhVp+J3pZm +OUdkLG5NrmJ7v2B0GbhWrJKsFjLtrWhV/pi60zTe9Mlhww6G9kuEYO4Ne7UyWHmRVSyBQ7N0H3qq +JZ4d16GLuc1CLgSkZoNNiTW2bKg2SnkheCLQQrzRQDGQob4Ez8pn7fXwgNNgyYMqIgXQBztSvwye +qiv5u+YfjyW6hY0XHgL+XVAEV8/+LbzvXMAaq7afJMbfc2hIkCwU9D9SGuTSyxTDYWnP4vkYxboz +nxSjBF25cfe1lNj2M8FawTSLfJvdkzrnE6JwYZ+vj+vYxXX4M2bUdGc6N3ec592kD3ZDZopD8p/7 +DEJ4Y9HiD2971KE9dJeFt0g5QdYg/NA6s/rob8SKunE3vouXsXgxT7PntgMTzlSdriVZzH81Xwj3 +QEUxeCp6 +-----END CERTIFICATE----- + +GlobalSign Root E46 +=================== +-----BEGIN CERTIFICATE----- +MIICCzCCAZGgAwIBAgISEdK7ujNu1LzmJGjFDYQdmOhDMAoGCCqGSM49BAMDMEYxCzAJBgNVBAYT +AkJFMRkwFwYDVQQKExBHbG9iYWxTaWduIG52LXNhMRwwGgYDVQQDExNHbG9iYWxTaWduIFJvb3Qg +RTQ2MB4XDTE5MDMyMDAwMDAwMFoXDTQ2MDMyMDAwMDAwMFowRjELMAkGA1UEBhMCQkUxGTAXBgNV +BAoTEEdsb2JhbFNpZ24gbnYtc2ExHDAaBgNVBAMTE0dsb2JhbFNpZ24gUm9vdCBFNDYwdjAQBgcq +hkjOPQIBBgUrgQQAIgNiAAScDrHPt+ieUnd1NPqlRqetMhkytAepJ8qUuwzSChDH2omwlwxwEwkB +jtjqR+q+soArzfwoDdusvKSGN+1wCAB16pMLey5SnCNoIwZD7JIvU4Tb+0cUB+hflGddyXqBPCCj +QjBAMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBQxCpCPtsad0kRL +gLWi5h+xEk8blTAKBggqhkjOPQQDAwNoADBlAjEA31SQ7Zvvi5QCkxeCmb6zniz2C5GMn0oUsfZk +vLtoURMMA/cVi4RguYv/Uo7njLwcAjA8+RHUjE7AwWHCFUyqqx0LMV87HOIAl0Qx5v5zli/altP+ +CAezNIm8BZ/3Hobui3A= +-----END CERTIFICATE----- + +GLOBALTRUST 2020 +================ +-----BEGIN CERTIFICATE----- +MIIFgjCCA2qgAwIBAgILWku9WvtPilv6ZeUwDQYJKoZIhvcNAQELBQAwTTELMAkGA1UEBhMCQVQx +IzAhBgNVBAoTGmUtY29tbWVyY2UgbW9uaXRvcmluZyBHbWJIMRkwFwYDVQQDExBHTE9CQUxUUlVT +VCAyMDIwMB4XDTIwMDIxMDAwMDAwMFoXDTQwMDYxMDAwMDAwMFowTTELMAkGA1UEBhMCQVQxIzAh +BgNVBAoTGmUtY29tbWVyY2UgbW9uaXRvcmluZyBHbWJIMRkwFwYDVQQDExBHTE9CQUxUUlVTVCAy +MDIwMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAri5WrRsc7/aVj6B3GyvTY4+ETUWi +D59bRatZe1E0+eyLinjF3WuvvcTfk0Uev5E4C64OFudBc/jbu9G4UeDLgztzOG53ig9ZYybNpyrO +VPu44sB8R85gfD+yc/LAGbaKkoc1DZAoouQVBGM+uq/ufF7MpotQsjj3QWPKzv9pj2gOlTblzLmM +CcpL3TGQlsjMH/1WljTbjhzqLL6FLmPdqqmV0/0plRPwyJiT2S0WR5ARg6I6IqIoV6Lr/sCMKKCm +fecqQjuCgGOlYx8ZzHyyZqjC0203b+J+BlHZRYQfEs4kUmSFC0iAToexIiIwquuuvuAC4EDosEKA +A1GqtH6qRNdDYfOiaxaJSaSjpCuKAsR49GiKweR6NrFvG5Ybd0mN1MkGco/PU+PcF4UgStyYJ9OR +JitHHmkHr96i5OTUawuzXnzUJIBHKWk7buis/UDr2O1xcSvy6Fgd60GXIsUf1DnQJ4+H4xj04KlG +DfV0OoIu0G4skaMxXDtG6nsEEFZegB31pWXogvziB4xiRfUg3kZwhqG8k9MedKZssCz3AwyIDMvU +clOGvGBG85hqwvG/Q/lwIHfKN0F5VVJjjVsSn8VoxIidrPIwq7ejMZdnrY8XD2zHc+0klGvIg5rQ +mjdJBKuxFshsSUktq6HQjJLyQUp5ISXbY9e2nKd+Qmn7OmMCAwEAAaNjMGEwDwYDVR0TAQH/BAUw +AwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFNwuH9FhN3nkq9XVsxJxaD1qaJwiMB8GA1Ud +IwQYMBaAFNwuH9FhN3nkq9XVsxJxaD1qaJwiMA0GCSqGSIb3DQEBCwUAA4ICAQCR8EICaEDuw2jA +VC/f7GLDw56KoDEoqoOOpFaWEhCGVrqXctJUMHytGdUdaG/7FELYjQ7ztdGl4wJCXtzoRlgHNQIw +4Lx0SsFDKv/bGtCwr2zD/cuz9X9tAy5ZVp0tLTWMstZDFyySCstd6IwPS3BD0IL/qMy/pJTAvoe9 +iuOTe8aPmxadJ2W8esVCgmxcB9CpwYhgROmYhRZf+I/KARDOJcP5YBugxZfD0yyIMaK9MOzQ0MAS +8cE54+X1+NZK3TTN+2/BT+MAi1bikvcoskJ3ciNnxz8RFbLEAwW+uxF7Cr+obuf/WEPPm2eggAe2 +HcqtbepBEX4tdJP7wry+UUTF72glJ4DjyKDUEuzZpTcdN3y0kcra1LGWge9oXHYQSa9+pTeAsRxS +vTOBTI/53WXZFM2KJVj04sWDpQmQ1GwUY7VA3+vA/MRYfg0UFodUJ25W5HCEuGwyEn6CMUO+1918 +oa2u1qsgEu8KwxCMSZY13At1XrFP1U80DhEgB3VDRemjEdqso5nCtnkn4rnvyOL2NSl6dPrFf4IF +YqYK6miyeUcGbvJXqBUzxvd4Sj1Ce2t+/vdG6tHrju+IaFvowdlxfv1k7/9nR4hYJS8+hge9+6jl +gqispdNpQ80xiEmEU5LAsTkbOYMBMMTyqfrQA71yN2BWHzZ8vTmR9W0Nv3vXkg== +-----END CERTIFICATE----- + +ANF Secure Server Root CA +========================= +-----BEGIN CERTIFICATE----- +MIIF7zCCA9egAwIBAgIIDdPjvGz5a7EwDQYJKoZIhvcNAQELBQAwgYQxEjAQBgNVBAUTCUc2MzI4 +NzUxMDELMAkGA1UEBhMCRVMxJzAlBgNVBAoTHkFORiBBdXRvcmlkYWQgZGUgQ2VydGlmaWNhY2lv +bjEUMBIGA1UECxMLQU5GIENBIFJhaXoxIjAgBgNVBAMTGUFORiBTZWN1cmUgU2VydmVyIFJvb3Qg +Q0EwHhcNMTkwOTA0MTAwMDM4WhcNMzkwODMwMTAwMDM4WjCBhDESMBAGA1UEBRMJRzYzMjg3NTEw +MQswCQYDVQQGEwJFUzEnMCUGA1UEChMeQU5GIEF1dG9yaWRhZCBkZSBDZXJ0aWZpY2FjaW9uMRQw +EgYDVQQLEwtBTkYgQ0EgUmFpejEiMCAGA1UEAxMZQU5GIFNlY3VyZSBTZXJ2ZXIgUm9vdCBDQTCC +AiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBANvrayvmZFSVgpCjcqQZAZ2cC4Ffc0m6p6zz +BE57lgvsEeBbphzOG9INgxwruJ4dfkUyYA8H6XdYfp9qyGFOtibBTI3/TO80sh9l2Ll49a2pcbnv +T1gdpd50IJeh7WhM3pIXS7yr/2WanvtH2Vdy8wmhrnZEE26cLUQ5vPnHO6RYPUG9tMJJo8gN0pcv +B2VSAKduyK9o7PQUlrZXH1bDOZ8rbeTzPvY1ZNoMHKGESy9LS+IsJJ1tk0DrtSOOMspvRdOoiXse +zx76W0OLzc2oD2rKDF65nkeP8Nm2CgtYZRczuSPkdxl9y0oukntPLxB3sY0vaJxizOBQ+OyRp1RM +VwnVdmPF6GUe7m1qzwmd+nxPrWAI/VaZDxUse6mAq4xhj0oHdkLePfTdsiQzW7i1o0TJrH93PB0j +7IKppuLIBkwC/qxcmZkLLxCKpvR/1Yd0DVlJRfbwcVw5Kda/SiOL9V8BY9KHcyi1Swr1+KuCLH5z +JTIdC2MKF4EA/7Z2Xue0sUDKIbvVgFHlSFJnLNJhiQcND85Cd8BEc5xEUKDbEAotlRyBr+Qc5RQe +8TZBAQIvfXOn3kLMTOmJDVb3n5HUA8ZsyY/b2BzgQJhdZpmYgG4t/wHFzstGH6wCxkPmrqKEPMVO +Hj1tyRRM4y5Bu8o5vzY8KhmqQYdOpc5LMnndkEl/AgMBAAGjYzBhMB8GA1UdIwQYMBaAFJxf0Gxj +o1+TypOYCK2Mh6UsXME3MB0GA1UdDgQWBBScX9BsY6Nfk8qTmAitjIelLFzBNzAOBgNVHQ8BAf8E +BAMCAYYwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAgEATh65isagmD9uw2nAalxJ +UqzLK114OMHVVISfk/CHGT0sZonrDUL8zPB1hT+L9IBdeeUXZ701guLyPI59WzbLWoAAKfLOKyzx +j6ptBZNscsdW699QIyjlRRA96Gejrw5VD5AJYu9LWaL2U/HANeQvwSS9eS9OICI7/RogsKQOLHDt +dD+4E5UGUcjohybKpFtqFiGS3XNgnhAY3jyB6ugYw3yJ8otQPr0R4hUDqDZ9MwFsSBXXiJCZBMXM +5gf0vPSQ7RPi6ovDj6MzD8EpTBNO2hVWcXNyglD2mjN8orGoGjR0ZVzO0eurU+AagNjqOknkJjCb +5RyKqKkVMoaZkgoQI1YS4PbOTOK7vtuNknMBZi9iPrJyJ0U27U1W45eZ/zo1PqVUSlJZS2Db7v54 +EX9K3BR5YLZrZAPbFYPhor72I5dQ8AkzNqdxliXzuUJ92zg/LFis6ELhDtjTO0wugumDLmsx2d1H +hk9tl5EuT+IocTUW0fJz/iUrB0ckYyfI+PbZa/wSMVYIwFNCr5zQM378BvAxRAMU8Vjq8moNqRGy +g77FGr8H6lnco4g175x2MjxNBiLOFeXdntiP2t7SxDnlF4HPOEfrf4htWRvfn0IUrn7PqLBmZdo3 +r5+qPeoott7VMVgWglvquxl1AnMaykgaIZOQCo6ThKd9OyMYkomgjaw= +-----END CERTIFICATE----- + +Certum EC-384 CA +================ +-----BEGIN CERTIFICATE----- +MIICZTCCAeugAwIBAgIQeI8nXIESUiClBNAt3bpz9DAKBggqhkjOPQQDAzB0MQswCQYDVQQGEwJQ +TDEhMB8GA1UEChMYQXNzZWNvIERhdGEgU3lzdGVtcyBTLkEuMScwJQYDVQQLEx5DZXJ0dW0gQ2Vy +dGlmaWNhdGlvbiBBdXRob3JpdHkxGTAXBgNVBAMTEENlcnR1bSBFQy0zODQgQ0EwHhcNMTgwMzI2 +MDcyNDU0WhcNNDMwMzI2MDcyNDU0WjB0MQswCQYDVQQGEwJQTDEhMB8GA1UEChMYQXNzZWNvIERh +dGEgU3lzdGVtcyBTLkEuMScwJQYDVQQLEx5DZXJ0dW0gQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkx +GTAXBgNVBAMTEENlcnR1bSBFQy0zODQgQ0EwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAATEKI6rGFtq +vm5kN2PkzeyrOvfMobgOgknXhimfoZTy42B4mIF4Bk3y7JoOV2CDn7TmFy8as10CW4kjPMIRBSqn +iBMY81CE1700LCeJVf/OTOffph8oxPBUw7l8t1Ot68KjQjBAMA8GA1UdEwEB/wQFMAMBAf8wHQYD +VR0OBBYEFI0GZnQkdjrzife81r1HfS+8EF9LMA4GA1UdDwEB/wQEAwIBBjAKBggqhkjOPQQDAwNo +ADBlAjADVS2m5hjEfO/JUG7BJw+ch69u1RsIGL2SKcHvlJF40jocVYli5RsJHrpka/F2tNQCMQC0 +QoSZ/6vnnvuRlydd3LBbMHHOXjgaatkl5+r3YZJW+OraNsKHZZYuciUvf9/DE8k= +-----END CERTIFICATE----- + +Certum Trusted Root CA +====================== +-----BEGIN CERTIFICATE----- +MIIFwDCCA6igAwIBAgIQHr9ZULjJgDdMBvfrVU+17TANBgkqhkiG9w0BAQ0FADB6MQswCQYDVQQG +EwJQTDEhMB8GA1UEChMYQXNzZWNvIERhdGEgU3lzdGVtcyBTLkEuMScwJQYDVQQLEx5DZXJ0dW0g +Q2VydGlmaWNhdGlvbiBBdXRob3JpdHkxHzAdBgNVBAMTFkNlcnR1bSBUcnVzdGVkIFJvb3QgQ0Ew +HhcNMTgwMzE2MTIxMDEzWhcNNDMwMzE2MTIxMDEzWjB6MQswCQYDVQQGEwJQTDEhMB8GA1UEChMY +QXNzZWNvIERhdGEgU3lzdGVtcyBTLkEuMScwJQYDVQQLEx5DZXJ0dW0gQ2VydGlmaWNhdGlvbiBB +dXRob3JpdHkxHzAdBgNVBAMTFkNlcnR1bSBUcnVzdGVkIFJvb3QgQ0EwggIiMA0GCSqGSIb3DQEB +AQUAA4ICDwAwggIKAoICAQDRLY67tzbqbTeRn06TpwXkKQMlzhyC93yZn0EGze2jusDbCSzBfN8p +fktlL5On1AFrAygYo9idBcEq2EXxkd7fO9CAAozPOA/qp1x4EaTByIVcJdPTsuclzxFUl6s1wB52 +HO8AU5853BSlLCIls3Jy/I2z5T4IHhQqNwuIPMqw9MjCoa68wb4pZ1Xi/K1ZXP69VyywkI3C7Te2 +fJmItdUDmj0VDT06qKhF8JVOJVkdzZhpu9PMMsmN74H+rX2Ju7pgE8pllWeg8xn2A1bUatMn4qGt +g/BKEiJ3HAVz4hlxQsDsdUaakFjgao4rpUYwBI4Zshfjvqm6f1bxJAPXsiEodg42MEx51UGamqi4 +NboMOvJEGyCI98Ul1z3G4z5D3Yf+xOr1Uz5MZf87Sst4WmsXXw3Hw09Omiqi7VdNIuJGmj8PkTQk +fVXjjJU30xrwCSss0smNtA0Aq2cpKNgB9RkEth2+dv5yXMSFytKAQd8FqKPVhJBPC/PgP5sZ0jeJ +P/J7UhyM9uH3PAeXjA6iWYEMspA90+NZRu0PqafegGtaqge2Gcu8V/OXIXoMsSt0Puvap2ctTMSY +njYJdmZm/Bo/6khUHL4wvYBQv3y1zgD2DGHZ5yQD4OMBgQ692IU0iL2yNqh7XAjlRICMb/gv1SHK +HRzQ+8S1h9E6Tsd2tTVItQIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBSM+xx1 +vALTn04uSNn5YFSqxLNP+jAOBgNVHQ8BAf8EBAMCAQYwDQYJKoZIhvcNAQENBQADggIBAEii1QAL +LtA/vBzVtVRJHlpr9OTy4EA34MwUe7nJ+jW1dReTagVphZzNTxl4WxmB82M+w85bj/UvXgF2Ez8s +ALnNllI5SW0ETsXpD4YN4fqzX4IS8TrOZgYkNCvozMrnadyHncI013nR03e4qllY/p0m+jiGPp2K +h2RX5Rc64vmNueMzeMGQ2Ljdt4NR5MTMI9UGfOZR0800McD2RrsLrfw9EAUqO0qRJe6M1ISHgCq8 +CYyqOhNf6DR5UMEQGfnTKB7U0VEwKbOukGfWHwpjscWpxkIxYxeU72nLL/qMFH3EQxiJ2fAyQOaA +4kZf5ePBAFmo+eggvIksDkc0C+pXwlM2/KfUrzHN/gLldfq5Jwn58/U7yn2fqSLLiMmq0Uc9Nneo +WWRrJ8/vJ8HjJLWG965+Mk2weWjROeiQWMODvA8s1pfrzgzhIMfatz7DP78v3DSk+yshzWePS/Tj +6tQ/50+6uaWTRRxmHyH6ZF5v4HaUMst19W7l9o/HuKTMqJZ9ZPskWkoDbGs4xugDQ5r3V7mzKWmT +OPQD8rv7gmsHINFSH5pkAnuYZttcTVoP0ISVoDwUQwbKytu4QTbaakRnh6+v40URFWkIsr4WOZck +bxJF0WddCajJFdr60qZfE2Efv4WstK2tBZQIgx51F9NxO5NQI1mg7TyRVJ12AMXDuDjb +-----END CERTIFICATE----- + +TunTrust Root CA +================ +-----BEGIN CERTIFICATE----- +MIIFszCCA5ugAwIBAgIUEwLV4kBMkkaGFmddtLu7sms+/BMwDQYJKoZIhvcNAQELBQAwYTELMAkG +A1UEBhMCVE4xNzA1BgNVBAoMLkFnZW5jZSBOYXRpb25hbGUgZGUgQ2VydGlmaWNhdGlvbiBFbGVj +dHJvbmlxdWUxGTAXBgNVBAMMEFR1blRydXN0IFJvb3QgQ0EwHhcNMTkwNDI2MDg1NzU2WhcNNDQw +NDI2MDg1NzU2WjBhMQswCQYDVQQGEwJUTjE3MDUGA1UECgwuQWdlbmNlIE5hdGlvbmFsZSBkZSBD +ZXJ0aWZpY2F0aW9uIEVsZWN0cm9uaXF1ZTEZMBcGA1UEAwwQVHVuVHJ1c3QgUm9vdCBDQTCCAiIw +DQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAMPN0/y9BFPdDCA61YguBUtB9YOCfvdZn56eY+hz +2vYGqU8ftPkLHzmMmiDQfgbU7DTZhrx1W4eI8NLZ1KMKsmwb60ksPqxd2JQDoOw05TDENX37Jk0b +bjBU2PWARZw5rZzJJQRNmpA+TkBuimvNKWfGzC3gdOgFVwpIUPp6Q9p+7FuaDmJ2/uqdHYVy7BG7 +NegfJ7/Boce7SBbdVtfMTqDhuazb1YMZGoXRlJfXyqNlC/M4+QKu3fZnz8k/9YosRxqZbwUN/dAd +gjH8KcwAWJeRTIAAHDOFli/LQcKLEITDCSSJH7UP2dl3RxiSlGBcx5kDPP73lad9UKGAwqmDrViW +VSHbhlnUr8a83YFuB9tgYv7sEG7aaAH0gxupPqJbI9dkxt/con3YS7qC0lH4Zr8GRuR5KiY2eY8f +Tpkdso8MDhz/yV3A/ZAQprE38806JG60hZC/gLkMjNWb1sjxVj8agIl6qeIbMlEsPvLfe/ZdeikZ +juXIvTZxi11Mwh0/rViizz1wTaZQmCXcI/m4WEEIcb9PuISgjwBUFfyRbVinljvrS5YnzWuioYas +DXxU5mZMZl+QviGaAkYt5IPCgLnPSz7ofzwB7I9ezX/SKEIBlYrilz0QIX32nRzFNKHsLA4KUiwS +VXAkPcvCFDVDXSdOvsC9qnyW5/yeYa1E0wCXAgMBAAGjYzBhMB0GA1UdDgQWBBQGmpsfU33x9aTI +04Y+oXNZtPdEITAPBgNVHRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFAaamx9TffH1pMjThj6hc1m0 +90QhMA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAgEAqgVutt0Vyb+zxiD2BkewhpMl +0425yAA/l/VSJ4hxyXT968pk21vvHl26v9Hr7lxpuhbI87mP0zYuQEkHDVneixCwSQXi/5E/S7fd +Ao74gShczNxtr18UnH1YeA32gAm56Q6XKRm4t+v4FstVEuTGfbvE7Pi1HE4+Z7/FXxttbUcoqgRY +YdZ2vyJ/0Adqp2RT8JeNnYA/u8EH22Wv5psymsNUk8QcCMNE+3tjEUPRahphanltkE8pjkcFwRJp +adbGNjHh/PqAulxPxOu3Mqz4dWEX1xAZufHSCe96Qp1bWgvUxpVOKs7/B9dPfhgGiPEZtdmYu65x +xBzndFlY7wyJz4sfdZMaBBSSSFCp61cpABbjNhzI+L/wM9VBD8TMPN3pM0MBkRArHtG5Xc0yGYuP +jCB31yLEQtyEFpslbei0VXF/sHyz03FJuc9SpAQ/3D2gu68zngowYI7bnV2UqL1g52KAdoGDDIzM +MEZJ4gzSqK/rYXHv5yJiqfdcZGyfFoxnNidF9Ql7v/YQCvGwjVRDjAS6oz/v4jXH+XTgbzRB0L9z +ZVcg+ZtnemZoJE6AZb0QmQZZ8mWvuMZHu/2QeItBcy6vVR/cO5JyboTT0GFMDcx2V+IthSIVNg3r +AZ3r2OvEhJn7wAzMMujjd9qDRIueVSjAi1jTkD5OGwDxFa2DK5o= +-----END CERTIFICATE----- + +HARICA TLS RSA Root CA 2021 +=========================== +-----BEGIN CERTIFICATE----- +MIIFpDCCA4ygAwIBAgIQOcqTHO9D88aOk8f0ZIk4fjANBgkqhkiG9w0BAQsFADBsMQswCQYDVQQG +EwJHUjE3MDUGA1UECgwuSGVsbGVuaWMgQWNhZGVtaWMgYW5kIFJlc2VhcmNoIEluc3RpdHV0aW9u +cyBDQTEkMCIGA1UEAwwbSEFSSUNBIFRMUyBSU0EgUm9vdCBDQSAyMDIxMB4XDTIxMDIxOTEwNTUz +OFoXDTQ1MDIxMzEwNTUzN1owbDELMAkGA1UEBhMCR1IxNzA1BgNVBAoMLkhlbGxlbmljIEFjYWRl +bWljIGFuZCBSZXNlYXJjaCBJbnN0aXR1dGlvbnMgQ0ExJDAiBgNVBAMMG0hBUklDQSBUTFMgUlNB +IFJvb3QgQ0EgMjAyMTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAIvC569lmwVnlskN +JLnQDmT8zuIkGCyEf3dRywQRNrhe7Wlxp57kJQmXZ8FHws+RFjZiPTgE4VGC/6zStGndLuwRo0Xu +a2s7TL+MjaQenRG56Tj5eg4MmOIjHdFOY9TnuEFE+2uva9of08WRiFukiZLRgeaMOVig1mlDqa2Y +Ulhu2wr7a89o+uOkXjpFc5gH6l8Cct4MpbOfrqkdtx2z/IpZ525yZa31MJQjB/OCFks1mJxTuy/K +5FrZx40d/JiZ+yykgmvwKh+OC19xXFyuQnspiYHLA6OZyoieC0AJQTPb5lh6/a6ZcMBaD9YThnEv +dmn8kN3bLW7R8pv1GmuebxWMevBLKKAiOIAkbDakO/IwkfN4E8/BPzWr8R0RI7VDIp4BkrcYAuUR +0YLbFQDMYTfBKnya4dC6s1BG7oKsnTH4+yPiAwBIcKMJJnkVU2DzOFytOOqBAGMUuTNe3QvboEUH +GjMJ+E20pwKmafTCWQWIZYVWrkvL4N48fS0ayOn7H6NhStYqE613TBoYm5EPWNgGVMWX+Ko/IIqm +haZ39qb8HOLubpQzKoNQhArlT4b4UEV4AIHrW2jjJo3Me1xR9BQsQL4aYB16cmEdH2MtiKrOokWQ +CPxrvrNQKlr9qEgYRtaQQJKQCoReaDH46+0N0x3GfZkYVVYnZS6NRcUk7M7jAgMBAAGjQjBAMA8G +A1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFApII6ZgpJIKM+qTW8VX6iVNvRLuMA4GA1UdDwEB/wQE +AwIBhjANBgkqhkiG9w0BAQsFAAOCAgEAPpBIqm5iFSVmewzVjIuJndftTgfvnNAUX15QvWiWkKQU +EapobQk1OUAJ2vQJLDSle1mESSmXdMgHHkdt8s4cUCbjnj1AUz/3f5Z2EMVGpdAgS1D0NTsY9FVq +QRtHBmg8uwkIYtlfVUKqrFOFrJVWNlar5AWMxajaH6NpvVMPxP/cyuN+8kyIhkdGGvMA9YCRotxD +QpSbIPDRzbLrLFPCU3hKTwSUQZqPJzLB5UkZv/HywouoCjkxKLR9YjYsTewfM7Z+d21+UPCfDtcR +j88YxeMn/ibvBZ3PzzfF0HvaO7AWhAw6k9a+F9sPPg4ZeAnHqQJyIkv3N3a6dcSFA1pj1bF1BcK5 +vZStjBWZp5N99sXzqnTPBIWUmAD04vnKJGW/4GKvyMX6ssmeVkjaef2WdhW+o45WxLM0/L5H9MG0 +qPzVMIho7suuyWPEdr6sOBjhXlzPrjoiUevRi7PzKzMHVIf6tLITe7pTBGIBnfHAT+7hOtSLIBD6 +Alfm78ELt5BGnBkpjNxvoEppaZS3JGWg/6w/zgH7IS79aPib8qXPMThcFarmlwDB31qlpzmq6YR/ +PFGoOtmUW4y/Twhx5duoXNTSpv4Ao8YWxw/ogM4cKGR0GQjTQuPOAF1/sdwTsOEFy9EgqoZ0njnn +kf3/W9b3raYvAwtt41dU63ZTGI0RmLo= +-----END CERTIFICATE----- + +HARICA TLS ECC Root CA 2021 +=========================== +-----BEGIN CERTIFICATE----- +MIICVDCCAdugAwIBAgIQZ3SdjXfYO2rbIvT/WeK/zjAKBggqhkjOPQQDAzBsMQswCQYDVQQGEwJH +UjE3MDUGA1UECgwuSGVsbGVuaWMgQWNhZGVtaWMgYW5kIFJlc2VhcmNoIEluc3RpdHV0aW9ucyBD +QTEkMCIGA1UEAwwbSEFSSUNBIFRMUyBFQ0MgUm9vdCBDQSAyMDIxMB4XDTIxMDIxOTExMDExMFoX +DTQ1MDIxMzExMDEwOVowbDELMAkGA1UEBhMCR1IxNzA1BgNVBAoMLkhlbGxlbmljIEFjYWRlbWlj +IGFuZCBSZXNlYXJjaCBJbnN0aXR1dGlvbnMgQ0ExJDAiBgNVBAMMG0hBUklDQSBUTFMgRUNDIFJv +b3QgQ0EgMjAyMTB2MBAGByqGSM49AgEGBSuBBAAiA2IABDgI/rGgltJ6rK9JOtDA4MM7KKrxcm1l +AEeIhPyaJmuqS7psBAqIXhfyVYf8MLA04jRYVxqEU+kw2anylnTDUR9YSTHMmE5gEYd103KUkE+b +ECUqqHgtvpBBWJAVcqeht6NCMEAwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUyRtTgRL+BNUW +0aq8mm+3oJUZbsowDgYDVR0PAQH/BAQDAgGGMAoGCCqGSM49BAMDA2cAMGQCMBHervjcToiwqfAi +rcJRQO9gcS3ujwLEXQNwSaSS6sUUiHCm0w2wqsosQJz76YJumgIwK0eaB8bRwoF8yguWGEEbo/Qw +CZ61IygNnxS2PFOiTAZpffpskcYqSUXm7LcT4Tps +-----END CERTIFICATE----- + +Autoridad de Certificacion Firmaprofesional CIF A62634068 +========================================================= +-----BEGIN CERTIFICATE----- +MIIGFDCCA/ygAwIBAgIIG3Dp0v+ubHEwDQYJKoZIhvcNAQELBQAwUTELMAkGA1UEBhMCRVMxQjBA +BgNVBAMMOUF1dG9yaWRhZCBkZSBDZXJ0aWZpY2FjaW9uIEZpcm1hcHJvZmVzaW9uYWwgQ0lGIEE2 +MjYzNDA2ODAeFw0xNDA5MjMxNTIyMDdaFw0zNjA1MDUxNTIyMDdaMFExCzAJBgNVBAYTAkVTMUIw +QAYDVQQDDDlBdXRvcmlkYWQgZGUgQ2VydGlmaWNhY2lvbiBGaXJtYXByb2Zlc2lvbmFsIENJRiBB +NjI2MzQwNjgwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDKlmuO6vj78aI14H9M2uDD +Utd9thDIAl6zQyrET2qyyhxdKJp4ERppWVevtSBC5IsP5t9bpgOSL/UR5GLXMnE42QQMcas9UX4P +B99jBVzpv5RvwSmCwLTaUbDBPLutN0pcyvFLNg4kq7/DhHf9qFD0sefGL9ItWY16Ck6WaVICqjaY +7Pz6FIMMNx/Jkjd/14Et5cS54D40/mf0PmbR0/RAz15iNA9wBj4gGFrO93IbJWyTdBSTo3OxDqqH +ECNZXyAFGUftaI6SEspd/NYrspI8IM/hX68gvqB2f3bl7BqGYTM+53u0P6APjqK5am+5hyZvQWyI +plD9amML9ZMWGxmPsu2bm8mQ9QEM3xk9Dz44I8kvjwzRAv4bVdZO0I08r0+k8/6vKtMFnXkIoctX +MbScyJCyZ/QYFpM6/EfY0XiWMR+6KwxfXZmtY4laJCB22N/9q06mIqqdXuYnin1oKaPnirjaEbsX +LZmdEyRG98Xi2J+Of8ePdG1asuhy9azuJBCtLxTa/y2aRnFHvkLfuwHb9H/TKI8xWVvTyQKmtFLK +bpf7Q8UIJm+K9Lv9nyiqDdVF8xM6HdjAeI9BZzwelGSuewvF6NkBiDkal4ZkQdU7hwxu+g/GvUgU +vzlN1J5Bto+WHWOWk9mVBngxaJ43BjuAiUVhOSPHG0SjFeUc+JIwuwIDAQABo4HvMIHsMB0GA1Ud +DgQWBBRlzeurNR4APn7VdMActHNHDhpkLzASBgNVHRMBAf8ECDAGAQH/AgEBMIGmBgNVHSAEgZ4w +gZswgZgGBFUdIAAwgY8wLwYIKwYBBQUHAgEWI2h0dHA6Ly93d3cuZmlybWFwcm9mZXNpb25hbC5j +b20vY3BzMFwGCCsGAQUFBwICMFAeTgBQAGEAcwBlAG8AIABkAGUAIABsAGEAIABCAG8AbgBhAG4A +bwB2AGEAIAA0ADcAIABCAGEAcgBjAGUAbABvAG4AYQAgADAAOAAwADEANzAOBgNVHQ8BAf8EBAMC +AQYwDQYJKoZIhvcNAQELBQADggIBAHSHKAIrdx9miWTtj3QuRhy7qPj4Cx2Dtjqn6EWKB7fgPiDL +4QjbEwj4KKE1soCzC1HA01aajTNFSa9J8OA9B3pFE1r/yJfY0xgsfZb43aJlQ3CTkBW6kN/oGbDb +LIpgD7dvlAceHabJhfa9NPhAeGIQcDq+fUs5gakQ1JZBu/hfHAsdCPKxsIl68veg4MSPi3i1O1il +I45PVf42O+AMt8oqMEEgtIDNrvx2ZnOorm7hfNoD6JQg5iKj0B+QXSBTFCZX2lSX3xZEEAEeiGaP +cjiT3SC3NL7X8e5jjkd5KAb881lFJWAiMxujX6i6KtoaPc1A6ozuBRWV1aUsIC+nmCjuRfzxuIgA +LI9C2lHVnOUTaHFFQ4ueCyE8S1wF3BqfmI7avSKecs2tCsvMo2ebKHTEm9caPARYpoKdrcd7b/+A +lun4jWq9GJAd/0kakFI3ky88Al2CdgtR5xbHV/g4+afNmyJU72OwFW1TZQNKXkqgsqeOSQBZONXH +9IBk9W6VULgRfhVwOEqwf9DEMnDAGf/JOC0ULGb0QkTmVXYbgBVX/8Cnp6o5qtjTcNAuuuuUavpf +NIbnYrX9ivAwhZTJryQCL2/W3Wf+47BVTwSYT6RBVuKT0Gro1vP7ZeDOdcQxWQzugsgMYDNKGbqE +ZycPvEJdvSRUDewdcAZfpLz6IHxV +-----END CERTIFICATE----- + +vTrus ECC Root CA +================= +-----BEGIN CERTIFICATE----- +MIICDzCCAZWgAwIBAgIUbmq8WapTvpg5Z6LSa6Q75m0c1towCgYIKoZIzj0EAwMwRzELMAkGA1UE +BhMCQ04xHDAaBgNVBAoTE2lUcnVzQ2hpbmEgQ28uLEx0ZC4xGjAYBgNVBAMTEXZUcnVzIEVDQyBS +b290IENBMB4XDTE4MDczMTA3MjY0NFoXDTQzMDczMTA3MjY0NFowRzELMAkGA1UEBhMCQ04xHDAa +BgNVBAoTE2lUcnVzQ2hpbmEgQ28uLEx0ZC4xGjAYBgNVBAMTEXZUcnVzIEVDQyBSb290IENBMHYw +EAYHKoZIzj0CAQYFK4EEACIDYgAEZVBKrox5lkqqHAjDo6LN/llWQXf9JpRCux3NCNtzslt188+c +ToL0v/hhJoVs1oVbcnDS/dtitN9Ti72xRFhiQgnH+n9bEOf+QP3A2MMrMudwpremIFUde4BdS49n +TPEQo0IwQDAdBgNVHQ4EFgQUmDnNvtiyjPeyq+GtJK97fKHbH88wDwYDVR0TAQH/BAUwAwEB/zAO +BgNVHQ8BAf8EBAMCAQYwCgYIKoZIzj0EAwMDaAAwZQIwV53dVvHH4+m4SVBrm2nDb+zDfSXkV5UT +QJtS0zvzQBm8JsctBp61ezaf9SXUY2sAAjEA6dPGnlaaKsyh2j/IZivTWJwghfqrkYpwcBE4YGQL +YgmRWAD5Tfs0aNoJrSEGGJTO +-----END CERTIFICATE----- + +vTrus Root CA +============= +-----BEGIN CERTIFICATE----- +MIIFVjCCAz6gAwIBAgIUQ+NxE9izWRRdt86M/TX9b7wFjUUwDQYJKoZIhvcNAQELBQAwQzELMAkG +A1UEBhMCQ04xHDAaBgNVBAoTE2lUcnVzQ2hpbmEgQ28uLEx0ZC4xFjAUBgNVBAMTDXZUcnVzIFJv +b3QgQ0EwHhcNMTgwNzMxMDcyNDA1WhcNNDMwNzMxMDcyNDA1WjBDMQswCQYDVQQGEwJDTjEcMBoG +A1UEChMTaVRydXNDaGluYSBDby4sTHRkLjEWMBQGA1UEAxMNdlRydXMgUm9vdCBDQTCCAiIwDQYJ +KoZIhvcNAQEBBQADggIPADCCAgoCggIBAL1VfGHTuB0EYgWgrmy3cLRB6ksDXhA/kFocizuwZots +SKYcIrrVQJLuM7IjWcmOvFjai57QGfIvWcaMY1q6n6MLsLOaXLoRuBLpDLvPbmyAhykUAyyNJJrI +ZIO1aqwTLDPxn9wsYTwaP3BVm60AUn/PBLn+NvqcwBauYv6WTEN+VRS+GrPSbcKvdmaVayqwlHeF +XgQPYh1jdfdr58tbmnDsPmcF8P4HCIDPKNsFxhQnL4Z98Cfe/+Z+M0jnCx5Y0ScrUw5XSmXX+6KA +YPxMvDVTAWqXcoKv8R1w6Jz1717CbMdHflqUhSZNO7rrTOiwCcJlwp2dCZtOtZcFrPUGoPc2BX70 +kLJrxLT5ZOrpGgrIDajtJ8nU57O5q4IikCc9Kuh8kO+8T/3iCiSn3mUkpF3qwHYw03dQ+A0Em5Q2 +AXPKBlim0zvc+gRGE1WKyURHuFE5Gi7oNOJ5y1lKCn+8pu8fA2dqWSslYpPZUxlmPCdiKYZNpGvu +/9ROutW04o5IWgAZCfEF2c6Rsffr6TlP9m8EQ5pV9T4FFL2/s1m02I4zhKOQUqqzApVg+QxMaPnu +1RcN+HFXtSXkKe5lXa/R7jwXC1pDxaWG6iSe4gUH3DRCEpHWOXSuTEGC2/KmSNGzm/MzqvOmwMVO +9fSddmPmAsYiS8GVP1BkLFTltvA8Kc9XAgMBAAGjQjBAMB0GA1UdDgQWBBRUYnBj8XWEQ1iO0RYg +scasGrz2iTAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQsFAAOC +AgEAKbqSSaet8PFww+SX8J+pJdVrnjT+5hpk9jprUrIQeBqfTNqK2uwcN1LgQkv7bHbKJAs5EhWd +nxEt/Hlk3ODg9d3gV8mlsnZwUKT+twpw1aA08XXXTUm6EdGz2OyC/+sOxL9kLX1jbhd47F18iMjr +jld22VkE+rxSH0Ws8HqA7Oxvdq6R2xCOBNyS36D25q5J08FsEhvMKar5CKXiNxTKsbhm7xqC5PD4 +8acWabfbqWE8n/Uxy+QARsIvdLGx14HuqCaVvIivTDUHKgLKeBRtRytAVunLKmChZwOgzoy8sHJn +xDHO2zTlJQNgJXtxmOTAGytfdELSS8VZCAeHvsXDf+eW2eHcKJfWjwXj9ZtOyh1QRwVTsMo554Wg +icEFOwE30z9J4nfrI8iIZjs9OXYhRvHsXyO466JmdXTBQPfYaJqT4i2pLr0cox7IdMakLXogqzu4 +sEb9b91fUlV1YvCXoHzXOP0l382gmxDPi7g4Xl7FtKYCNqEeXxzP4padKar9mK5S4fNBUvupLnKW +nyfjqnN9+BojZns7q2WwMgFLFT49ok8MKzWixtlnEjUwzXYuFrOZnk1PTi07NEPhmg4NpGaXutIc +SkwsKouLgU9xGqndXHt7CMUADTdA43x7VF8vhV929vensBxXVsFy6K2ir40zSbofitzmdHxghm+H +l3s= +-----END CERTIFICATE----- + +ISRG Root X2 +============ +-----BEGIN CERTIFICATE----- +MIICGzCCAaGgAwIBAgIQQdKd0XLq7qeAwSxs6S+HUjAKBggqhkjOPQQDAzBPMQswCQYDVQQGEwJV +UzEpMCcGA1UEChMgSW50ZXJuZXQgU2VjdXJpdHkgUmVzZWFyY2ggR3JvdXAxFTATBgNVBAMTDElT +UkcgUm9vdCBYMjAeFw0yMDA5MDQwMDAwMDBaFw00MDA5MTcxNjAwMDBaME8xCzAJBgNVBAYTAlVT +MSkwJwYDVQQKEyBJbnRlcm5ldCBTZWN1cml0eSBSZXNlYXJjaCBHcm91cDEVMBMGA1UEAxMMSVNS +RyBSb290IFgyMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEzZvVn4CDCuwJSvMWSj5cz3es3mcFDR0H +ttwW+1qLFNvicWDEukWVEYmO6gbf9yoWHKS5xcUy4APgHoIYOIvXRdgKam7mAHf7AlF9ItgKbppb +d9/w+kHsOdx1ymgHDB/qo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNV +HQ4EFgQUfEKWrt5LSDv6kviejM9ti6lyN5UwCgYIKoZIzj0EAwMDaAAwZQIwe3lORlCEwkSHRhtF +cP9Ymd70/aTSVaYgLXTWNLxBo1BfASdWtL4ndQavEi51mI38AjEAi/V3bNTIZargCyzuFJ0nN6T5 +U6VR5CmD1/iQMVtCnwr1/q4AaOeMSQ+2b1tbFfLn +-----END CERTIFICATE----- + +HiPKI Root CA - G1 +================== +-----BEGIN CERTIFICATE----- +MIIFajCCA1KgAwIBAgIQLd2szmKXlKFD6LDNdmpeYDANBgkqhkiG9w0BAQsFADBPMQswCQYDVQQG +EwJUVzEjMCEGA1UECgwaQ2h1bmdod2EgVGVsZWNvbSBDby4sIEx0ZC4xGzAZBgNVBAMMEkhpUEtJ +IFJvb3QgQ0EgLSBHMTAeFw0xOTAyMjIwOTQ2MDRaFw0zNzEyMzExNTU5NTlaME8xCzAJBgNVBAYT +AlRXMSMwIQYDVQQKDBpDaHVuZ2h3YSBUZWxlY29tIENvLiwgTHRkLjEbMBkGA1UEAwwSSGlQS0kg +Um9vdCBDQSAtIEcxMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA9B5/UnMyDHPkvRN0 +o9QwqNCuS9i233VHZvR85zkEHmpwINJaR3JnVfSl6J3VHiGh8Ge6zCFovkRTv4354twvVcg3Px+k +wJyz5HdcoEb+d/oaoDjq7Zpy3iu9lFc6uux55199QmQ5eiY29yTw1S+6lZgRZq2XNdZ1AYDgr/SE +YYwNHl98h5ZeQa/rh+r4XfEuiAU+TCK72h8q3VJGZDnzQs7ZngyzsHeXZJzA9KMuH5UHsBffMNsA +GJZMoYFL3QRtU6M9/Aes1MU3guvklQgZKILSQjqj2FPseYlgSGDIcpJQ3AOPgz+yQlda22rpEZfd +hSi8MEyr48KxRURHH+CKFgeW0iEPU8DtqX7UTuybCeyvQqww1r/REEXgphaypcXTT3OUM3ECoWqj +1jOXTyFjHluP2cFeRXF3D4FdXyGarYPM+l7WjSNfGz1BryB1ZlpK9p/7qxj3ccC2HTHsOyDry+K4 +9a6SsvfhhEvyovKTmiKe0xRvNlS9H15ZFblzqMF8b3ti6RZsR1pl8w4Rm0bZ/W3c1pzAtH2lsN0/ +Vm+h+fbkEkj9Bn8SV7apI09bA8PgcSojt/ewsTu8mL3WmKgMa/aOEmem8rJY5AIJEzypuxC00jBF +8ez3ABHfZfjcK0NVvxaXxA/VLGGEqnKG/uY6fsI/fe78LxQ+5oXdUG+3Se0CAwEAAaNCMEAwDwYD +VR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQU8ncX+l6o/vY9cdVouslGDDjYr7AwDgYDVR0PAQH/BAQD +AgGGMA0GCSqGSIb3DQEBCwUAA4ICAQBQUfB13HAE4/+qddRxosuej6ip0691x1TPOhwEmSKsxBHi +7zNKpiMdDg1H2DfHb680f0+BazVP6XKlMeJ45/dOlBhbQH3PayFUhuaVevvGyuqcSE5XCV0vrPSl +tJczWNWseanMX/mF+lLFjfiRFOs6DRfQUsJ748JzjkZ4Bjgs6FzaZsT0pPBWGTMpWmWSBUdGSquE +wx4noR8RkpkndZMPvDY7l1ePJlsMu5wP1G4wB9TcXzZoZjmDlicmisjEOf6aIW/Vcobpf2Lll07Q +JNBAsNB1CI69aO4I1258EHBGG3zgiLKecoaZAeO/n0kZtCW+VmWuF2PlHt/o/0elv+EmBYTksMCv +5wiZqAxeJoBF1PhoL5aPruJKHJwWDBNvOIf2u8g0X5IDUXlwpt/L9ZlNec1OvFefQ05rLisY+Gpz +jLrFNe85akEez3GoorKGB1s6yeHvP2UEgEcyRHCVTjFnanRbEEV16rCf0OY1/k6fi8wrkkVbbiVg +hUbN0aqwdmaTd5a+g744tiROJgvM7XpWGuDpWsZkrUx6AEhEL7lAuxM+vhV4nYWBSipX3tUZQ9rb +yltHhoMLP7YNdnhzeSJesYAfz77RP1YQmCuVh6EfnWQUYDksswBVLuT1sw5XxJFBAJw/6KXf6vb/ +yPCtbVKoF6ubYfwSUTXkJf2vqmqGOQ== +-----END CERTIFICATE----- + +GlobalSign ECC Root CA - R4 +=========================== +-----BEGIN CERTIFICATE----- +MIIB3DCCAYOgAwIBAgINAgPlfvU/k/2lCSGypjAKBggqhkjOPQQDAjBQMSQwIgYDVQQLExtHbG9i +YWxTaWduIEVDQyBSb290IENBIC0gUjQxEzARBgNVBAoTCkdsb2JhbFNpZ24xEzARBgNVBAMTCkds +b2JhbFNpZ24wHhcNMTIxMTEzMDAwMDAwWhcNMzgwMTE5MDMxNDA3WjBQMSQwIgYDVQQLExtHbG9i +YWxTaWduIEVDQyBSb290IENBIC0gUjQxEzARBgNVBAoTCkdsb2JhbFNpZ24xEzARBgNVBAMTCkds +b2JhbFNpZ24wWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAS4xnnTj2wlDp8uORkcA6SumuU5BwkW +ymOxuYb4ilfBV85C+nOh92VC/x7BALJucw7/xyHlGKSq2XE/qNS5zowdo0IwQDAOBgNVHQ8BAf8E +BAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUVLB7rUW44kB/+wpu+74zyTyjhNUwCgYI +KoZIzj0EAwIDRwAwRAIgIk90crlgr/HmnKAWBVBfw147bmF0774BxL4YSFlhgjICICadVGNA3jdg +UM/I2O2dgq43mLyjj0xMqTQrbO/7lZsm +-----END CERTIFICATE----- + +GTS Root R1 +=========== +-----BEGIN CERTIFICATE----- +MIIFVzCCAz+gAwIBAgINAgPlk28xsBNJiGuiFzANBgkqhkiG9w0BAQwFADBHMQswCQYDVQQGEwJV +UzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEUMBIGA1UEAxMLR1RTIFJvb3Qg +UjEwHhcNMTYwNjIyMDAwMDAwWhcNMzYwNjIyMDAwMDAwWjBHMQswCQYDVQQGEwJVUzEiMCAGA1UE +ChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjEwggIiMA0G +CSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC2EQKLHuOhd5s73L+UPreVp0A8of2C+X0yBoJx9vaM +f/vo27xqLpeXo4xL+Sv2sfnOhB2x+cWX3u+58qPpvBKJXqeqUqv4IyfLpLGcY9vXmX7wCl7raKb0 +xlpHDU0QM+NOsROjyBhsS+z8CZDfnWQpJSMHobTSPS5g4M/SCYe7zUjwTcLCeoiKu7rPWRnWr4+w +B7CeMfGCwcDfLqZtbBkOtdh+JhpFAz2weaSUKK0PfyblqAj+lug8aJRT7oM6iCsVlgmy4HqMLnXW +nOunVmSPlk9orj2XwoSPwLxAwAtcvfaHszVsrBhQf4TgTM2S0yDpM7xSma8ytSmzJSq0SPly4cpk +9+aCEI3oncKKiPo4Zor8Y/kB+Xj9e1x3+naH+uzfsQ55lVe0vSbv1gHR6xYKu44LtcXFilWr06zq +kUspzBmkMiVOKvFlRNACzqrOSbTqn3yDsEB750Orp2yjj32JgfpMpf/VjsPOS+C12LOORc92wO1A +K/1TD7Cn1TsNsYqiA94xrcx36m97PtbfkSIS5r762DL8EGMUUXLeXdYWk70paDPvOmbsB4om3xPX +V2V4J95eSRQAogB/mqghtqmxlbCluQ0WEdrHbEg8QOB+DVrNVjzRlwW5y0vtOUucxD/SVRNuJLDW +cfr0wbrM7Rv1/oFB2ACYPTrIrnqYNxgFlQIDAQABo0IwQDAOBgNVHQ8BAf8EBAMCAYYwDwYDVR0T +AQH/BAUwAwEB/zAdBgNVHQ4EFgQU5K8rJnEaK0gnhS9SZizv8IkTcT4wDQYJKoZIhvcNAQEMBQAD +ggIBAJ+qQibbC5u+/x6Wki4+omVKapi6Ist9wTrYggoGxval3sBOh2Z5ofmmWJyq+bXmYOfg6LEe +QkEzCzc9zolwFcq1JKjPa7XSQCGYzyI0zzvFIoTgxQ6KfF2I5DUkzps+GlQebtuyh6f88/qBVRRi +ClmpIgUxPoLW7ttXNLwzldMXG+gnoot7TiYaelpkttGsN/H9oPM47HLwEXWdyzRSjeZ2axfG34ar +J45JK3VmgRAhpuo+9K4l/3wV3s6MJT/KYnAK9y8JZgfIPxz88NtFMN9iiMG1D53Dn0reWVlHxYci +NuaCp+0KueIHoI17eko8cdLiA6EfMgfdG+RCzgwARWGAtQsgWSl4vflVy2PFPEz0tv/bal8xa5me +LMFrUKTX5hgUvYU/Z6tGn6D/Qqc6f1zLXbBwHSs09dR2CQzreExZBfMzQsNhFRAbd03OIozUhfJF +fbdT6u9AWpQKXCBfTkBdYiJ23//OYb2MI3jSNwLgjt7RETeJ9r/tSQdirpLsQBqvFAnZ0E6yove+ +7u7Y/9waLd64NnHi/Hm3lCXRSHNboTXns5lndcEZOitHTtNCjv0xyBZm2tIMPNuzjsmhDYAPexZ3 +FL//2wmUspO8IFgV6dtxQ/PeEMMA3KgqlbbC1j+Qa3bbbP6MvPJwNQzcmRk13NfIRmPVNnGuV/u3 +gm3c +-----END CERTIFICATE----- + +GTS Root R2 +=========== +-----BEGIN CERTIFICATE----- +MIIFVzCCAz+gAwIBAgINAgPlrsWNBCUaqxElqjANBgkqhkiG9w0BAQwFADBHMQswCQYDVQQGEwJV +UzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEUMBIGA1UEAxMLR1RTIFJvb3Qg +UjIwHhcNMTYwNjIyMDAwMDAwWhcNMzYwNjIyMDAwMDAwWjBHMQswCQYDVQQGEwJVUzEiMCAGA1UE +ChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjIwggIiMA0G +CSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDO3v2m++zsFDQ8BwZabFn3GTXd98GdVarTzTukk3Lv +CvptnfbwhYBboUhSnznFt+4orO/LdmgUud+tAWyZH8QiHZ/+cnfgLFuv5AS/T3KgGjSY6Dlo7JUl +e3ah5mm5hRm9iYz+re026nO8/4Piy33B0s5Ks40FnotJk9/BW9BuXvAuMC6C/Pq8tBcKSOWIm8Wb +a96wyrQD8Nr0kLhlZPdcTK3ofmZemde4wj7I0BOdre7kRXuJVfeKH2JShBKzwkCX44ofR5GmdFrS ++LFjKBC4swm4VndAoiaYecb+3yXuPuWgf9RhD1FLPD+M2uFwdNjCaKH5wQzpoeJ/u1U8dgbuak7M +kogwTZq9TwtImoS1mKPV+3PBV2HdKFZ1E66HjucMUQkQdYhMvI35ezzUIkgfKtzra7tEscszcTJG +r61K8YzodDqs5xoic4DSMPclQsciOzsSrZYuxsN2B6ogtzVJV+mSSeh2FnIxZyuWfoqjx5RWIr9q +S34BIbIjMt/kmkRtWVtd9QCgHJvGeJeNkP+byKq0rxFROV7Z+2et1VsRnTKaG73VululycslaVNV +J1zgyjbLiGH7HrfQy+4W+9OmTN6SpdTi3/UGVN4unUu0kzCqgc7dGtxRcw1PcOnlthYhGXmy5okL +dWTK1au8CcEYof/UVKGFPP0UJAOyh9OktwIDAQABo0IwQDAOBgNVHQ8BAf8EBAMCAYYwDwYDVR0T +AQH/BAUwAwEB/zAdBgNVHQ4EFgQUu//KjiOfT5nK2+JopqUVJxce2Q4wDQYJKoZIhvcNAQEMBQAD +ggIBAB/Kzt3HvqGf2SdMC9wXmBFqiN495nFWcrKeGk6c1SuYJF2ba3uwM4IJvd8lRuqYnrYb/oM8 +0mJhwQTtzuDFycgTE1XnqGOtjHsB/ncw4c5omwX4Eu55MaBBRTUoCnGkJE+M3DyCB19m3H0Q/gxh +swWV7uGugQ+o+MePTagjAiZrHYNSVc61LwDKgEDg4XSsYPWHgJ2uNmSRXbBoGOqKYcl3qJfEycel +/FVL8/B/uWU9J2jQzGv6U53hkRrJXRqWbTKH7QMgyALOWr7Z6v2yTcQvG99fevX4i8buMTolUVVn +jWQye+mew4K6Ki3pHrTgSAai/GevHyICc/sgCq+dVEuhzf9gR7A/Xe8bVr2XIZYtCtFenTgCR2y5 +9PYjJbigapordwj6xLEokCZYCDzifqrXPW+6MYgKBesntaFJ7qBFVHvmJ2WZICGoo7z7GJa7Um8M +7YNRTOlZ4iBgxcJlkoKM8xAfDoqXvneCbT+PHV28SSe9zE8P4c52hgQjxcCMElv924SgJPFI/2R8 +0L5cFtHvma3AH/vLrrw4IgYmZNralw4/KBVEqE8AyvCazM90arQ+POuV7LXTWtiBmelDGDfrs7vR +WGJB82bSj6p4lVQgw1oudCvV0b4YacCs1aTPObpRhANl6WLAYv7YTVWW4tAR+kg0Eeye7QUd5MjW +HYbL +-----END CERTIFICATE----- + +GTS Root R3 +=========== +-----BEGIN CERTIFICATE----- +MIICCTCCAY6gAwIBAgINAgPluILrIPglJ209ZjAKBggqhkjOPQQDAzBHMQswCQYDVQQGEwJVUzEi +MCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjMw +HhcNMTYwNjIyMDAwMDAwWhcNMzYwNjIyMDAwMDAwWjBHMQswCQYDVQQGEwJVUzEiMCAGA1UEChMZ +R29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjMwdjAQBgcqhkjO +PQIBBgUrgQQAIgNiAAQfTzOHMymKoYTey8chWEGJ6ladK0uFxh1MJ7x/JlFyb+Kf1qPKzEUURout +736GjOyxfi//qXGdGIRFBEFVbivqJn+7kAHjSxm65FSWRQmx1WyRRK2EE46ajA2ADDL24CejQjBA +MA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBTB8Sa6oC2uhYHP0/Eq +Er24Cmf9vDAKBggqhkjOPQQDAwNpADBmAjEA9uEglRR7VKOQFhG/hMjqb2sXnh5GmCCbn9MN2azT +L818+FsuVbu/3ZL3pAzcMeGiAjEA/JdmZuVDFhOD3cffL74UOO0BzrEXGhF16b0DjyZ+hOXJYKaV +11RZt+cRLInUue4X +-----END CERTIFICATE----- + +GTS Root R4 +=========== +-----BEGIN CERTIFICATE----- +MIICCTCCAY6gAwIBAgINAgPlwGjvYxqccpBQUjAKBggqhkjOPQQDAzBHMQswCQYDVQQGEwJVUzEi +MCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjQw +HhcNMTYwNjIyMDAwMDAwWhcNMzYwNjIyMDAwMDAwWjBHMQswCQYDVQQGEwJVUzEiMCAGA1UEChMZ +R29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjQwdjAQBgcqhkjO +PQIBBgUrgQQAIgNiAATzdHOnaItgrkO4NcWBMHtLSZ37wWHO5t5GvWvVYRg1rkDdc/eJkTBa6zzu +hXyiQHY7qca4R9gq55KRanPpsXI5nymfopjTX15YhmUPoYRlBtHci8nHc8iMai/lxKvRHYqjQjBA +MA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBSATNbrdP9JNqPV2Py1 +PsVq8JQdjDAKBggqhkjOPQQDAwNpADBmAjEA6ED/g94D9J+uHXqnLrmvT/aDHQ4thQEd0dlq7A/C +r8deVl5c1RxYIigL9zC2L7F8AjEA8GE8p/SgguMh1YQdc4acLa/KNJvxn7kjNuK8YAOdgLOaVsjh +4rsUecrNIdSUtUlD +-----END CERTIFICATE----- + +Telia Root CA v2 +================ +-----BEGIN CERTIFICATE----- +MIIFdDCCA1ygAwIBAgIPAWdfJ9b+euPkrL4JWwWeMA0GCSqGSIb3DQEBCwUAMEQxCzAJBgNVBAYT +AkZJMRowGAYDVQQKDBFUZWxpYSBGaW5sYW5kIE95ajEZMBcGA1UEAwwQVGVsaWEgUm9vdCBDQSB2 +MjAeFw0xODExMjkxMTU1NTRaFw00MzExMjkxMTU1NTRaMEQxCzAJBgNVBAYTAkZJMRowGAYDVQQK +DBFUZWxpYSBGaW5sYW5kIE95ajEZMBcGA1UEAwwQVGVsaWEgUm9vdCBDQSB2MjCCAiIwDQYJKoZI +hvcNAQEBBQADggIPADCCAgoCggIBALLQPwe84nvQa5n44ndp586dpAO8gm2h/oFlH0wnrI4AuhZ7 +6zBqAMCzdGh+sq/H1WKzej9Qyow2RCRj0jbpDIX2Q3bVTKFgcmfiKDOlyzG4OiIjNLh9vVYiQJ3q +9HsDrWj8soFPmNB06o3lfc1jw6P23pLCWBnglrvFxKk9pXSW/q/5iaq9lRdU2HhE8Qx3FZLgmEKn +pNaqIJLNwaCzlrI6hEKNfdWV5Nbb6WLEWLN5xYzTNTODn3WhUidhOPFZPY5Q4L15POdslv5e2QJl +tI5c0BE0312/UqeBAMN/mUWZFdUXyApT7GPzmX3MaRKGwhfwAZ6/hLzRUssbkmbOpFPlob/E2wnW +5olWK8jjfN7j/4nlNW4o6GwLI1GpJQXrSPjdscr6bAhR77cYbETKJuFzxokGgeWKrLDiKca5JLNr +RBH0pUPCTEPlcDaMtjNXepUugqD0XBCzYYP2AgWGLnwtbNwDRm41k9V6lS/eINhbfpSQBGq6WT0E +BXWdN6IOLj3rwaRSg/7Qa9RmjtzG6RJOHSpXqhC8fF6CfaamyfItufUXJ63RDolUK5X6wK0dmBR4 +M0KGCqlztft0DbcbMBnEWg4cJ7faGND/isgFuvGqHKI3t+ZIpEYslOqodmJHixBTB0hXbOKSTbau +BcvcwUpej6w9GU7C7WB1K9vBykLVAgMBAAGjYzBhMB8GA1UdIwQYMBaAFHKs5DN5qkWH9v2sHZ7W +xy+G2CQ5MB0GA1UdDgQWBBRyrOQzeapFh/b9rB2e1scvhtgkOTAOBgNVHQ8BAf8EBAMCAQYwDwYD +VR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAgEAoDtZpwmUPjaE0n4vOaWWl/oRrfxn83EJ +8rKJhGdEr7nv7ZbsnGTbMjBvZ5qsfl+yqwE2foH65IRe0qw24GtixX1LDoJt0nZi0f6X+J8wfBj5 +tFJ3gh1229MdqfDBmgC9bXXYfef6xzijnHDoRnkDry5023X4blMMA8iZGok1GTzTyVR8qPAs5m4H +eW9q4ebqkYJpCh3DflminmtGFZhb069GHWLIzoBSSRE/yQQSwxN8PzuKlts8oB4KtItUsiRnDe+C +y748fdHif64W1lZYudogsYMVoe+KTTJvQS8TUoKU1xrBeKJR3Stwbbca+few4GeXVtt8YVMJAygC +QMez2P2ccGrGKMOF6eLtGpOg3kuYooQ+BXcBlj37tCAPnHICehIv1aO6UXivKitEZU61/Qrowc15 +h2Er3oBXRb9n8ZuRXqWk7FlIEA04x7D6w0RtBPV4UBySllva9bguulvP5fBqnUsvWHMtTy3EHD70 +sz+rFQ47GUGKpMFXEmZxTPpT41frYpUJnlTd0cI8Vzy9OK2YZLe4A5pTVmBds9hCG1xLEooc6+t9 +xnppxyd/pPiL8uSUZodL6ZQHCRJ5irLrdATczvREWeAWysUsWNc8e89ihmpQfTU2Zqf7N+cox9jQ +raVplI/owd8k+BsHMYeB2F326CjYSlKArBPuUBQemMc= +-----END CERTIFICATE----- + +D-TRUST BR Root CA 1 2020 +========================= +-----BEGIN CERTIFICATE----- +MIIC2zCCAmCgAwIBAgIQfMmPK4TX3+oPyWWa00tNljAKBggqhkjOPQQDAzBIMQswCQYDVQQGEwJE +RTEVMBMGA1UEChMMRC1UcnVzdCBHbWJIMSIwIAYDVQQDExlELVRSVVNUIEJSIFJvb3QgQ0EgMSAy +MDIwMB4XDTIwMDIxMTA5NDUwMFoXDTM1MDIxMTA5NDQ1OVowSDELMAkGA1UEBhMCREUxFTATBgNV +BAoTDEQtVHJ1c3QgR21iSDEiMCAGA1UEAxMZRC1UUlVTVCBCUiBSb290IENBIDEgMjAyMDB2MBAG +ByqGSM49AgEGBSuBBAAiA2IABMbLxyjR+4T1mu9CFCDhQ2tuda38KwOE1HaTJddZO0Flax7mNCq7 +dPYSzuht56vkPE4/RAiLzRZxy7+SmfSk1zxQVFKQhYN4lGdnoxwJGT11NIXe7WB9xwy0QVK5buXu +QqOCAQ0wggEJMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFHOREKv/VbNafAkl1bK6CKBrqx9t +MA4GA1UdDwEB/wQEAwIBBjCBxgYDVR0fBIG+MIG7MD6gPKA6hjhodHRwOi8vY3JsLmQtdHJ1c3Qu +bmV0L2NybC9kLXRydXN0X2JyX3Jvb3RfY2FfMV8yMDIwLmNybDB5oHegdYZzbGRhcDovL2RpcmVj +dG9yeS5kLXRydXN0Lm5ldC9DTj1ELVRSVVNUJTIwQlIlMjBSb290JTIwQ0ElMjAxJTIwMjAyMCxP +PUQtVHJ1c3QlMjBHbWJILEM9REU/Y2VydGlmaWNhdGVyZXZvY2F0aW9ubGlzdDAKBggqhkjOPQQD +AwNpADBmAjEAlJAtE/rhY/hhY+ithXhUkZy4kzg+GkHaQBZTQgjKL47xPoFWwKrY7RjEsK70Pvom +AjEA8yjixtsrmfu3Ubgko6SUeho/5jbiA1czijDLgsfWFBHVdWNbFJWcHwHP2NVypw87 +-----END CERTIFICATE----- + +D-TRUST EV Root CA 1 2020 +========================= +-----BEGIN CERTIFICATE----- +MIIC2zCCAmCgAwIBAgIQXwJB13qHfEwDo6yWjfv/0DAKBggqhkjOPQQDAzBIMQswCQYDVQQGEwJE +RTEVMBMGA1UEChMMRC1UcnVzdCBHbWJIMSIwIAYDVQQDExlELVRSVVNUIEVWIFJvb3QgQ0EgMSAy +MDIwMB4XDTIwMDIxMTEwMDAwMFoXDTM1MDIxMTA5NTk1OVowSDELMAkGA1UEBhMCREUxFTATBgNV +BAoTDEQtVHJ1c3QgR21iSDEiMCAGA1UEAxMZRC1UUlVTVCBFViBSb290IENBIDEgMjAyMDB2MBAG +ByqGSM49AgEGBSuBBAAiA2IABPEL3YZDIBnfl4XoIkqbz52Yv7QFJsnL46bSj8WeeHsxiamJrSc8 +ZRCC/N/DnU7wMyPE0jL1HLDfMxddxfCxivnvubcUyilKwg+pf3VlSSowZ/Rk99Yad9rDwpdhQntJ +raOCAQ0wggEJMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFH8QARY3OqQo5FD4pPfsazK2/umL +MA4GA1UdDwEB/wQEAwIBBjCBxgYDVR0fBIG+MIG7MD6gPKA6hjhodHRwOi8vY3JsLmQtdHJ1c3Qu +bmV0L2NybC9kLXRydXN0X2V2X3Jvb3RfY2FfMV8yMDIwLmNybDB5oHegdYZzbGRhcDovL2RpcmVj +dG9yeS5kLXRydXN0Lm5ldC9DTj1ELVRSVVNUJTIwRVYlMjBSb290JTIwQ0ElMjAxJTIwMjAyMCxP +PUQtVHJ1c3QlMjBHbWJILEM9REU/Y2VydGlmaWNhdGVyZXZvY2F0aW9ubGlzdDAKBggqhkjOPQQD +AwNpADBmAjEAyjzGKnXCXnViOTYAYFqLwZOZzNnbQTs7h5kXO9XMT8oi96CAy/m0sRtW9XLS/BnR +AjEAkfcwkz8QRitxpNA7RJvAKQIFskF3UfN5Wp6OFKBOQtJbgfM0agPnIjhQW+0ZT0MW +-----END CERTIFICATE----- + +DigiCert TLS ECC P384 Root G5 +============================= +-----BEGIN CERTIFICATE----- +MIICGTCCAZ+gAwIBAgIQCeCTZaz32ci5PhwLBCou8zAKBggqhkjOPQQDAzBOMQswCQYDVQQGEwJV +UzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4xJjAkBgNVBAMTHURpZ2lDZXJ0IFRMUyBFQ0MgUDM4 +NCBSb290IEc1MB4XDTIxMDExNTAwMDAwMFoXDTQ2MDExNDIzNTk1OVowTjELMAkGA1UEBhMCVVMx +FzAVBgNVBAoTDkRpZ2lDZXJ0LCBJbmMuMSYwJAYDVQQDEx1EaWdpQ2VydCBUTFMgRUNDIFAzODQg +Um9vdCBHNTB2MBAGByqGSM49AgEGBSuBBAAiA2IABMFEoc8Rl1Ca3iOCNQfN0MsYndLxf3c1Tzvd +lHJS7cI7+Oz6e2tYIOyZrsn8aLN1udsJ7MgT9U7GCh1mMEy7H0cKPGEQQil8pQgO4CLp0zVozptj +n4S1mU1YoI71VOeVyaNCMEAwHQYDVR0OBBYEFMFRRVBZqz7nLFr6ICISB4CIfBFqMA4GA1UdDwEB +/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MAoGCCqGSM49BAMDA2gAMGUCMQCJao1H5+z8blUD2Wds +Jk6Dxv3J+ysTvLd6jLRl0mlpYxNjOyZQLgGheQaRnUi/wr4CMEfDFXuxoJGZSZOoPHzoRgaLLPIx +AJSdYsiJvRmEFOml+wG4DXZDjC5Ty3zfDBeWUA== +-----END CERTIFICATE----- + +DigiCert TLS RSA4096 Root G5 +============================ +-----BEGIN CERTIFICATE----- +MIIFZjCCA06gAwIBAgIQCPm0eKj6ftpqMzeJ3nzPijANBgkqhkiG9w0BAQwFADBNMQswCQYDVQQG +EwJVUzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4xJTAjBgNVBAMTHERpZ2lDZXJ0IFRMUyBSU0E0 +MDk2IFJvb3QgRzUwHhcNMjEwMTE1MDAwMDAwWhcNNDYwMTE0MjM1OTU5WjBNMQswCQYDVQQGEwJV +UzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4xJTAjBgNVBAMTHERpZ2lDZXJ0IFRMUyBSU0E0MDk2 +IFJvb3QgRzUwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCz0PTJeRGd/fxmgefM1eS8 +7IE+ajWOLrfn3q/5B03PMJ3qCQuZvWxX2hhKuHisOjmopkisLnLlvevxGs3npAOpPxG02C+JFvuU +AT27L/gTBaF4HI4o4EXgg/RZG5Wzrn4DReW+wkL+7vI8toUTmDKdFqgpwgscONyfMXdcvyej/Ces +tyu9dJsXLfKB2l2w4SMXPohKEiPQ6s+d3gMXsUJKoBZMpG2T6T867jp8nVid9E6P/DsjyG244gXa +zOvswzH016cpVIDPRFtMbzCe88zdH5RDnU1/cHAN1DrRN/BsnZvAFJNY781BOHW8EwOVfH/jXOnV +DdXifBBiqmvwPXbzP6PosMH976pXTayGpxi0KcEsDr9kvimM2AItzVwv8n/vFfQMFawKsPHTDU9q +TXeXAaDxZre3zu/O7Oyldcqs4+Fj97ihBMi8ez9dLRYiVu1ISf6nL3kwJZu6ay0/nTvEF+cdLvvy +z6b84xQslpghjLSR6Rlgg/IwKwZzUNWYOwbpx4oMYIwo+FKbbuH2TbsGJJvXKyY//SovcfXWJL5/ +MZ4PbeiPT02jP/816t9JXkGPhvnxd3lLG7SjXi/7RgLQZhNeXoVPzthwiHvOAbWWl9fNff2C+MIk +wcoBOU+NosEUQB+cZtUMCUbW8tDRSHZWOkPLtgoRObqME2wGtZ7P6wIDAQABo0IwQDAdBgNVHQ4E +FgQUUTMc7TZArxfTJc1paPKvTiM+s0EwDgYDVR0PAQH/BAQDAgGGMA8GA1UdEwEB/wQFMAMBAf8w +DQYJKoZIhvcNAQEMBQADggIBAGCmr1tfV9qJ20tQqcQjNSH/0GEwhJG3PxDPJY7Jv0Y02cEhJhxw +GXIeo8mH/qlDZJY6yFMECrZBu8RHANmfGBg7sg7zNOok992vIGCukihfNudd5N7HPNtQOa27PShN +lnx2xlv0wdsUpasZYgcYQF+Xkdycx6u1UQ3maVNVzDl92sURVXLFO4uJ+DQtpBflF+aZfTCIITfN +MBc9uPK8qHWgQ9w+iUuQrm0D4ByjoJYJu32jtyoQREtGBzRj7TG5BO6jm5qu5jF49OokYTurWGT/ +u4cnYiWB39yhL/btp/96j1EuMPikAdKFOV8BmZZvWltwGUb+hmA+rYAQCd05JS9Yf7vSdPD3Rh9G +OUrYU9DzLjtxpdRv/PNn5AeP3SYZ4Y1b+qOTEZvpyDrDVWiakuFSdjjo4bq9+0/V77PnSIMx8IIh +47a+p6tv75/fTM8BuGJqIz3nCU2AG3swpMPdB380vqQmsvZB6Akd4yCYqjdP//fx4ilwMUc/dNAU +FvohigLVigmUdy7yWSiLfFCSCmZ4OIN1xLVaqBHG5cGdZlXPU8Sv13WFqUITVuwhd4GTWgzqltlJ +yqEI8pc7bZsEGCREjnwB8twl2F6GmrE52/WRMmrRpnCKovfepEWFJqgejF0pW8hL2JpqA15w8oVP +bEtoL8pU9ozaMv7Da4M/OMZ+ +-----END CERTIFICATE----- + +Certainly Root R1 +================= +-----BEGIN CERTIFICATE----- +MIIFRzCCAy+gAwIBAgIRAI4P+UuQcWhlM1T01EQ5t+AwDQYJKoZIhvcNAQELBQAwPTELMAkGA1UE +BhMCVVMxEjAQBgNVBAoTCUNlcnRhaW5seTEaMBgGA1UEAxMRQ2VydGFpbmx5IFJvb3QgUjEwHhcN +MjEwNDAxMDAwMDAwWhcNNDYwNDAxMDAwMDAwWjA9MQswCQYDVQQGEwJVUzESMBAGA1UEChMJQ2Vy +dGFpbmx5MRowGAYDVQQDExFDZXJ0YWlubHkgUm9vdCBSMTCCAiIwDQYJKoZIhvcNAQEBBQADggIP +ADCCAgoCggIBANA21B/q3avk0bbm+yLA3RMNansiExyXPGhjZjKcA7WNpIGD2ngwEc/csiu+kr+O +5MQTvqRoTNoCaBZ0vrLdBORrKt03H2As2/X3oXyVtwxwhi7xOu9S98zTm/mLvg7fMbedaFySpvXl +8wo0tf97ouSHocavFwDvA5HtqRxOcT3Si2yJ9HiG5mpJoM610rCrm/b01C7jcvk2xusVtyWMOvwl +DbMicyF0yEqWYZL1LwsYpfSt4u5BvQF5+paMjRcCMLT5r3gajLQ2EBAHBXDQ9DGQilHFhiZ5shGI +XsXwClTNSaa/ApzSRKft43jvRl5tcdF5cBxGX1HpyTfcX35pe0HfNEXgO4T0oYoKNp43zGJS4YkN +KPl6I7ENPT2a/Z2B7yyQwHtETrtJ4A5KVpK8y7XdeReJkd5hiXSSqOMyhb5OhaRLWcsrxXiOcVTQ +AjeZjOVJ6uBUcqQRBi8LjMFbvrWhsFNunLhgkR9Za/kt9JQKl7XsxXYDVBtlUrpMklZRNaBA2Cnb +rlJ2Oy0wQJuK0EJWtLeIAaSHO1OWzaMWj/Nmqhexx2DgwUMFDO6bW2BvBlyHWyf5QBGenDPBt+U1 +VwV/J84XIIwc/PH72jEpSe31C4SnT8H2TsIonPru4K8H+zMReiFPCyEQtkA6qyI6BJyLm4SGcprS +p6XEtHWRqSsjAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1Ud +DgQWBBTgqj8ljZ9EXME66C6ud0yEPmcM9DANBgkqhkiG9w0BAQsFAAOCAgEAuVevuBLaV4OPaAsz +HQNTVfSVcOQrPbA56/qJYv331hgELyE03fFo8NWWWt7CgKPBjcZq91l3rhVkz1t5BXdm6ozTaw3d +8VkswTOlMIAVRQdFGjEitpIAq5lNOo93r6kiyi9jyhXWx8bwPWz8HA2YEGGeEaIi1wrykXprOQ4v +MMM2SZ/g6Q8CRFA3lFV96p/2O7qUpUzpvD5RtOjKkjZUbVwlKNrdrRT90+7iIgXr0PK3aBLXWopB +GsaSpVo7Y0VPv+E6dyIvXL9G+VoDhRNCX8reU9ditaY1BMJH/5n9hN9czulegChB8n3nHpDYT3Y+ +gjwN/KUD+nsa2UUeYNrEjvn8K8l7lcUq/6qJ34IxD3L/DCfXCh5WAFAeDJDBlrXYFIW7pw0WwfgH +JBu6haEaBQmAupVjyTrsJZ9/nbqkRxWbRHDxakvWOF5D8xh+UG7pWijmZeZ3Gzr9Hb4DJqPb1OG7 +fpYnKx3upPvaJVQTA945xsMfTZDsjxtK0hzthZU4UHlG1sGQUDGpXJpuHfUzVounmdLyyCwzk5Iw +x06MZTMQZBf9JBeW0Y3COmor6xOLRPIh80oat3df1+2IpHLlOR+Vnb5nwXARPbv0+Em34yaXOp/S +X3z7wJl8OSngex2/DaeP0ik0biQVy96QXr8axGbqwua6OV+KmalBWQewLK8= +-----END CERTIFICATE----- + +Certainly Root E1 +================= +-----BEGIN CERTIFICATE----- +MIIB9zCCAX2gAwIBAgIQBiUzsUcDMydc+Y2aub/M+DAKBggqhkjOPQQDAzA9MQswCQYDVQQGEwJV +UzESMBAGA1UEChMJQ2VydGFpbmx5MRowGAYDVQQDExFDZXJ0YWlubHkgUm9vdCBFMTAeFw0yMTA0 +MDEwMDAwMDBaFw00NjA0MDEwMDAwMDBaMD0xCzAJBgNVBAYTAlVTMRIwEAYDVQQKEwlDZXJ0YWlu +bHkxGjAYBgNVBAMTEUNlcnRhaW5seSBSb290IEUxMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAE3m/4 +fxzf7flHh4axpMCK+IKXgOqPyEpeKn2IaKcBYhSRJHpcnqMXfYqGITQYUBsQ3tA3SybHGWCA6TS9 +YBk2QNYphwk8kXr2vBMj3VlOBF7PyAIcGFPBMdjaIOlEjeR2o0IwQDAOBgNVHQ8BAf8EBAMCAQYw +DwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQU8ygYy2R17ikq6+2uI1g4hevIIgcwCgYIKoZIzj0E +AwMDaAAwZQIxALGOWiDDshliTd6wT99u0nCK8Z9+aozmut6Dacpps6kFtZaSF4fC0urQe87YQVt8 +rgIwRt7qy12a7DLCZRawTDBcMPPaTnOGBtjOiQRINzf43TNRnXCve1XYAS59BWQOhriR +-----END CERTIFICATE----- + +Security Communication RootCA3 +============================== +-----BEGIN CERTIFICATE----- +MIIFfzCCA2egAwIBAgIJAOF8N0D9G/5nMA0GCSqGSIb3DQEBDAUAMF0xCzAJBgNVBAYTAkpQMSUw +IwYDVQQKExxTRUNPTSBUcnVzdCBTeXN0ZW1zIENPLixMVEQuMScwJQYDVQQDEx5TZWN1cml0eSBD +b21tdW5pY2F0aW9uIFJvb3RDQTMwHhcNMTYwNjE2MDYxNzE2WhcNMzgwMTE4MDYxNzE2WjBdMQsw +CQYDVQQGEwJKUDElMCMGA1UEChMcU0VDT00gVHJ1c3QgU3lzdGVtcyBDTy4sTFRELjEnMCUGA1UE +AxMeU2VjdXJpdHkgQ29tbXVuaWNhdGlvbiBSb290Q0EzMIICIjANBgkqhkiG9w0BAQEFAAOCAg8A +MIICCgKCAgEA48lySfcw3gl8qUCBWNO0Ot26YQ+TUG5pPDXC7ltzkBtnTCHsXzW7OT4rCmDvu20r +hvtxosis5FaU+cmvsXLUIKx00rgVrVH+hXShuRD+BYD5UpOzQD11EKzAlrenfna84xtSGc4RHwsE +NPXY9Wk8d/Nk9A2qhd7gCVAEF5aEt8iKvE1y/By7z/MGTfmfZPd+pmaGNXHIEYBMwXFAWB6+oHP2 +/D5Q4eAvJj1+XCO1eXDe+uDRpdYMQXF79+qMHIjH7Iv10S9VlkZ8WjtYO/u62C21Jdp6Ts9EriGm +npjKIG58u4iFW/vAEGK78vknR+/RiTlDxN/e4UG/VHMgly1s2vPUB6PmudhvrvyMGS7TZ2crldtY +XLVqAvO4g160a75BflcJdURQVc1aEWEhCmHCqYj9E7wtiS/NYeCVvsq1e+F7NGcLH7YMx3weGVPK +p7FKFSBWFHA9K4IsD50VHUeAR/94mQ4xr28+j+2GaR57GIgUssL8gjMunEst+3A7caoreyYn8xrC +3PsXuKHqy6C0rtOUfnrQq8PsOC0RLoi/1D+tEjtCrI8Cbn3M0V9hvqG8OmpI6iZVIhZdXw3/JzOf +GAN0iltSIEdrRU0id4xVJ/CvHozJgyJUt5rQT9nO/NkuHJYosQLTA70lUhw0Zk8jq/R3gpYd0Vcw +CBEF/VfR2ccCAwEAAaNCMEAwHQYDVR0OBBYEFGQUfPxYchamCik0FW8qy7z8r6irMA4GA1UdDwEB +/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBDAUAA4ICAQDcAiMI4u8hOscNtybS +YpOnpSNyByCCYN8Y11StaSWSntkUz5m5UoHPrmyKO1o5yGwBQ8IibQLwYs1OY0PAFNr0Y/Dq9HHu +Tofjcan0yVflLl8cebsjqodEV+m9NU1Bu0soo5iyG9kLFwfl9+qd9XbXv8S2gVj/yP9kaWJ5rW4O +H3/uHWnlt3Jxs/6lATWUVCvAUm2PVcTJ0rjLyjQIUYWg9by0F1jqClx6vWPGOi//lkkZhOpn2ASx +YfQAW0q3nHE3GYV5v4GwxxMOdnE+OoAGrgYWp421wsTL/0ClXI2lyTrtcoHKXJg80jQDdwj98ClZ +XSEIx2C/pHF7uNkegr4Jr2VvKKu/S7XuPghHJ6APbw+LP6yVGPO5DtxnVW5inkYO0QR4ynKudtml ++LLfiAlhi+8kTtFZP1rUPcmTPCtk9YENFpb3ksP+MW/oKjJ0DvRMmEoYDjBU1cXrvMUVnuiZIesn +KwkK2/HmcBhWuwzkvvnoEKQTkrgc4NtnHVMDpCKn3F2SEDzq//wbEBrD2NCcnWXL0CsnMQMeNuE9 +dnUM/0Umud1RvCPHX9jYhxBAEg09ODfnRDwYwFMJZI//1ZqmfHAuc1Uh6N//g7kdPjIe1qZ9LPFm +6Vwdp6POXiUyK+OVrCoHzrQoeIY8LaadTdJ0MN1kURXbg4NR16/9M51NZg== +-----END CERTIFICATE----- + +Security Communication ECC RootCA1 +================================== +-----BEGIN CERTIFICATE----- +MIICODCCAb6gAwIBAgIJANZdm7N4gS7rMAoGCCqGSM49BAMDMGExCzAJBgNVBAYTAkpQMSUwIwYD +VQQKExxTRUNPTSBUcnVzdCBTeXN0ZW1zIENPLixMVEQuMSswKQYDVQQDEyJTZWN1cml0eSBDb21t +dW5pY2F0aW9uIEVDQyBSb290Q0ExMB4XDTE2MDYxNjA1MTUyOFoXDTM4MDExODA1MTUyOFowYTEL +MAkGA1UEBhMCSlAxJTAjBgNVBAoTHFNFQ09NIFRydXN0IFN5c3RlbXMgQ08uLExURC4xKzApBgNV +BAMTIlNlY3VyaXR5IENvbW11bmljYXRpb24gRUNDIFJvb3RDQTEwdjAQBgcqhkjOPQIBBgUrgQQA +IgNiAASkpW9gAwPDvTH00xecK4R1rOX9PVdu12O/5gSJko6BnOPpR27KkBLIE+CnnfdldB9sELLo +5OnvbYUymUSxXv3MdhDYW72ixvnWQuRXdtyQwjWpS4g8EkdtXP9JTxpKULGjQjBAMB0GA1UdDgQW +BBSGHOf+LaVKiwj+KBH6vqNm+GBZLzAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAK +BggqhkjOPQQDAwNoADBlAjAVXUI9/Lbu9zuxNuie9sRGKEkz0FhDKmMpzE2xtHqiuQ04pV1IKv3L +snNdo4gIxwwCMQDAqy0Obe0YottT6SXbVQjgUMzfRGEWgqtJsLKB7HOHeLRMsmIbEvoWTSVLY70e +N9k= +-----END CERTIFICATE----- + +BJCA Global Root CA1 +==================== +-----BEGIN CERTIFICATE----- +MIIFdDCCA1ygAwIBAgIQVW9l47TZkGobCdFsPsBsIDANBgkqhkiG9w0BAQsFADBUMQswCQYDVQQG +EwJDTjEmMCQGA1UECgwdQkVJSklORyBDRVJUSUZJQ0FURSBBVVRIT1JJVFkxHTAbBgNVBAMMFEJK +Q0EgR2xvYmFsIFJvb3QgQ0ExMB4XDTE5MTIxOTAzMTYxN1oXDTQ0MTIxMjAzMTYxN1owVDELMAkG +A1UEBhMCQ04xJjAkBgNVBAoMHUJFSUpJTkcgQ0VSVElGSUNBVEUgQVVUSE9SSVRZMR0wGwYDVQQD +DBRCSkNBIEdsb2JhbCBSb290IENBMTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAPFm +CL3ZxRVhy4QEQaVpN3cdwbB7+sN3SJATcmTRuHyQNZ0YeYjjlwE8R4HyDqKYDZ4/N+AZspDyRhyS +sTphzvq3Rp4Dhtczbu33RYx2N95ulpH3134rhxfVizXuhJFyV9xgw8O558dnJCNPYwpj9mZ9S1Wn +P3hkSWkSl+BMDdMJoDIwOvqfwPKcxRIqLhy1BDPapDgRat7GGPZHOiJBhyL8xIkoVNiMpTAK+BcW +yqw3/XmnkRd4OJmtWO2y3syJfQOcs4ll5+M7sSKGjwZteAf9kRJ/sGsciQ35uMt0WwfCyPQ10WRj +eulumijWML3mG90Vr4TqnMfK9Q7q8l0ph49pczm+LiRvRSGsxdRpJQaDrXpIhRMsDQa4bHlW/KNn +MoH1V6XKV0Jp6VwkYe/iMBhORJhVb3rCk9gZtt58R4oRTklH2yiUAguUSiz5EtBP6DF+bHq/pj+b +OT0CFqMYs2esWz8sgytnOYFcuX6U1WTdno9uruh8W7TXakdI136z1C2OVnZOz2nxbkRs1CTqjSSh +GL+9V/6pmTW12xB3uD1IutbB5/EjPtffhZ0nPNRAvQoMvfXnjSXWgXSHRtQpdaJCbPdzied9v3pK +H9MiyRVVz99vfFXQpIsHETdfg6YmV6YBW37+WGgHqel62bno/1Afq8K0wM7o6v0PvY1NuLxxAgMB +AAGjQjBAMB0GA1UdDgQWBBTF7+3M2I0hxkjk49cULqcWk+WYATAPBgNVHRMBAf8EBTADAQH/MA4G +A1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAgEAUoKsITQfI/Ki2Pm4rzc2IInRNwPWaZ+4 +YRC6ojGYWUfo0Q0lHhVBDOAqVdVXUsv45Mdpox1NcQJeXyFFYEhcCY5JEMEE3KliawLwQ8hOnThJ +dMkycFRtwUf8jrQ2ntScvd0g1lPJGKm1Vrl2i5VnZu69mP6u775u+2D2/VnGKhs/I0qUJDAnyIm8 +60Qkmss9vk/Ves6OF8tiwdneHg56/0OGNFK8YT88X7vZdrRTvJez/opMEi4r89fO4aL/3Xtw+zuh +TaRjAv04l5U/BXCga99igUOLtFkNSoxUnMW7gZ/NfaXvCyUeOiDbHPwfmGcCCtRzRBPbUYQaVQNW +4AB+dAb/OMRyHdOoP2gxXdMJxy6MW2Pg6Nwe0uxhHvLe5e/2mXZgLR6UcnHGCyoyx5JO1UbXHfmp +GQrI+pXObSOYqgs4rZpWDW+N8TEAiMEXnM0ZNjX+VVOg4DwzX5Ze4jLp3zO7Bkqp2IRzznfSxqxx +4VyjHQy7Ct9f4qNx2No3WqB4K/TUfet27fJhcKVlmtOJNBir+3I+17Q9eVzYH6Eze9mCUAyTF6ps +3MKCuwJXNq+YJyo5UOGwifUll35HaBC07HPKs5fRJNz2YqAo07WjuGS3iGJCz51TzZm+ZGiPTx4S +SPfSKcOYKMryMguTjClPPGAyzQWWYezyr/6zcCwupvI= +-----END CERTIFICATE----- + +BJCA Global Root CA2 +==================== +-----BEGIN CERTIFICATE----- +MIICJTCCAaugAwIBAgIQLBcIfWQqwP6FGFkGz7RK6zAKBggqhkjOPQQDAzBUMQswCQYDVQQGEwJD +TjEmMCQGA1UECgwdQkVJSklORyBDRVJUSUZJQ0FURSBBVVRIT1JJVFkxHTAbBgNVBAMMFEJKQ0Eg +R2xvYmFsIFJvb3QgQ0EyMB4XDTE5MTIxOTAzMTgyMVoXDTQ0MTIxMjAzMTgyMVowVDELMAkGA1UE +BhMCQ04xJjAkBgNVBAoMHUJFSUpJTkcgQ0VSVElGSUNBVEUgQVVUSE9SSVRZMR0wGwYDVQQDDBRC +SkNBIEdsb2JhbCBSb290IENBMjB2MBAGByqGSM49AgEGBSuBBAAiA2IABJ3LgJGNU2e1uVCxA/jl +SR9BIgmwUVJY1is0j8USRhTFiy8shP8sbqjV8QnjAyEUxEM9fMEsxEtqSs3ph+B99iK++kpRuDCK +/eHeGBIK9ke35xe/J4rUQUyWPGCWwf0VHKNCMEAwHQYDVR0OBBYEFNJKsVF/BvDRgh9Obl+rg/xI +1LCRMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMAoGCCqGSM49BAMDA2gAMGUCMBq8 +W9f+qdJUDkpd0m2xQNz0Q9XSSpkZElaA94M04TVOSG0ED1cxMDAtsaqdAzjbBgIxAMvMh1PLet8g +UXOQwKhbYdDFUDn9hf7B43j4ptZLvZuHjw/l1lOWqzzIQNph91Oj9w== +-----END CERTIFICATE----- + +Sectigo Public Server Authentication Root E46 +============================================= +-----BEGIN CERTIFICATE----- +MIICOjCCAcGgAwIBAgIQQvLM2htpN0RfFf51KBC49DAKBggqhkjOPQQDAzBfMQswCQYDVQQGEwJH +QjEYMBYGA1UEChMPU2VjdGlnbyBMaW1pdGVkMTYwNAYDVQQDEy1TZWN0aWdvIFB1YmxpYyBTZXJ2 +ZXIgQXV0aGVudGljYXRpb24gUm9vdCBFNDYwHhcNMjEwMzIyMDAwMDAwWhcNNDYwMzIxMjM1OTU5 +WjBfMQswCQYDVQQGEwJHQjEYMBYGA1UEChMPU2VjdGlnbyBMaW1pdGVkMTYwNAYDVQQDEy1TZWN0 +aWdvIFB1YmxpYyBTZXJ2ZXIgQXV0aGVudGljYXRpb24gUm9vdCBFNDYwdjAQBgcqhkjOPQIBBgUr +gQQAIgNiAAR2+pmpbiDt+dd34wc7qNs9Xzjoq1WmVk/WSOrsfy2qw7LFeeyZYX8QeccCWvkEN/U0 +NSt3zn8gj1KjAIns1aeibVvjS5KToID1AZTc8GgHHs3u/iVStSBDHBv+6xnOQ6OjQjBAMB0GA1Ud +DgQWBBTRItpMWfFLXyY4qp3W7usNw/upYTAOBgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB +/zAKBggqhkjOPQQDAwNnADBkAjAn7qRaqCG76UeXlImldCBteU/IvZNeWBj7LRoAasm4PdCkT0RH +lAFWovgzJQxC36oCMB3q4S6ILuH5px0CMk7yn2xVdOOurvulGu7t0vzCAxHrRVxgED1cf5kDW21U +SAGKcw== +-----END CERTIFICATE----- + +Sectigo Public Server Authentication Root R46 +============================================= +-----BEGIN CERTIFICATE----- +MIIFijCCA3KgAwIBAgIQdY39i658BwD6qSWn4cetFDANBgkqhkiG9w0BAQwFADBfMQswCQYDVQQG +EwJHQjEYMBYGA1UEChMPU2VjdGlnbyBMaW1pdGVkMTYwNAYDVQQDEy1TZWN0aWdvIFB1YmxpYyBT +ZXJ2ZXIgQXV0aGVudGljYXRpb24gUm9vdCBSNDYwHhcNMjEwMzIyMDAwMDAwWhcNNDYwMzIxMjM1 +OTU5WjBfMQswCQYDVQQGEwJHQjEYMBYGA1UEChMPU2VjdGlnbyBMaW1pdGVkMTYwNAYDVQQDEy1T +ZWN0aWdvIFB1YmxpYyBTZXJ2ZXIgQXV0aGVudGljYXRpb24gUm9vdCBSNDYwggIiMA0GCSqGSIb3 +DQEBAQUAA4ICDwAwggIKAoICAQCTvtU2UnXYASOgHEdCSe5jtrch/cSV1UgrJnwUUxDaef0rty2k +1Cz66jLdScK5vQ9IPXtamFSvnl0xdE8H/FAh3aTPaE8bEmNtJZlMKpnzSDBh+oF8HqcIStw+Kxwf +GExxqjWMrfhu6DtK2eWUAtaJhBOqbchPM8xQljeSM9xfiOefVNlI8JhD1mb9nxc4Q8UBUQvX4yMP +FF1bFOdLvt30yNoDN9HWOaEhUTCDsG3XME6WW5HwcCSrv0WBZEMNvSE6Lzzpng3LILVCJ8zab5vu +ZDCQOc2TZYEhMbUjUDM3IuM47fgxMMxF/mL50V0yeUKH32rMVhlATc6qu/m1dkmU8Sf4kaWD5Qaz +Yw6A3OASVYCmO2a0OYctyPDQ0RTp5A1NDvZdV3LFOxxHVp3i1fuBYYzMTYCQNFu31xR13NgESJ/A +wSiItOkcyqex8Va3e0lMWeUgFaiEAin6OJRpmkkGj80feRQXEgyDet4fsZfu+Zd4KKTIRJLpfSYF +plhym3kT2BFfrsU4YjRosoYwjviQYZ4ybPUHNs2iTG7sijbt8uaZFURww3y8nDnAtOFr94MlI1fZ +EoDlSfB1D++N6xybVCi0ITz8fAr/73trdf+LHaAZBav6+CuBQug4urv7qv094PPK306Xlynt8xhW +6aWWrL3DkJiy4Pmi1KZHQ3xtzwIDAQABo0IwQDAdBgNVHQ4EFgQUVnNYZJX5khqwEioEYnmhQBWI +IUkwDgYDVR0PAQH/BAQDAgGGMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEMBQADggIBAC9c +mTz8Bl6MlC5w6tIyMY208FHVvArzZJ8HXtXBc2hkeqK5Duj5XYUtqDdFqij0lgVQYKlJfp/imTYp +E0RHap1VIDzYm/EDMrraQKFz6oOht0SmDpkBm+S8f74TlH7Kph52gDY9hAaLMyZlbcp+nv4fjFg4 +exqDsQ+8FxG75gbMY/qB8oFM2gsQa6H61SilzwZAFv97fRheORKkU55+MkIQpiGRqRxOF3yEvJ+M +0ejf5lG5Nkc/kLnHvALcWxxPDkjBJYOcCj+esQMzEhonrPcibCTRAUH4WAP+JWgiH5paPHxsnnVI +84HxZmduTILA7rpXDhjvLpr3Etiga+kFpaHpaPi8TD8SHkXoUsCjvxInebnMMTzD9joiFgOgyY9m +pFuiTdaBJQbpdqQACj7LzTWb4OE4y2BThihCQRxEV+ioratF4yUQvNs+ZUH7G6aXD+u5dHn5Hrwd +Vw1Hr8Mvn4dGp+smWg9WY7ViYG4A++MnESLn/pmPNPW56MORcr3Ywx65LvKRRFHQV80MNNVIIb/b +E/FmJUNS0nAiNs2fxBx1IK1jcmMGDw4nztJqDby1ORrp0XZ60Vzk50lJLVU3aPAaOpg+VBeHVOmm +J1CJeyAvP/+/oYtKR5j/K3tJPsMpRmAYQqszKbrAKbkTidOIijlBO8n9pu0f9GBj39ItVQGL +-----END CERTIFICATE----- + +SSL.com TLS RSA Root CA 2022 +============================ +-----BEGIN CERTIFICATE----- +MIIFiTCCA3GgAwIBAgIQb77arXO9CEDii02+1PdbkTANBgkqhkiG9w0BAQsFADBOMQswCQYDVQQG +EwJVUzEYMBYGA1UECgwPU1NMIENvcnBvcmF0aW9uMSUwIwYDVQQDDBxTU0wuY29tIFRMUyBSU0Eg +Um9vdCBDQSAyMDIyMB4XDTIyMDgyNTE2MzQyMloXDTQ2MDgxOTE2MzQyMVowTjELMAkGA1UEBhMC +VVMxGDAWBgNVBAoMD1NTTCBDb3Jwb3JhdGlvbjElMCMGA1UEAwwcU1NMLmNvbSBUTFMgUlNBIFJv +b3QgQ0EgMjAyMjCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBANCkCXJPQIgSYT41I57u +9nTPL3tYPc48DRAokC+X94xI2KDYJbFMsBFMF3NQ0CJKY7uB0ylu1bUJPiYYf7ISf5OYt6/wNr/y +7hienDtSxUcZXXTzZGbVXcdotL8bHAajvI9AI7YexoS9UcQbOcGV0insS657Lb85/bRi3pZ7Qcac +oOAGcvvwB5cJOYF0r/c0WRFXCsJbwST0MXMwgsadugL3PnxEX4MN8/HdIGkWCVDi1FW24IBydm5M +R7d1VVm0U3TZlMZBrViKMWYPHqIbKUBOL9975hYsLfy/7PO0+r4Y9ptJ1O4Fbtk085zx7AGL0SDG +D6C1vBdOSHtRwvzpXGk3R2azaPgVKPC506QVzFpPulJwoxJF3ca6TvvC0PeoUidtbnm1jPx7jMEW +TO6Af77wdr5BUxIzrlo4QqvXDz5BjXYHMtWrifZOZ9mxQnUjbvPNQrL8VfVThxc7wDNY8VLS+YCk +8OjwO4s4zKTGkH8PnP2L0aPP2oOnaclQNtVcBdIKQXTbYxE3waWglksejBYSd66UNHsef8JmAOSq +g+qKkK3ONkRN0VHpvB/zagX9wHQfJRlAUW7qglFA35u5CCoGAtUjHBPW6dvbxrB6y3snm/vg1UYk +7RBLY0ulBY+6uB0rpvqR4pJSvezrZ5dtmi2fgTIFZzL7SAg/2SW4BCUvAgMBAAGjYzBhMA8GA1Ud +EwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAU+y437uOEeicuzRk1sTN8/9REQrkwHQYDVR0OBBYEFPsu +N+7jhHonLs0ZNbEzfP/UREK5MA4GA1UdDwEB/wQEAwIBhjANBgkqhkiG9w0BAQsFAAOCAgEAjYlt +hEUY8U+zoO9opMAdrDC8Z2awms22qyIZZtM7QbUQnRC6cm4pJCAcAZli05bg4vsMQtfhWsSWTVTN +j8pDU/0quOr4ZcoBwq1gaAafORpR2eCNJvkLTqVTJXojpBzOCBvfR4iyrT7gJ4eLSYwfqUdYe5by +iB0YrrPRpgqU+tvT5TgKa3kSM/tKWTcWQA673vWJDPFs0/dRa1419dvAJuoSc06pkZCmF8NsLzjU +o3KUQyxi4U5cMj29TH0ZR6LDSeeWP4+a0zvkEdiLA9z2tmBVGKaBUfPhqBVq6+AL8BQx1rmMRTqo +ENjwuSfr98t67wVylrXEj5ZzxOhWc5y8aVFjvO9nHEMaX3cZHxj4HCUp+UmZKbaSPaKDN7Egkaib +MOlqbLQjk2UEqxHzDh1TJElTHaE/nUiSEeJ9DU/1172iWD54nR4fK/4huxoTtrEoZP2wAgDHbICi +vRZQIA9ygV/MlP+7mea6kMvq+cYMwq7FGc4zoWtcu358NFcXrfA/rs3qr5nsLFR+jM4uElZI7xc7 +P0peYNLcdDa8pUNjyw9bowJWCZ4kLOGGgYz+qxcs+sjiMho6/4UIyYOf8kpIEFR3N+2ivEC+5BB0 +9+Rbu7nzifmPQdjH5FCQNYA+HLhNkNPU98OwoX6EyneSMSy4kLGCenROmxMmtNVQZlR4rmA= +-----END CERTIFICATE----- + +SSL.com TLS ECC Root CA 2022 +============================ +-----BEGIN CERTIFICATE----- +MIICOjCCAcCgAwIBAgIQFAP1q/s3ixdAW+JDsqXRxDAKBggqhkjOPQQDAzBOMQswCQYDVQQGEwJV +UzEYMBYGA1UECgwPU1NMIENvcnBvcmF0aW9uMSUwIwYDVQQDDBxTU0wuY29tIFRMUyBFQ0MgUm9v +dCBDQSAyMDIyMB4XDTIyMDgyNTE2MzM0OFoXDTQ2MDgxOTE2MzM0N1owTjELMAkGA1UEBhMCVVMx +GDAWBgNVBAoMD1NTTCBDb3Jwb3JhdGlvbjElMCMGA1UEAwwcU1NMLmNvbSBUTFMgRUNDIFJvb3Qg +Q0EgMjAyMjB2MBAGByqGSM49AgEGBSuBBAAiA2IABEUpNXP6wrgjzhR9qLFNoFs27iosU8NgCTWy +JGYmacCzldZdkkAZDsalE3D07xJRKF3nzL35PIXBz5SQySvOkkJYWWf9lCcQZIxPBLFNSeR7T5v1 +5wj4A4j3p8OSSxlUgaNjMGEwDwYDVR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBSJjy+j6CugFFR7 +81a4Jl9nOAuc0DAdBgNVHQ4EFgQUiY8vo+groBRUe/NWuCZfZzgLnNAwDgYDVR0PAQH/BAQDAgGG +MAoGCCqGSM49BAMDA2gAMGUCMFXjIlbp15IkWE8elDIPDAI2wv2sdDJO4fscgIijzPvX6yv/N33w +7deedWo1dlJF4AIxAMeNb0Igj762TVntd00pxCAgRWSGOlDGxK0tk/UYfXLtqc/ErFc2KAhl3zx5 +Zn6g6g== +-----END CERTIFICATE----- + +Atos TrustedRoot Root CA ECC TLS 2021 +===================================== +-----BEGIN CERTIFICATE----- +MIICFTCCAZugAwIBAgIQPZg7pmY9kGP3fiZXOATvADAKBggqhkjOPQQDAzBMMS4wLAYDVQQDDCVB +dG9zIFRydXN0ZWRSb290IFJvb3QgQ0EgRUNDIFRMUyAyMDIxMQ0wCwYDVQQKDARBdG9zMQswCQYD +VQQGEwJERTAeFw0yMTA0MjIwOTI2MjNaFw00MTA0MTcwOTI2MjJaMEwxLjAsBgNVBAMMJUF0b3Mg +VHJ1c3RlZFJvb3QgUm9vdCBDQSBFQ0MgVExTIDIwMjExDTALBgNVBAoMBEF0b3MxCzAJBgNVBAYT +AkRFMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEloZYKDcKZ9Cg3iQZGeHkBQcfl+3oZIK59sRxUM6K +DP/XtXa7oWyTbIOiaG6l2b4siJVBzV3dscqDY4PMwL502eCdpO5KTlbgmClBk1IQ1SQ4AjJn8ZQS +b+/Xxd4u/RmAo0IwQDAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBR2KCXWfeBmmnoJsmo7jjPX +NtNPojAOBgNVHQ8BAf8EBAMCAYYwCgYIKoZIzj0EAwMDaAAwZQIwW5kp85wxtolrbNa9d+F851F+ +uDrNozZffPc8dz7kUK2o59JZDCaOMDtuCCrCp1rIAjEAmeMM56PDr9NJLkaCI2ZdyQAUEv049OGY +a3cpetskz2VAv9LcjBHo9H1/IISpQuQo +-----END CERTIFICATE----- + +Atos TrustedRoot Root CA RSA TLS 2021 +===================================== +-----BEGIN CERTIFICATE----- +MIIFZDCCA0ygAwIBAgIQU9XP5hmTC/srBRLYwiqipDANBgkqhkiG9w0BAQwFADBMMS4wLAYDVQQD +DCVBdG9zIFRydXN0ZWRSb290IFJvb3QgQ0EgUlNBIFRMUyAyMDIxMQ0wCwYDVQQKDARBdG9zMQsw +CQYDVQQGEwJERTAeFw0yMTA0MjIwOTIxMTBaFw00MTA0MTcwOTIxMDlaMEwxLjAsBgNVBAMMJUF0 +b3MgVHJ1c3RlZFJvb3QgUm9vdCBDQSBSU0EgVExTIDIwMjExDTALBgNVBAoMBEF0b3MxCzAJBgNV +BAYTAkRFMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAtoAOxHm9BYx9sKOdTSJNy/BB +l01Z4NH+VoyX8te9j2y3I49f1cTYQcvyAh5x5en2XssIKl4w8i1mx4QbZFc4nXUtVsYvYe+W/CBG +vevUez8/fEc4BKkbqlLfEzfTFRVOvV98r61jx3ncCHvVoOX3W3WsgFWZkmGbzSoXfduP9LVq6hdK +ZChmFSlsAvFr1bqjM9xaZ6cF4r9lthawEO3NUDPJcFDsGY6wx/J0W2tExn2WuZgIWWbeKQGb9Cpt +0xU6kGpn8bRrZtkh68rZYnxGEFzedUlnnkL5/nWpo63/dgpnQOPF943HhZpZnmKaau1Fh5hnstVK +PNe0OwANwI8f4UDErmwh3El+fsqyjW22v5MvoVw+j8rtgI5Y4dtXz4U2OLJxpAmMkokIiEjxQGMY +sluMWuPD0xeqqxmjLBvk1cbiZnrXghmmOxYsL3GHX0WelXOTwkKBIROW1527k2gV+p2kHYzygeBY +Br3JtuP2iV2J+axEoctr+hbxx1A9JNr3w+SH1VbxT5Aw+kUJWdo0zuATHAR8ANSbhqRAvNncTFd+ +rrcztl524WWLZt+NyteYr842mIycg5kDcPOvdO3GDjbnvezBc6eUWsuSZIKmAMFwoW4sKeFYV+xa +fJlrJaSQOoD0IJ2azsct+bJLKZWD6TWNp0lIpw9MGZHQ9b8Q4HECAwEAAaNCMEAwDwYDVR0TAQH/ +BAUwAwEB/zAdBgNVHQ4EFgQUdEmZ0f+0emhFdcN+tNzMzjkz2ggwDgYDVR0PAQH/BAQDAgGGMA0G +CSqGSIb3DQEBDAUAA4ICAQAjQ1MkYlxt/T7Cz1UAbMVWiLkO3TriJQ2VSpfKgInuKs1l+NsW4AmS +4BjHeJi78+xCUvuppILXTdiK/ORO/auQxDh1MoSf/7OwKwIzNsAQkG8dnK/haZPso0UvFJ/1TCpl +Q3IM98P4lYsU84UgYt1UU90s3BiVaU+DR3BAM1h3Egyi61IxHkzJqM7F78PRreBrAwA0JrRUITWX +AdxfG/F851X6LWh3e9NpzNMOa7pNdkTWwhWaJuywxfW70Xp0wmzNxbVe9kzmWy2B27O3Opee7c9G +slA9hGCZcbUztVdF5kJHdWoOsAgMrr3e97sPWD2PAzHoPYJQyi9eDF20l74gNAf0xBLh7tew2Vkt +afcxBPTy+av5EzH4AXcOPUIjJsyacmdRIXrMPIWo6iFqO9taPKU0nprALN+AnCng33eU0aKAQv9q +TFsR0PXNor6uzFFcw9VUewyu1rkGd4Di7wcaaMxZUa1+XGdrudviB0JbuAEFWDlN5LuYo7Ey7Nmj +1m+UI/87tyll5gfp77YZ6ufCOB0yiJA8EytuzO+rdwY0d4RPcuSBhPm5dDTedk+SKlOxJTnbPP/l +PqYO5Wue/9vsL3SD3460s6neFE3/MaNFcyT6lSnMEpcEoji2jbDwN/zIIX8/syQbPYtuzE2wFg2W +HYMfRsCbvUOZ58SWLs5fyQ== +-----END CERTIFICATE----- diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gio/modules/giolibproxy.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gio/modules/giolibproxy.dll new file mode 100644 index 000000000..f348693df Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gio/modules/giolibproxy.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gio/modules/gioopenssl.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gio/modules/gioopenssl.dll new file mode 100644 index 000000000..ca4c70fe4 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gio/modules/gioopenssl.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gsta52dec.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gsta52dec.dll new file mode 100644 index 000000000..56f8143d7 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gsta52dec.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstaccurip.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstaccurip.dll new file mode 100644 index 000000000..9279d0825 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstaccurip.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstadaptivedemux2.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstadaptivedemux2.dll new file mode 100644 index 000000000..fc9471412 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstadaptivedemux2.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstadder.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstadder.dll new file mode 100644 index 000000000..ce1e34804 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstadder.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstadpcmdec.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstadpcmdec.dll new file mode 100644 index 000000000..52660069b Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstadpcmdec.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstadpcmenc.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstadpcmenc.dll new file mode 100644 index 000000000..d7d20bd7d Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstadpcmenc.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstaes.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstaes.dll new file mode 100644 index 000000000..95c0c7230 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstaes.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstaiff.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstaiff.dll new file mode 100644 index 000000000..af096de7b Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstaiff.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstalaw.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstalaw.dll new file mode 100644 index 000000000..b7d82e831 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstalaw.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstalpha.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstalpha.dll new file mode 100644 index 000000000..32a67ebb5 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstalpha.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstalphacolor.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstalphacolor.dll new file mode 100644 index 000000000..c66cd8546 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstalphacolor.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstamfcodec.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstamfcodec.dll new file mode 100644 index 000000000..2b00cba76 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstamfcodec.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstamrnb.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstamrnb.dll new file mode 100644 index 000000000..c82a22128 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstamrnb.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstamrwbdec.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstamrwbdec.dll new file mode 100644 index 000000000..b4eb803c8 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstamrwbdec.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstanalyticsoverlay.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstanalyticsoverlay.dll new file mode 100644 index 000000000..85714fcef Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstanalyticsoverlay.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstapetag.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstapetag.dll new file mode 100644 index 000000000..d2c8b9213 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstapetag.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstapp.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstapp.dll new file mode 100644 index 000000000..47ce4bde6 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstapp.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstasf.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstasf.dll new file mode 100644 index 000000000..bcc0ba007 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstasf.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstasfmux.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstasfmux.dll new file mode 100644 index 000000000..8b7aa0d18 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstasfmux.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstasio.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstasio.dll new file mode 100644 index 000000000..5b9e0581b Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstasio.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstassrender.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstassrender.dll new file mode 100644 index 000000000..207a122d1 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstassrender.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstaudiobuffersplit.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstaudiobuffersplit.dll new file mode 100644 index 000000000..cddfbd300 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstaudiobuffersplit.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstaudioconvert.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstaudioconvert.dll new file mode 100644 index 000000000..ef0ad8f85 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstaudioconvert.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstaudiofx.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstaudiofx.dll new file mode 100644 index 000000000..d8b96bd76 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstaudiofx.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstaudiofxbad.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstaudiofxbad.dll new file mode 100644 index 000000000..afc595e91 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstaudiofxbad.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstaudiolatency.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstaudiolatency.dll new file mode 100644 index 000000000..b7e99c861 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstaudiolatency.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstaudiomixer.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstaudiomixer.dll new file mode 100644 index 000000000..96a4cd146 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstaudiomixer.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstaudiomixmatrix.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstaudiomixmatrix.dll new file mode 100644 index 000000000..59201861a Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstaudiomixmatrix.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstaudioparsers.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstaudioparsers.dll new file mode 100644 index 000000000..6157b31b1 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstaudioparsers.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstaudiorate.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstaudiorate.dll new file mode 100644 index 000000000..79beef0c7 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstaudiorate.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstaudioresample.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstaudioresample.dll new file mode 100644 index 000000000..3f2dd37a7 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstaudioresample.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstaudiotestsrc.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstaudiotestsrc.dll new file mode 100644 index 000000000..83135e18f Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstaudiotestsrc.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstaudiovisualizers.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstaudiovisualizers.dll new file mode 100644 index 000000000..3f96b340c Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstaudiovisualizers.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstauparse.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstauparse.dll new file mode 100644 index 000000000..9260fc8d5 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstauparse.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstautoconvert.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstautoconvert.dll new file mode 100644 index 000000000..0988e2b29 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstautoconvert.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstautodetect.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstautodetect.dll new file mode 100644 index 000000000..0cb2b3425 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstautodetect.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstavi.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstavi.dll new file mode 100644 index 000000000..e179a8d35 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstavi.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstaws.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstaws.dll new file mode 100644 index 000000000..bbf50cc84 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstaws.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstbayer.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstbayer.dll new file mode 100644 index 000000000..2b46908ef Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstbayer.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstburn.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstburn.dll new file mode 100644 index 000000000..68d3a13fa Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstburn.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstbz2.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstbz2.dll new file mode 100644 index 000000000..e7e8e1757 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstbz2.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstcairo.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstcairo.dll new file mode 100644 index 000000000..d7bafe440 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstcairo.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstcamerabin.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstcamerabin.dll new file mode 100644 index 000000000..5144c1858 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstcamerabin.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstcdg.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstcdg.dll new file mode 100644 index 000000000..abc9d9e86 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstcdg.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstclaxon.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstclaxon.dll new file mode 100644 index 000000000..32cb88cc6 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstclaxon.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstclosedcaption.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstclosedcaption.dll new file mode 100644 index 000000000..431739792 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstclosedcaption.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstcodecalpha.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstcodecalpha.dll new file mode 100644 index 000000000..28c616ec3 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstcodecalpha.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstcodectimestamper.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstcodectimestamper.dll new file mode 100644 index 000000000..f352e7106 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstcodectimestamper.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstcoloreffects.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstcoloreffects.dll new file mode 100644 index 000000000..fa480f6ad Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstcoloreffects.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstcompositor.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstcompositor.dll new file mode 100644 index 000000000..c46f422ba Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstcompositor.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstcoreelements.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstcoreelements.dll new file mode 100644 index 000000000..1802d5631 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstcoreelements.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstcoretracers.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstcoretracers.dll new file mode 100644 index 000000000..a6e3ac676 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstcoretracers.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstcurl.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstcurl.dll new file mode 100644 index 000000000..38bbc20e7 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstcurl.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstcutter.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstcutter.dll new file mode 100644 index 000000000..3f881d934 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstcutter.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstd3d.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstd3d.dll new file mode 100644 index 000000000..312764312 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstd3d.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstd3d11.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstd3d11.dll new file mode 100644 index 000000000..6a1c00a27 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstd3d11.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstd3d12.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstd3d12.dll new file mode 100644 index 000000000..a192ebfa7 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstd3d12.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstdash.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstdash.dll new file mode 100644 index 000000000..9e152f9bc Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstdash.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstdav1d.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstdav1d.dll new file mode 100644 index 000000000..0e88f96f7 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstdav1d.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstdebug.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstdebug.dll new file mode 100644 index 000000000..e2f1a7545 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstdebug.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstdebugutilsbad.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstdebugutilsbad.dll new file mode 100644 index 000000000..6a664162b Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstdebugutilsbad.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstdecklink.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstdecklink.dll new file mode 100644 index 000000000..befc423f5 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstdecklink.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstdeinterlace.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstdeinterlace.dll new file mode 100644 index 000000000..c9a56efde Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstdeinterlace.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstdemucs.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstdemucs.dll new file mode 100644 index 000000000..fe4d85c9c Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstdemucs.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstdirectshow.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstdirectshow.dll new file mode 100644 index 000000000..09a2ad222 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstdirectshow.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstdirectsound.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstdirectsound.dll new file mode 100644 index 000000000..5767cb631 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstdirectsound.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstdirectsoundsrc.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstdirectsoundsrc.dll new file mode 100644 index 000000000..2b89e91a6 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstdirectsoundsrc.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstdtls.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstdtls.dll new file mode 100644 index 000000000..75893711d Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstdtls.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstdtmf.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstdtmf.dll new file mode 100644 index 000000000..5cafa25f9 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstdtmf.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstdtsdec.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstdtsdec.dll new file mode 100644 index 000000000..6d1155420 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstdtsdec.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstdv.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstdv.dll new file mode 100644 index 000000000..32f8c61d1 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstdv.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstdvbsubenc.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstdvbsubenc.dll new file mode 100644 index 000000000..55a893db5 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstdvbsubenc.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstdvbsuboverlay.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstdvbsuboverlay.dll new file mode 100644 index 000000000..9ecb5ae5c Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstdvbsuboverlay.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstdvdlpcmdec.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstdvdlpcmdec.dll new file mode 100644 index 000000000..36fb93c1a Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstdvdlpcmdec.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstdvdread.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstdvdread.dll new file mode 100644 index 000000000..a7d18edff Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstdvdread.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstdvdspu.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstdvdspu.dll new file mode 100644 index 000000000..10cb9829f Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstdvdspu.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstdvdsub.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstdvdsub.dll new file mode 100644 index 000000000..c4bb74344 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstdvdsub.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstdwrite.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstdwrite.dll new file mode 100644 index 000000000..0c2247566 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstdwrite.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gsteffectv.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gsteffectv.dll new file mode 100644 index 000000000..b4fbc2350 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gsteffectv.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstelevenlabs.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstelevenlabs.dll new file mode 100644 index 000000000..0d95a678b Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstelevenlabs.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstencoding.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstencoding.dll new file mode 100644 index 000000000..635c84ce8 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstencoding.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstequalizer.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstequalizer.dll new file mode 100644 index 000000000..2d0a59172 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstequalizer.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstfallbackswitch.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstfallbackswitch.dll new file mode 100644 index 000000000..76cb21f82 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstfallbackswitch.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstffv1.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstffv1.dll new file mode 100644 index 000000000..7dcad6261 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstffv1.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstfieldanalysis.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstfieldanalysis.dll new file mode 100644 index 000000000..e9a65cc20 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstfieldanalysis.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstflac.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstflac.dll new file mode 100644 index 000000000..ad616a6d4 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstflac.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstflv.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstflv.dll new file mode 100644 index 000000000..381f2e2d9 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstflv.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstflxdec.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstflxdec.dll new file mode 100644 index 000000000..a86d747d5 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstflxdec.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstfreeverb.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstfreeverb.dll new file mode 100644 index 000000000..2c7f2ec51 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstfreeverb.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstfrei0r.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstfrei0r.dll new file mode 100644 index 000000000..4cc8db4f8 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstfrei0r.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstgaudieffects.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstgaudieffects.dll new file mode 100644 index 000000000..9e0d542bd Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstgaudieffects.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstgdkpixbuf.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstgdkpixbuf.dll new file mode 100644 index 000000000..16d488382 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstgdkpixbuf.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstgdp.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstgdp.dll new file mode 100644 index 000000000..487813af3 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstgdp.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstgeometrictransform.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstgeometrictransform.dll new file mode 100644 index 000000000..c5e06c016 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstgeometrictransform.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstges.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstges.dll new file mode 100644 index 000000000..673e0c074 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstges.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstgif.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstgif.dll new file mode 100644 index 000000000..fba7011cb Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstgif.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstgio.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstgio.dll new file mode 100644 index 000000000..beccad521 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstgio.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstgoom.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstgoom.dll new file mode 100644 index 000000000..284483bcd Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstgoom.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstgoom2k1.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstgoom2k1.dll new file mode 100644 index 000000000..2c7a9f0de Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstgoom2k1.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstgopbuffer.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstgopbuffer.dll new file mode 100644 index 000000000..018151490 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstgopbuffer.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstgtk4.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstgtk4.dll new file mode 100644 index 000000000..09086caa7 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstgtk4.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gsthls.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gsthls.dll new file mode 100644 index 000000000..94a3a37a4 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gsthls.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gsthlsmultivariantsink.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gsthlsmultivariantsink.dll new file mode 100644 index 000000000..a65a2dd27 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gsthlsmultivariantsink.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gsthlssink3.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gsthlssink3.dll new file mode 100644 index 000000000..9f6636013 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gsthlssink3.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gsthsv.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gsthsv.dll new file mode 100644 index 000000000..eac6704e3 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gsthsv.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gsticecast.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gsticecast.dll new file mode 100644 index 000000000..a52fd7125 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gsticecast.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gsticydemux.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gsticydemux.dll new file mode 100644 index 000000000..a68b9e822 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gsticydemux.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstid3demux.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstid3demux.dll new file mode 100644 index 000000000..6f8c4318c Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstid3demux.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstid3tag.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstid3tag.dll new file mode 100644 index 000000000..5ce38cf2b Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstid3tag.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstimagefreeze.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstimagefreeze.dll new file mode 100644 index 000000000..91f1f433c Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstimagefreeze.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstinsertbin.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstinsertbin.dll new file mode 100644 index 000000000..c8cdc7427 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstinsertbin.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstinter.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstinter.dll new file mode 100644 index 000000000..959db4a00 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstinter.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstinterlace.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstinterlace.dll new file mode 100644 index 000000000..8d26fa701 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstinterlace.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstinterleave.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstinterleave.dll new file mode 100644 index 000000000..dd80a53fb Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstinterleave.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstipcpipeline.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstipcpipeline.dll new file mode 100644 index 000000000..c4f38ffe0 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstipcpipeline.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstisobmff.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstisobmff.dll new file mode 100644 index 000000000..7ab299655 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstisobmff.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstisomp4.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstisomp4.dll new file mode 100644 index 000000000..6ae077514 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstisomp4.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstivfparse.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstivfparse.dll new file mode 100644 index 000000000..ba5983ec3 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstivfparse.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstivtc.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstivtc.dll new file mode 100644 index 000000000..cfd09cefe Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstivtc.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstjack.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstjack.dll new file mode 100644 index 000000000..d1a0e9705 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstjack.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstjpeg.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstjpeg.dll new file mode 100644 index 000000000..a68709c9e Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstjpeg.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstjpegformat.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstjpegformat.dll new file mode 100644 index 000000000..516ffa1c6 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstjpegformat.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstjson.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstjson.dll new file mode 100644 index 000000000..d15db93d6 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstjson.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstladspa.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstladspa.dll new file mode 100644 index 000000000..06c01d195 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstladspa.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstlame.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstlame.dll new file mode 100644 index 000000000..a0c0ab0ea Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstlame.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstlcevcdecoder.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstlcevcdecoder.dll new file mode 100644 index 000000000..f33a18a8a Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstlcevcdecoder.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstlegacyrawparse.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstlegacyrawparse.dll new file mode 100644 index 000000000..c85f14c9d Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstlegacyrawparse.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstlevel.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstlevel.dll new file mode 100644 index 000000000..ad69fac2b Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstlevel.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstlewton.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstlewton.dll new file mode 100644 index 000000000..a28c7d249 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstlewton.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstlibav.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstlibav.dll new file mode 100644 index 000000000..10f7cd2bb Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstlibav.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstlivesync.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstlivesync.dll new file mode 100644 index 000000000..ac97d2387 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstlivesync.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstmatroska.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstmatroska.dll new file mode 100644 index 000000000..7cd8cb841 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstmatroska.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstmediafoundation.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstmediafoundation.dll new file mode 100644 index 000000000..e8bfee4a9 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstmediafoundation.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstmidi.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstmidi.dll new file mode 100644 index 000000000..8db2c5c37 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstmidi.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstmpegpsdemux.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstmpegpsdemux.dll new file mode 100644 index 000000000..e68572021 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstmpegpsdemux.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstmpegpsmux.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstmpegpsmux.dll new file mode 100644 index 000000000..3fb929d4d Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstmpegpsmux.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstmpegtsdemux.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstmpegtsdemux.dll new file mode 100644 index 000000000..8fbc69064 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstmpegtsdemux.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstmpegtslive.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstmpegtslive.dll new file mode 100644 index 000000000..e544637bd Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstmpegtslive.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstmpegtsmux.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstmpegtsmux.dll new file mode 100644 index 000000000..0bd1d4874 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstmpegtsmux.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstmpg123.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstmpg123.dll new file mode 100644 index 000000000..8da3ad136 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstmpg123.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstmse.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstmse.dll new file mode 100644 index 000000000..e0500b7b4 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstmse.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstmulaw.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstmulaw.dll new file mode 100644 index 000000000..336752161 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstmulaw.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstmultifile.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstmultifile.dll new file mode 100644 index 000000000..27b23a115 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstmultifile.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstmultipart.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstmultipart.dll new file mode 100644 index 000000000..bcdd9618d Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstmultipart.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstmxf.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstmxf.dll new file mode 100644 index 000000000..5b785c02a Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstmxf.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstndi.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstndi.dll new file mode 100644 index 000000000..c994b0a59 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstndi.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstnetsim.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstnetsim.dll new file mode 100644 index 000000000..1161ae9b4 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstnetsim.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstnice.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstnice.dll new file mode 100644 index 000000000..728403147 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstnice.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstnle.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstnle.dll new file mode 100644 index 000000000..018f55481 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstnle.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstnvcodec.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstnvcodec.dll new file mode 100644 index 000000000..155b0d808 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstnvcodec.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstogg.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstogg.dll new file mode 100644 index 000000000..ed2dc9742 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstogg.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstopengl.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstopengl.dll new file mode 100644 index 000000000..f62ec2e45 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstopengl.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstopenh264.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstopenh264.dll new file mode 100644 index 000000000..27ce5d3a2 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstopenh264.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstopenjpeg.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstopenjpeg.dll new file mode 100644 index 000000000..9b8126d72 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstopenjpeg.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstopus.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstopus.dll new file mode 100644 index 000000000..7a2051205 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstopus.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstopusparse.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstopusparse.dll new file mode 100644 index 000000000..e12237366 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstopusparse.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstoriginalbuffer.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstoriginalbuffer.dll new file mode 100644 index 000000000..61f8ead36 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstoriginalbuffer.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstoverlaycomposition.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstoverlaycomposition.dll new file mode 100644 index 000000000..05821d373 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstoverlaycomposition.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstpango.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstpango.dll new file mode 100644 index 000000000..ddfb532f9 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstpango.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstpbtypes.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstpbtypes.dll new file mode 100644 index 000000000..ba4f6371f Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstpbtypes.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstpcapparse.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstpcapparse.dll new file mode 100644 index 000000000..d2092e0fd Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstpcapparse.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstplayback.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstplayback.dll new file mode 100644 index 000000000..70a5b9309 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstplayback.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstpng.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstpng.dll new file mode 100644 index 000000000..f7cd8ae8e Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstpng.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstpnm.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstpnm.dll new file mode 100644 index 000000000..8728b4125 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstpnm.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstproxy.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstproxy.dll new file mode 100644 index 000000000..3e9539205 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstproxy.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstpython.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstpython.dll new file mode 100644 index 000000000..58afce20a Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstpython.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstqroverlay.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstqroverlay.dll new file mode 100644 index 000000000..ee849f272 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstqroverlay.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstqsv.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstqsv.dll new file mode 100644 index 000000000..554cd2054 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstqsv.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstquinn.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstquinn.dll new file mode 100644 index 000000000..f89a4576d Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstquinn.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstraptorq.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstraptorq.dll new file mode 100644 index 000000000..bc538755c Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstraptorq.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstrav1e.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstrav1e.dll new file mode 100644 index 000000000..3741cfb50 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstrav1e.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstrawparse.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstrawparse.dll new file mode 100644 index 000000000..804fedb09 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstrawparse.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstrealmedia.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstrealmedia.dll new file mode 100644 index 000000000..71871f074 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstrealmedia.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstregex.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstregex.dll new file mode 100644 index 000000000..66273d2bf Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstregex.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstremovesilence.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstremovesilence.dll new file mode 100644 index 000000000..246bb3e5e Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstremovesilence.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstreplaygain.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstreplaygain.dll new file mode 100644 index 000000000..e60c5cdbd Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstreplaygain.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstreqwest.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstreqwest.dll new file mode 100644 index 000000000..2a0182eee Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstreqwest.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstresindvd.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstresindvd.dll new file mode 100644 index 000000000..4a69a9f47 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstresindvd.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstrfbsrc.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstrfbsrc.dll new file mode 100644 index 000000000..0bb36a6c2 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstrfbsrc.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstrist.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstrist.dll new file mode 100644 index 000000000..e65172e9c Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstrist.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstrsanalytics.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstrsanalytics.dll new file mode 100644 index 000000000..16e06fd3b Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstrsanalytics.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstrsaudiofx.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstrsaudiofx.dll new file mode 100644 index 000000000..b70ee4263 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstrsaudiofx.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstrsaudioparsers.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstrsaudioparsers.dll new file mode 100644 index 000000000..0f92b4a0a Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstrsaudioparsers.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstrsclosedcaption.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstrsclosedcaption.dll new file mode 100644 index 000000000..748fdeabf Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstrsclosedcaption.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstrsinter.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstrsinter.dll new file mode 100644 index 000000000..04a8068c0 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstrsinter.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstrsonvif.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstrsonvif.dll new file mode 100644 index 000000000..91523ee61 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstrsonvif.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstrspng.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstrspng.dll new file mode 100644 index 000000000..cbf4a143b Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstrspng.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstrsrtp.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstrsrtp.dll new file mode 100644 index 000000000..71c9d0564 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstrsrtp.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstrsrtsp.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstrsrtsp.dll new file mode 100644 index 000000000..2ad63642c Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstrsrtsp.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstrstracers.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstrstracers.dll new file mode 100644 index 000000000..a8520c1f3 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstrstracers.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstrsvg.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstrsvg.dll new file mode 100644 index 000000000..3b28d819b Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstrsvg.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstrsvideofx.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstrsvideofx.dll new file mode 100644 index 000000000..cb10e0c42 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstrsvideofx.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstrswebrtc.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstrswebrtc.dll new file mode 100644 index 000000000..4ed5830a0 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstrswebrtc.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstrtmp.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstrtmp.dll new file mode 100644 index 000000000..4284c1354 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstrtmp.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstrtmp2.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstrtmp2.dll new file mode 100644 index 000000000..ec8cca100 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstrtmp2.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstrtp.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstrtp.dll new file mode 100644 index 000000000..e706b9e72 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstrtp.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstrtpmanager.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstrtpmanager.dll new file mode 100644 index 000000000..d43f1eb9a Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstrtpmanager.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstrtpmanagerbad.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstrtpmanagerbad.dll new file mode 100644 index 000000000..089f75fa8 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstrtpmanagerbad.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstrtponvif.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstrtponvif.dll new file mode 100644 index 000000000..3fcd146c7 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstrtponvif.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstrtsp.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstrtsp.dll new file mode 100644 index 000000000..73d847477 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstrtsp.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstrtspclientsink.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstrtspclientsink.dll new file mode 100644 index 000000000..268dd3853 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstrtspclientsink.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstsbc.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstsbc.dll new file mode 100644 index 000000000..276be1fb8 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstsbc.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstsctp.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstsctp.dll new file mode 100644 index 000000000..afbac24a3 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstsctp.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstsdpelem.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstsdpelem.dll new file mode 100644 index 000000000..6568e4a5e Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstsdpelem.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstsegmentclip.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstsegmentclip.dll new file mode 100644 index 000000000..189c18a43 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstsegmentclip.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstshapewipe.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstshapewipe.dll new file mode 100644 index 000000000..f096d2ab3 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstshapewipe.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstsiren.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstsiren.dll new file mode 100644 index 000000000..1b9ddee88 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstsiren.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstsmooth.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstsmooth.dll new file mode 100644 index 000000000..b98ad8835 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstsmooth.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstsmoothstreaming.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstsmoothstreaming.dll new file mode 100644 index 000000000..54d921b9e Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstsmoothstreaming.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstsmpte.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstsmpte.dll new file mode 100644 index 000000000..8800a7779 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstsmpte.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstsoundtouch.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstsoundtouch.dll new file mode 100644 index 000000000..05aacba61 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstsoundtouch.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstsoup.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstsoup.dll new file mode 100644 index 000000000..3edfd63b7 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstsoup.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstspandsp.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstspandsp.dll new file mode 100644 index 000000000..160f5b7bb Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstspandsp.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstspectrum.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstspectrum.dll new file mode 100644 index 000000000..190a39313 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstspectrum.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstspeechmatics.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstspeechmatics.dll new file mode 100644 index 000000000..08879d7e0 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstspeechmatics.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstspeed.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstspeed.dll new file mode 100644 index 000000000..e97009f3e Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstspeed.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstspeex.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstspeex.dll new file mode 100644 index 000000000..7e8bdef65 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstspeex.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstsrt.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstsrt.dll new file mode 100644 index 000000000..7d0f61142 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstsrt.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstsrtp.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstsrtp.dll new file mode 100644 index 000000000..c8adbd37d Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstsrtp.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gststreamgrouper.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gststreamgrouper.dll new file mode 100644 index 000000000..9b92949c8 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gststreamgrouper.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstsubenc.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstsubenc.dll new file mode 100644 index 000000000..e78d0f454 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstsubenc.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstsubparse.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstsubparse.dll new file mode 100644 index 000000000..70825b328 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstsubparse.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstsvtav1.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstsvtav1.dll new file mode 100644 index 000000000..8cd564b34 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstsvtav1.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstsvtjpegxs.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstsvtjpegxs.dll new file mode 100644 index 000000000..ee4518a97 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstsvtjpegxs.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstswitchbin.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstswitchbin.dll new file mode 100644 index 000000000..adbcf6ef5 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstswitchbin.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gsttaglib.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gsttaglib.dll new file mode 100644 index 000000000..27906c1ff Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gsttaglib.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gsttcp.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gsttcp.dll new file mode 100644 index 000000000..7a24d5655 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gsttcp.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gsttensordecoders.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gsttensordecoders.dll new file mode 100644 index 000000000..99aa09cc5 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gsttensordecoders.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gsttextahead.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gsttextahead.dll new file mode 100644 index 000000000..1b136d380 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gsttextahead.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gsttextwrap.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gsttextwrap.dll new file mode 100644 index 000000000..8d9fcd0a4 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gsttextwrap.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gsttheora.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gsttheora.dll new file mode 100644 index 000000000..556c18fc0 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gsttheora.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstthreadshare.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstthreadshare.dll new file mode 100644 index 000000000..1ddfa58bd Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstthreadshare.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gsttimecode.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gsttimecode.dll new file mode 100644 index 000000000..f4ae7a456 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gsttimecode.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gsttogglerecord.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gsttogglerecord.dll new file mode 100644 index 000000000..ad95bc478 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gsttogglerecord.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gsttranscode.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gsttranscode.dll new file mode 100644 index 000000000..fe481c7f7 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gsttranscode.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gsttypefindfunctions.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gsttypefindfunctions.dll new file mode 100644 index 000000000..848b00da5 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gsttypefindfunctions.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstudp.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstudp.dll new file mode 100644 index 000000000..562a170c9 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstudp.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gsturiplaylistbin.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gsturiplaylistbin.dll new file mode 100644 index 000000000..4f1ac143e Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gsturiplaylistbin.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstvideobox.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstvideobox.dll new file mode 100644 index 000000000..d525fbe3a Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstvideobox.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstvideoconvertscale.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstvideoconvertscale.dll new file mode 100644 index 000000000..dce21665e Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstvideoconvertscale.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstvideocrop.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstvideocrop.dll new file mode 100644 index 000000000..fdf32f8b6 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstvideocrop.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstvideofilter.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstvideofilter.dll new file mode 100644 index 000000000..6d303ed4c Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstvideofilter.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstvideofiltersbad.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstvideofiltersbad.dll new file mode 100644 index 000000000..cd37ef7e4 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstvideofiltersbad.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstvideoframe_audiolevel.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstvideoframe_audiolevel.dll new file mode 100644 index 000000000..16e06e468 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstvideoframe_audiolevel.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstvideomixer.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstvideomixer.dll new file mode 100644 index 000000000..a2973a50c Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstvideomixer.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstvideoparsersbad.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstvideoparsersbad.dll new file mode 100644 index 000000000..2d2ca9713 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstvideoparsersbad.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstvideorate.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstvideorate.dll new file mode 100644 index 000000000..df5f884d0 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstvideorate.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstvideosignal.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstvideosignal.dll new file mode 100644 index 000000000..55fe45ef9 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstvideosignal.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstvideotestsrc.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstvideotestsrc.dll new file mode 100644 index 000000000..3ef458028 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstvideotestsrc.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstvoaacenc.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstvoaacenc.dll new file mode 100644 index 000000000..36fe55096 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstvoaacenc.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstvolume.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstvolume.dll new file mode 100644 index 000000000..07bf135d3 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstvolume.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstvorbis.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstvorbis.dll new file mode 100644 index 000000000..19c8f8115 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstvorbis.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstvpx.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstvpx.dll new file mode 100644 index 000000000..96d842b7a Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstvpx.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstwasapi.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstwasapi.dll new file mode 100644 index 000000000..fcc2e6ef4 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstwasapi.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstwasapi2.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstwasapi2.dll new file mode 100644 index 000000000..d280ca52d Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstwasapi2.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstwavenc.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstwavenc.dll new file mode 100644 index 000000000..7c5e9b2cb Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstwavenc.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstwavpack.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstwavpack.dll new file mode 100644 index 000000000..705af2ba6 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstwavpack.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstwavparse.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstwavparse.dll new file mode 100644 index 000000000..14e6c7c74 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstwavparse.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstwebrtc.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstwebrtc.dll new file mode 100644 index 000000000..a0cf7227a Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstwebrtc.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstwebrtcdsp.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstwebrtcdsp.dll new file mode 100644 index 000000000..4943eee5c Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstwebrtcdsp.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstwebrtchttp.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstwebrtchttp.dll new file mode 100644 index 000000000..4480b8aaf Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstwebrtchttp.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstwebview2.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstwebview2.dll new file mode 100644 index 000000000..16f77aaa7 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstwebview2.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstwic.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstwic.dll new file mode 100644 index 000000000..917a716ee Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstwic.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstwin32ipc.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstwin32ipc.dll new file mode 100644 index 000000000..363639f95 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstwin32ipc.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstwinks.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstwinks.dll new file mode 100644 index 000000000..4dcee3ade Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstwinks.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstwinscreencap.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstwinscreencap.dll new file mode 100644 index 000000000..2d5c4d897 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstwinscreencap.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstx264.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstx264.dll new file mode 100644 index 000000000..286a318ee Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstx264.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstx265.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstx265.dll new file mode 100644 index 000000000..c5558224a Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstx265.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstxingmux.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstxingmux.dll new file mode 100644 index 000000000..32f436056 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstxingmux.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gsty4m.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gsty4m.dll new file mode 100644 index 000000000..8e7c1e621 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gsty4m.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstzbar.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstzbar.dll new file mode 100644 index 000000000..cec58449e Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstzbar.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/include/gst/d3d11/gstd3d11config.h b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/include/gst/d3d11/gstd3d11config.h new file mode 100644 index 000000000..b8d917865 --- /dev/null +++ b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/include/gst/d3d11/gstd3d11config.h @@ -0,0 +1,10 @@ +#pragma once + +#include + +G_BEGIN_DECLS + +#define GST_D3D11_WINAPI_ONLY_APP 0 +#define GST_D3D11_WINAPI_APP 1 + +G_END_DECLS diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/include/gst/gl/gstglconfig.h b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/include/gst/gl/gstglconfig.h new file mode 100644 index 000000000..3872f9b9a --- /dev/null +++ b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/include/gst/gl/gstglconfig.h @@ -0,0 +1,49 @@ +/* gstglconfig.h + */ + +#ifndef __GST_GL_CONFIG_H__ +#define __GST_GL_CONFIG_H__ + +#include + +G_BEGIN_DECLS + + +#define GST_GL_HAVE_OPENGL 1 +#define GST_GL_HAVE_GLES2 0 +#define GST_GL_HAVE_GLES3 0 +#define GST_GL_HAVE_GLES3EXT3_H 0 + +#define GST_GL_HAVE_WINDOW_X11 0 +#define GST_GL_HAVE_WINDOW_COCOA 0 +#define GST_GL_HAVE_WINDOW_WIN32 1 +#define GST_GL_HAVE_WINDOW_WINRT 0 +#define GST_GL_HAVE_WINDOW_WAYLAND 0 +#define GST_GL_HAVE_WINDOW_ANDROID 0 +#define GST_GL_HAVE_WINDOW_DISPMANX 0 +#define GST_GL_HAVE_WINDOW_EAGL 0 +#define GST_GL_HAVE_WINDOW_VIV_FB 0 +#define GST_GL_HAVE_WINDOW_GBM 0 + +#define GST_GL_HAVE_PLATFORM_EGL 0 +#define GST_GL_HAVE_PLATFORM_GLX 0 +#define GST_GL_HAVE_PLATFORM_WGL 1 +#define GST_GL_HAVE_PLATFORM_CGL 0 +#define GST_GL_HAVE_PLATFORM_EAGL 0 + +#define GST_GL_HAVE_DMABUF 0 +#define GST_GL_HAVE_VIV_DIRECTVIV 0 + +#define GST_GL_HAVE_GLEGLIMAGEOES 1 +#define GST_GL_HAVE_GLCHAR 1 +#define GST_GL_HAVE_GLSIZEIPTR 1 +#define GST_GL_HAVE_GLINTPTR 1 +#define GST_GL_HAVE_GLSYNC 1 +#define GST_GL_HAVE_GLUINT64 1 +#define GST_GL_HAVE_GLINT64 1 +#define GST_GL_HAVE_EGLATTRIB 0 +#define GST_GL_HAVE_EGLUINT64KHR 0 + +G_END_DECLS + +#endif /* __GST_GL_CONFIG_H__ */ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstaws.pc b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstaws.pc new file mode 100644 index 000000000..3d883264f --- /dev/null +++ b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstaws.pc @@ -0,0 +1,13 @@ +prefix=${pcfiledir}/../../.. +exec_prefix=${prefix} +libdir=${prefix}/lib +includedir=${prefix}/include + +Name: gstaws +Description: GStreamer Amazon Web Services plugin +Version: 0.15.2 +Libs: -L${libdir}/gstreamer-1.0 -lgstaws +Cflags: +Libs.private: -lgstaudio-1.0 -lgstbase-1.0 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgstvideo-1.0 -lgstbase-1.0 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgstbase-1.0 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lbcrypt -ladvapi32 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgio-2.0 -lgobject-2.0 -lglib-2.0 -lintl -lgobject-2.0 -lglib-2.0 -lintl -lgobject-2.0 -lglib-2.0 -lintl -llegacy_stdio_definitions -lkernel32 -lntdll -luserenv -lws2_32 -ldbghelp +Requires.private: gstrsworkspace, gstreamer-1.0, gstreamer-base-1.0, gobject-2.0, glib-2.0, gmodule-2.0, openssl + diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstburn.pc b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstburn.pc new file mode 100644 index 000000000..c6f9b087d --- /dev/null +++ b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstburn.pc @@ -0,0 +1,13 @@ +prefix=${pcfiledir}/../../.. +exec_prefix=${prefix} +libdir=${prefix}/lib +includedir=${prefix}/include + +Name: gstburn +Description: GStreamer Burn plugin +Version: 0.15.2 +Libs: -L${libdir}/gstreamer-1.0 -lgstburn +Cflags: +Libs.private: -lgstanalytics-1.0 -lgstvideo-1.0 -lgstbase-1.0 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lbcrypt -ladvapi32 -lgstvideo-1.0 -lgstbase-1.0 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgstbase-1.0 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgio-2.0 -lgobject-2.0 -lglib-2.0 -lintl -lgobject-2.0 -lglib-2.0 -lintl -lgobject-2.0 -lglib-2.0 -lintl -llegacy_stdio_definitions -lkernel32 -lntdll -luserenv -lws2_32 -ldbghelp +Requires.private: gstrsworkspace, gstreamer-1.0, gstreamer-base-1.0, gstreamer-video-1.0, gstreamer-analytics-1.0, gobject-2.0, glib-2.0, gmodule-2.0 + diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstcdg.pc b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstcdg.pc new file mode 100644 index 000000000..cb104100d --- /dev/null +++ b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstcdg.pc @@ -0,0 +1,13 @@ +prefix=${pcfiledir}/../../.. +exec_prefix=${prefix} +libdir=${prefix}/lib +includedir=${prefix}/include + +Name: gstcdg +Description: GStreamer CDG codec Plugin +Version: 0.15.2 +Libs: -L${libdir}/gstreamer-1.0 -lgstcdg +Cflags: +Libs.private: -lgstvideo-1.0 -lgstbase-1.0 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgstbase-1.0 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgio-2.0 -lgobject-2.0 -lglib-2.0 -lintl -lgobject-2.0 -lglib-2.0 -lintl -lgobject-2.0 -lglib-2.0 -lintl -llegacy_stdio_definitions -lkernel32 -lntdll -luserenv -lws2_32 -ldbghelp +Requires.private: gstrsworkspace, gstreamer-1.0, gstreamer-base-1.0, gstreamer-video-1.0, gobject-2.0, glib-2.0, gmodule-2.0 + diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstclaxon.pc b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstclaxon.pc new file mode 100644 index 000000000..155df58bd --- /dev/null +++ b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstclaxon.pc @@ -0,0 +1,13 @@ +prefix=${pcfiledir}/../../.. +exec_prefix=${prefix} +libdir=${prefix}/lib +includedir=${prefix}/include + +Name: gstclaxon +Description: GStreamer Claxon FLAC Decoder Plugin +Version: 0.15.2 +Libs: -L${libdir}/gstreamer-1.0 -lgstclaxon +Cflags: +Libs.private: -lgstaudio-1.0 -lgstbase-1.0 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgstbase-1.0 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgio-2.0 -lgobject-2.0 -lglib-2.0 -lintl -lgobject-2.0 -lglib-2.0 -lintl -lgobject-2.0 -lglib-2.0 -lintl -llegacy_stdio_definitions -lkernel32 -lntdll -luserenv -lws2_32 -ldbghelp +Requires.private: gstrsworkspace, gstreamer-1.0, gstreamer-audio-1.0, gobject-2.0, glib-2.0, gmodule-2.0 + diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstdav1d.pc b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstdav1d.pc new file mode 100644 index 000000000..8c06bbfa9 --- /dev/null +++ b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstdav1d.pc @@ -0,0 +1,13 @@ +prefix=${pcfiledir}/../../.. +exec_prefix=${prefix} +libdir=${prefix}/lib +includedir=${prefix}/include + +Name: gstdav1d +Description: GStreamer dav1d AV1 decoder Plugin +Version: 0.15.2 +Libs: -L${libdir}/gstreamer-1.0 -lgstdav1d +Cflags: +Libs.private: -ldav1d -lgstvideo-1.0 -lgstbase-1.0 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgstbase-1.0 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgio-2.0 -lgobject-2.0 -lglib-2.0 -lintl -lgobject-2.0 -lglib-2.0 -lintl -lgobject-2.0 -lglib-2.0 -lintl -llegacy_stdio_definitions -lkernel32 -lntdll -luserenv -lws2_32 -ldbghelp +Requires.private: gstrsworkspace, gstreamer-1.0, gstreamer-base-1.0, gstreamer-video-1.0, gobject-2.0, glib-2.0, gmodule-2.0, dav1d + diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstdemucs.pc b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstdemucs.pc new file mode 100644 index 000000000..94f3df756 --- /dev/null +++ b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstdemucs.pc @@ -0,0 +1,13 @@ +prefix=${pcfiledir}/../../.. +exec_prefix=${prefix} +libdir=${prefix}/lib +includedir=${prefix}/include + +Name: gstdemucs +Description: GStreamer Demucs Plugin +Version: 0.15.2 +Libs: -L${libdir}/gstreamer-1.0 -lgstdemucs +Cflags: +Libs.private: -lgstaudio-1.0 -lgstbase-1.0 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgstbase-1.0 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lbcrypt -ladvapi32 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgio-2.0 -lgobject-2.0 -lglib-2.0 -lintl -lgobject-2.0 -lglib-2.0 -lintl -lgobject-2.0 -lglib-2.0 -lintl -llegacy_stdio_definitions -lkernel32 -lntdll -luserenv -lws2_32 -ldbghelp +Requires.private: gstrsworkspace, gstreamer-1.0, gstreamer-base-1.0, gstreamer-audio-1.0, gobject-2.0, glib-2.0, gmodule-2.0 + diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstelevenlabs.pc b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstelevenlabs.pc new file mode 100644 index 000000000..1228bcdf8 --- /dev/null +++ b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstelevenlabs.pc @@ -0,0 +1,13 @@ +prefix=${pcfiledir}/../../.. +exec_prefix=${prefix} +libdir=${prefix}/lib +includedir=${prefix}/include + +Name: gstelevenlabs +Description: GStreamer ElevenLabs plugin +Version: 0.15.2 +Libs: -L${libdir}/gstreamer-1.0 -lgstelevenlabs +Cflags: +Libs.private: -lgstaudio-1.0 -lgstbase-1.0 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgstbase-1.0 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lbcrypt -ladvapi32 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgio-2.0 -lgobject-2.0 -lglib-2.0 -lintl -lgobject-2.0 -lglib-2.0 -lintl -lgobject-2.0 -lglib-2.0 -lintl -llegacy_stdio_definitions -lkernel32 -lntdll -luserenv -lws2_32 -ldbghelp +Requires.private: gstrsworkspace, gstreamer-1.0, gstreamer-base-1.0, gstreamer-audio-1.0, gobject-2.0, glib-2.0, gmodule-2.0 + diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstfallbackswitch.pc b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstfallbackswitch.pc new file mode 100644 index 000000000..58fe8cf6f --- /dev/null +++ b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstfallbackswitch.pc @@ -0,0 +1,13 @@ +prefix=${pcfiledir}/../../.. +exec_prefix=${prefix} +libdir=${prefix}/lib +includedir=${prefix}/include + +Name: gstfallbackswitch +Description: GStreamer Fallback Switcher and Source Plugin +Version: 0.15.2 +Libs: -L${libdir}/gstreamer-1.0 -lgstfallbackswitch +Cflags: +Libs.private: -lgstvideo-1.0 -lgstbase-1.0 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgstaudio-1.0 -lgstbase-1.0 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgstbase-1.0 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgio-2.0 -lgobject-2.0 -lglib-2.0 -lintl -lgobject-2.0 -lglib-2.0 -lintl -lgobject-2.0 -lglib-2.0 -lintl -llegacy_stdio_definitions -lkernel32 -lntdll -luserenv -lws2_32 -ldbghelp +Requires.private: gstrsworkspace, gstreamer-1.0, gstreamer-base-1.0, gstreamer-audio-1.0, gstreamer-video-1.0, gobject-2.0, glib-2.0, gmodule-2.0 + diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstffv1.pc b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstffv1.pc new file mode 100644 index 000000000..b1aa6defe --- /dev/null +++ b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstffv1.pc @@ -0,0 +1,13 @@ +prefix=${pcfiledir}/../../.. +exec_prefix=${prefix} +libdir=${prefix}/lib +includedir=${prefix}/include + +Name: gstffv1 +Description: GStreamer FFV1 Decoder Plugin +Version: 0.15.2 +Libs: -L${libdir}/gstreamer-1.0 -lgstffv1 +Cflags: +Libs.private: -lgstvideo-1.0 -lgstbase-1.0 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgstbase-1.0 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgio-2.0 -lgobject-2.0 -lglib-2.0 -lintl -lgobject-2.0 -lglib-2.0 -lintl -lgobject-2.0 -lglib-2.0 -lintl -llegacy_stdio_definitions -lkernel32 -lntdll -luserenv -lws2_32 -ldbghelp +Requires.private: gstrsworkspace, gstreamer-1.0, gstreamer-base-1.0, gstreamer-video-1.0, gobject-2.0, glib-2.0 + diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstgif.pc b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstgif.pc new file mode 100644 index 000000000..cd88164d5 --- /dev/null +++ b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstgif.pc @@ -0,0 +1,13 @@ +prefix=${pcfiledir}/../../.. +exec_prefix=${prefix} +libdir=${prefix}/lib +includedir=${prefix}/include + +Name: gstgif +Description: GStreamer GIF plugin +Version: 0.15.2 +Libs: -L${libdir}/gstreamer-1.0 -lgstgif +Cflags: +Libs.private: -lgstvideo-1.0 -lgstbase-1.0 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgstbase-1.0 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgio-2.0 -lgobject-2.0 -lglib-2.0 -lintl -lgobject-2.0 -lglib-2.0 -lintl -lgobject-2.0 -lglib-2.0 -lintl -llegacy_stdio_definitions -lkernel32 -lntdll -luserenv -lws2_32 -ldbghelp +Requires.private: gstrsworkspace, gstreamer-1.0, gstreamer-video-1.0, gobject-2.0, glib-2.0, gmodule-2.0 + diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstgopbuffer.pc b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstgopbuffer.pc new file mode 100644 index 000000000..b0739a245 --- /dev/null +++ b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstgopbuffer.pc @@ -0,0 +1,13 @@ +prefix=${pcfiledir}/../../.. +exec_prefix=${prefix} +libdir=${prefix}/lib +includedir=${prefix}/include + +Name: gstgopbuffer +Description: Store complete groups of pictures at a time +Version: 0.15.2 +Libs: -L${libdir}/gstreamer-1.0 -lgstgopbuffer +Cflags: +Libs.private: -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgio-2.0 -lgobject-2.0 -lglib-2.0 -lintl -lgobject-2.0 -lglib-2.0 -lintl -lgobject-2.0 -lglib-2.0 -lintl -llegacy_stdio_definitions -lkernel32 -lntdll -luserenv -lws2_32 -ldbghelp +Requires.private: gstrsworkspace, gstreamer-1.0, gstreamer-base-1.0, gstreamer-audio-1.0, gstreamer-video-1.0, gobject-2.0, glib-2.0, gmodule-2.0 + diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstgtk4.pc b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstgtk4.pc new file mode 100644 index 000000000..20a02a6d7 --- /dev/null +++ b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstgtk4.pc @@ -0,0 +1,13 @@ +prefix=${pcfiledir}/../../.. +exec_prefix=${prefix} +libdir=${prefix}/lib +includedir=${prefix}/include + +Name: gstgtk4 +Description: GStreamer GTK 4 sink element +Version: 0.15.2 +Libs: -L${libdir}/gstreamer-1.0 -lgstgtk4 +Cflags: +Libs.private: -lgstgl-1.0 -lgstvideo-1.0 -lgstbase-1.0 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgstvideo-1.0 -lgstbase-1.0 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgstbase-1.0 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgtk-4 -lpangowin32-1.0 -lpangocairo-1.0 -lpango-1.0 -lharfbuzz -lgdk_pixbuf-2.0 -lcairo-gobject -lcairo -lgraphene-1.0 -lgio-2.0 -lgobject-2.0 -lglib-2.0 -lintl -lgtk-4 -lpangowin32-1.0 -lpangocairo-1.0 -lpango-1.0 -lharfbuzz -lgdk_pixbuf-2.0 -lcairo-gobject -lcairo -lgraphene-1.0 -lgio-2.0 -lgobject-2.0 -lglib-2.0 -lintl -lgraphene-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgtk-4 -lpangowin32-1.0 -lpangocairo-1.0 -lpango-1.0 -lharfbuzz -lgdk_pixbuf-2.0 -lcairo-gobject -lcairo -lgraphene-1.0 -lgio-2.0 -lgobject-2.0 -lglib-2.0 -lintl -lpango-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lharfbuzz -lgdk_pixbuf-2.0 -lgobject-2.0 -lglib-2.0 -lintl -lcairo-gobject -lcairo -lgobject-2.0 -lglib-2.0 -lintl -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgio-2.0 -lgobject-2.0 -lglib-2.0 -lintl -lgobject-2.0 -lglib-2.0 -lintl -lgobject-2.0 -lglib-2.0 -lintl -llegacy_stdio_definitions -lkernel32 -lntdll -luserenv -lws2_32 -ldbghelp +Requires.private: gstrsworkspace, gstreamer-1.0, gstreamer-base-1.0, gstreamer-video-1.0, gtk4, gobject-2.0, glib-2.0, gmodule-2.0 + diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gsthlsmultivariantsink.pc b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gsthlsmultivariantsink.pc new file mode 100644 index 000000000..e88e3f41e --- /dev/null +++ b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gsthlsmultivariantsink.pc @@ -0,0 +1,13 @@ +prefix=${pcfiledir}/../../.. +exec_prefix=${prefix} +libdir=${prefix}/lib +includedir=${prefix}/include + +Name: gsthlsmultivariantsink +Description: GStreamer HLS (HTTP Live Streaming) multi-variant sink Plugin +Version: 0.15.2 +Libs: -L${libdir}/gstreamer-1.0 -lgsthlsmultivariantsink +Cflags: +Libs.private: -lgstpbutils-1.0 -lgstvideo-1.0 -lgstaudio-1.0 -lgstbase-1.0 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgstvideo-1.0 -lgstbase-1.0 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgstaudio-1.0 -lgstbase-1.0 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgstbase-1.0 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgio-2.0 -lgobject-2.0 -lglib-2.0 -lintl -lgobject-2.0 -lglib-2.0 -lintl -lgobject-2.0 -lglib-2.0 -lintl -llegacy_stdio_definitions -lkernel32 -lntdll -luserenv -lws2_32 -ldbghelp +Requires.private: gstrsworkspace, gstreamer-1.0, gstreamer-base-1.0, gobject-2.0, glib-2.0, gmodule-2.0 + diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gsthlssink3.pc b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gsthlssink3.pc new file mode 100644 index 000000000..bb3b226ed --- /dev/null +++ b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gsthlssink3.pc @@ -0,0 +1,13 @@ +prefix=${pcfiledir}/../../.. +exec_prefix=${prefix} +libdir=${prefix}/lib +includedir=${prefix}/include + +Name: gsthlssink3 +Description: GStreamer HLS (HTTP Live Streaming) Plugin +Version: 0.15.2 +Libs: -L${libdir}/gstreamer-1.0 -lgsthlssink3 +Cflags: +Libs.private: -lgstapp-1.0 -lgstbase-1.0 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgstbase-1.0 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgio-2.0 -lgobject-2.0 -lglib-2.0 -lintl -lgobject-2.0 -lglib-2.0 -lintl -lgobject-2.0 -lglib-2.0 -lintl -llegacy_stdio_definitions -lkernel32 -lntdll -luserenv -lws2_32 -ldbghelp +Requires.private: gstrsworkspace, gstreamer-1.0, gstreamer-base-1.0, gobject-2.0, glib-2.0, gmodule-2.0 + diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gsthsv.pc b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gsthsv.pc new file mode 100644 index 000000000..c540c12cc --- /dev/null +++ b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gsthsv.pc @@ -0,0 +1,13 @@ +prefix=${pcfiledir}/../../.. +exec_prefix=${prefix} +libdir=${prefix}/lib +includedir=${prefix}/include + +Name: gsthsv +Description: GStreamer plugin with HSV manipulation elements +Version: 0.15.2 +Libs: -L${libdir}/gstreamer-1.0 -lgsthsv +Cflags: +Libs.private: -lgstvideo-1.0 -lgstbase-1.0 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgstbase-1.0 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgio-2.0 -lgobject-2.0 -lglib-2.0 -lintl -lgobject-2.0 -lglib-2.0 -lintl -lgobject-2.0 -lglib-2.0 -lintl -llegacy_stdio_definitions -lkernel32 -lntdll -luserenv -lws2_32 -ldbghelp +Requires.private: gstrsworkspace, gstreamer-1.0, gstreamer-base-1.0, gstreamer-video-1.0, gstreamer-audio-1.0, gobject-2.0, glib-2.0, gmodule-2.0 + diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gsticecast.pc b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gsticecast.pc new file mode 100644 index 000000000..84b36d3b7 --- /dev/null +++ b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gsticecast.pc @@ -0,0 +1,13 @@ +prefix=${pcfiledir}/../../.. +exec_prefix=${prefix} +libdir=${prefix}/lib +includedir=${prefix}/include + +Name: gsticecast +Description: GStreamer Icecast Sink Plugin +Version: 0.15.2 +Libs: -L${libdir}/gstreamer-1.0 -lgsticecast +Cflags: +Libs.private: -lgstbase-1.0 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lbcrypt -ladvapi32 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgio-2.0 -lgobject-2.0 -lglib-2.0 -lintl -lgobject-2.0 -lglib-2.0 -lintl -lgobject-2.0 -lglib-2.0 -lintl -llegacy_stdio_definitions -lkernel32 -lntdll -luserenv -lws2_32 -ldbghelp +Requires.private: gstrsworkspace, gstreamer-1.0, gstreamer-base-1.0, gobject-2.0, glib-2.0, gmodule-2.0 + diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstisobmff.pc b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstisobmff.pc new file mode 100644 index 000000000..4c08f512a --- /dev/null +++ b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstisobmff.pc @@ -0,0 +1,13 @@ +prefix=${pcfiledir}/../../.. +exec_prefix=${prefix} +libdir=${prefix}/lib +includedir=${prefix}/include + +Name: gstisobmff +Description: GStreamer ISO Base Media File Format (MP4) Plugin +Version: 0.15.2 +Libs: -L${libdir}/gstreamer-1.0 -lgstisobmff +Cflags: +Libs.private: -lgsttag-1.0 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgstpbutils-1.0 -lgstvideo-1.0 -lgstaudio-1.0 -lgstbase-1.0 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgstvideo-1.0 -lgstbase-1.0 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgstaudio-1.0 -lgstbase-1.0 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgstbase-1.0 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgio-2.0 -lgobject-2.0 -lglib-2.0 -lintl -lgobject-2.0 -lglib-2.0 -lintl -lgobject-2.0 -lglib-2.0 -lintl -llegacy_stdio_definitions -lkernel32 -lntdll -luserenv -lws2_32 -ldbghelp +Requires.private: gstrsworkspace, gstreamer-1.0, gstreamer-base-1.0, gstreamer-audio-1.0, gstreamer-video-1.0, gobject-2.0, glib-2.0, gmodule-2.0 + diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstjson.pc b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstjson.pc new file mode 100644 index 000000000..b3d01c74f --- /dev/null +++ b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstjson.pc @@ -0,0 +1,13 @@ +prefix=${pcfiledir}/../../.. +exec_prefix=${prefix} +libdir=${prefix}/lib +includedir=${prefix}/include + +Name: gstjson +Description: GStreamer JSON Plugin +Version: 0.15.2 +Libs: -L${libdir}/gstreamer-1.0 -lgstjson +Cflags: +Libs.private: -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgio-2.0 -lgobject-2.0 -lglib-2.0 -lintl -lgobject-2.0 -lglib-2.0 -lintl -lgobject-2.0 -lglib-2.0 -lintl -llegacy_stdio_definitions -lkernel32 -lntdll -luserenv -lws2_32 -ldbghelp +Requires.private: gstrsworkspace, gstreamer-1.0, gobject-2.0, glib-2.0, gmodule-2.0 + diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstlewton.pc b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstlewton.pc new file mode 100644 index 000000000..dafb2d65e --- /dev/null +++ b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstlewton.pc @@ -0,0 +1,13 @@ +prefix=${pcfiledir}/../../.. +exec_prefix=${prefix} +libdir=${prefix}/lib +includedir=${prefix}/include + +Name: gstlewton +Description: GStreamer lewton Vorbis Decoder Plugin +Version: 0.15.2 +Libs: -L${libdir}/gstreamer-1.0 -lgstlewton +Cflags: +Libs.private: -lgstaudio-1.0 -lgstbase-1.0 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgstbase-1.0 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgio-2.0 -lgobject-2.0 -lglib-2.0 -lintl -lgobject-2.0 -lglib-2.0 -lintl -lgobject-2.0 -lglib-2.0 -lintl -llegacy_stdio_definitions -lkernel32 -lntdll -luserenv -lws2_32 -ldbghelp +Requires.private: gstrsworkspace, gstreamer-1.0, gstreamer-audio-1.0, gobject-2.0, glib-2.0, gmodule-2.0 + diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstlivesync.pc b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstlivesync.pc new file mode 100644 index 000000000..03f9b4658 --- /dev/null +++ b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstlivesync.pc @@ -0,0 +1,13 @@ +prefix=${pcfiledir}/../../.. +exec_prefix=${prefix} +libdir=${prefix}/lib +includedir=${prefix}/include + +Name: gstlivesync +Description: Livesync Plugin +Version: 0.15.2 +Libs: -L${libdir}/gstreamer-1.0 -lgstlivesync +Cflags: +Libs.private: -lgstaudio-1.0 -lgstbase-1.0 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgstbase-1.0 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgio-2.0 -lgobject-2.0 -lglib-2.0 -lintl -lgobject-2.0 -lglib-2.0 -lintl -lgobject-2.0 -lglib-2.0 -lintl -llegacy_stdio_definitions -lkernel32 -lntdll -luserenv -lws2_32 -ldbghelp +Requires.private: gstrsworkspace, gstreamer-1.0, gstreamer-audio-1.0, gobject-2.0, glib-2.0, gmodule-2.0 + diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstmpegtslive.pc b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstmpegtslive.pc new file mode 100644 index 000000000..41df90db0 --- /dev/null +++ b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstmpegtslive.pc @@ -0,0 +1,13 @@ +prefix=${pcfiledir}/../../.. +exec_prefix=${prefix} +libdir=${prefix}/lib +includedir=${prefix}/include + +Name: gstmpegtslive +Description: GStreamer MPEG-TS Live sources +Version: 0.15.2 +Libs: -L${libdir}/gstreamer-1.0 -lgstmpegtslive +Cflags: +Libs.private: -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgio-2.0 -lgobject-2.0 -lglib-2.0 -lintl -lgobject-2.0 -lglib-2.0 -lintl -lgobject-2.0 -lglib-2.0 -lintl -llegacy_stdio_definitions -lkernel32 -lntdll -luserenv -lws2_32 -ldbghelp +Requires.private: gstrsworkspace, gstreamer-1.0, gstreamer-base-1.0, gobject-2.0, glib-2.0, gmodule-2.0 + diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstndi.pc b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstndi.pc new file mode 100644 index 000000000..d16226280 --- /dev/null +++ b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstndi.pc @@ -0,0 +1,13 @@ +prefix=${pcfiledir}/../../.. +exec_prefix=${prefix} +libdir=${prefix}/lib +includedir=${prefix}/include + +Name: gstndi +Description: GStreamer NewTek NDI Plugin +Version: 0.15.2 +Libs: -L${libdir}/gstreamer-1.0 -lgstndi +Cflags: +Libs.private: -lgstaudio-1.0 -lgstbase-1.0 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgstvideo-1.0 -lgstbase-1.0 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgstbase-1.0 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgio-2.0 -lgobject-2.0 -lglib-2.0 -lintl -lgobject-2.0 -lglib-2.0 -lintl -lgobject-2.0 -lglib-2.0 -lintl -llegacy_stdio_definitions -lkernel32 -lntdll -luserenv -lws2_32 -ldbghelp +Requires.private: gstrsworkspace, gstreamer-1.0, gstreamer-base-1.0, gstreamer-audio-1.0, gstreamer-video-1.0, gobject-2.0, glib-2.0, gmodule-2.0 + diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstoriginalbuffer.pc b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstoriginalbuffer.pc new file mode 100644 index 000000000..0426be761 --- /dev/null +++ b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstoriginalbuffer.pc @@ -0,0 +1,13 @@ +prefix=${pcfiledir}/../../.. +exec_prefix=${prefix} +libdir=${prefix}/lib +includedir=${prefix}/include + +Name: gstoriginalbuffer +Description: GStreamer Origin buffer meta Plugin +Version: 0.15.2 +Libs: -L${libdir}/gstreamer-1.0 -lgstoriginalbuffer +Cflags: +Libs.private: -lgstvideo-1.0 -lgstbase-1.0 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgstbase-1.0 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgio-2.0 -lgobject-2.0 -lglib-2.0 -lintl -lgobject-2.0 -lglib-2.0 -lintl -lgobject-2.0 -lglib-2.0 -lintl -llegacy_stdio_definitions -lkernel32 -lntdll -luserenv -lws2_32 -ldbghelp +Requires.private: gstrsworkspace, gstreamer-1.0, gstreamer-base-1.0, gobject-2.0, glib-2.0, gmodule-2.0 + diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstquinn.pc b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstquinn.pc new file mode 100644 index 000000000..d5e9229de --- /dev/null +++ b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstquinn.pc @@ -0,0 +1,13 @@ +prefix=${pcfiledir}/../../.. +exec_prefix=${prefix} +libdir=${prefix}/lib +includedir=${prefix}/include + +Name: gstquinn +Description: GStreamer Plugin for QUIC +Version: 0.15.2 +Libs: -L${libdir}/gstreamer-1.0 -lgstquinn +Cflags: +Libs.private: -lgstbase-1.0 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lbcrypt -ladvapi32 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgio-2.0 -lgobject-2.0 -lglib-2.0 -lintl -lgobject-2.0 -lglib-2.0 -lintl -lgobject-2.0 -lglib-2.0 -lintl -llegacy_stdio_definitions -lkernel32 -lntdll -luserenv -lws2_32 -ldbghelp +Requires.private: gstrsworkspace, gstreamer-1.0, gstreamer-base-1.0, gobject-2.0, glib-2.0 + diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstraptorq.pc b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstraptorq.pc new file mode 100644 index 000000000..93ed49053 --- /dev/null +++ b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstraptorq.pc @@ -0,0 +1,13 @@ +prefix=${pcfiledir}/../../.. +exec_prefix=${prefix} +libdir=${prefix}/lib +includedir=${prefix}/include + +Name: gstraptorq +Description: GStreamer RaptorQ FEC Plugin +Version: 0.15.2 +Libs: -L${libdir}/gstreamer-1.0 -lgstraptorq +Cflags: +Libs.private: -lgstrtp-1.0 -lgstbase-1.0 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgstbase-1.0 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgio-2.0 -lgobject-2.0 -lglib-2.0 -lintl -lgobject-2.0 -lglib-2.0 -lintl -lgobject-2.0 -lglib-2.0 -lintl -llegacy_stdio_definitions -lkernel32 -lntdll -luserenv -lws2_32 -ldbghelp +Requires.private: gstrsworkspace, gstreamer-1.0, gstreamer-base-1.0, gstreamer-rtp-1.0, gobject-2.0, glib-2.0, gmodule-2.0 + diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstrav1e.pc b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstrav1e.pc new file mode 100644 index 000000000..03d7a0419 --- /dev/null +++ b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstrav1e.pc @@ -0,0 +1,13 @@ +prefix=${pcfiledir}/../../.. +exec_prefix=${prefix} +libdir=${prefix}/lib +includedir=${prefix}/include + +Name: gstrav1e +Description: GStreamer rav1e AV1 Encoder Plugin +Version: 0.15.2 +Libs: -L${libdir}/gstreamer-1.0 -lgstrav1e +Cflags: +Libs.private: -lgstvideo-1.0 -lgstbase-1.0 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgstbase-1.0 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgio-2.0 -lgobject-2.0 -lglib-2.0 -lintl -lgobject-2.0 -lglib-2.0 -lintl -lgobject-2.0 -lglib-2.0 -lintl -llegacy_stdio_definitions -lkernel32 -lntdll -luserenv -lws2_32 -ldbghelp +Requires.private: gstrsworkspace, gstreamer-1.0, gstreamer-video-1.0, gobject-2.0, glib-2.0, gmodule-2.0 + diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstregex.pc b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstregex.pc new file mode 100644 index 000000000..1c311847a --- /dev/null +++ b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstregex.pc @@ -0,0 +1,13 @@ +prefix=${pcfiledir}/../../.. +exec_prefix=${prefix} +libdir=${prefix}/lib +includedir=${prefix}/include + +Name: gstregex +Description: GStreamer Regular Expression Plugin +Version: 0.15.2 +Libs: -L${libdir}/gstreamer-1.0 -lgstregex +Cflags: +Libs.private: -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgio-2.0 -lgobject-2.0 -lglib-2.0 -lintl -lgobject-2.0 -lglib-2.0 -lintl -lgobject-2.0 -lglib-2.0 -lintl -llegacy_stdio_definitions -lkernel32 -lntdll -luserenv -lws2_32 -ldbghelp +Requires.private: gstrsworkspace, gstreamer-1.0, gobject-2.0, glib-2.0, gmodule-2.0 + diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstreqwest.pc b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstreqwest.pc new file mode 100644 index 000000000..90b9b8306 --- /dev/null +++ b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstreqwest.pc @@ -0,0 +1,13 @@ +prefix=${pcfiledir}/../../.. +exec_prefix=${prefix} +libdir=${prefix}/lib +includedir=${prefix}/include + +Name: gstreqwest +Description: GStreamer reqwest HTTP Source Plugin +Version: 0.15.2 +Libs: -L${libdir}/gstreamer-1.0 -lgstreqwest +Cflags: +Libs.private: -lgstbase-1.0 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lbcrypt -ladvapi32 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgio-2.0 -lgobject-2.0 -lglib-2.0 -lintl -lgobject-2.0 -lglib-2.0 -lintl -lgobject-2.0 -lglib-2.0 -lintl -llegacy_stdio_definitions -lkernel32 -lntdll -luserenv -lws2_32 -ldbghelp +Requires.private: gstrsworkspace, gstreamer-1.0, gstreamer-base-1.0, gobject-2.0, glib-2.0, gmodule-2.0 + diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstrsanalytics.pc b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstrsanalytics.pc new file mode 100644 index 000000000..cf4b956b1 --- /dev/null +++ b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstrsanalytics.pc @@ -0,0 +1,13 @@ +prefix=${pcfiledir}/../../.. +exec_prefix=${prefix} +libdir=${prefix}/lib +includedir=${prefix}/include + +Name: gstrsanalytics +Description: GStreamer Rust Analytics Plugin +Version: 0.15.2 +Libs: -L${libdir}/gstreamer-1.0 -lgstrsanalytics +Cflags: +Libs.private: -lgstvideo-1.0 -lgstbase-1.0 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgstbase-1.0 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgstanalytics-1.0 -lgstvideo-1.0 -lgstbase-1.0 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgio-2.0 -lgobject-2.0 -lglib-2.0 -lintl -lgobject-2.0 -lglib-2.0 -lintl -lgobject-2.0 -lglib-2.0 -lintl -llegacy_stdio_definitions -lkernel32 -lntdll -luserenv -lws2_32 -ldbghelp +Requires.private: gstrsworkspace, gstreamer-1.0, gstreamer-base-1.0, gobject-2.0, glib-2.0, gmodule-2.0, gstreamer-analytics-1.0 + diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstrsaudiofx.pc b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstrsaudiofx.pc new file mode 100644 index 000000000..1cdca409b --- /dev/null +++ b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstrsaudiofx.pc @@ -0,0 +1,13 @@ +prefix=${pcfiledir}/../../.. +exec_prefix=${prefix} +libdir=${prefix}/lib +includedir=${prefix}/include + +Name: gstrsaudiofx +Description: GStreamer Rust Audio Effects Plugin +Version: 0.15.2 +Libs: -L${libdir}/gstreamer-1.0 -lgstrsaudiofx +Cflags: +Libs.private: -lgstaudio-1.0 -lgstbase-1.0 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgstbase-1.0 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgio-2.0 -lgobject-2.0 -lglib-2.0 -lintl -lgobject-2.0 -lglib-2.0 -lintl -lgobject-2.0 -lglib-2.0 -lintl -llegacy_stdio_definitions -lkernel32 -lntdll -luserenv -lws2_32 -ldbghelp +Requires.private: gstrsworkspace, gstreamer-1.0, gstreamer-base-1.0, gstreamer-audio-1.0, gobject-2.0, glib-2.0, gmodule-2.0 + diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstrsaudioparsers.pc b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstrsaudioparsers.pc new file mode 100644 index 000000000..8e507bc4e --- /dev/null +++ b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstrsaudioparsers.pc @@ -0,0 +1,13 @@ +prefix=${pcfiledir}/../../.. +exec_prefix=${prefix} +libdir=${prefix}/lib +includedir=${prefix}/include + +Name: gstrsaudioparsers +Description: GStreamer Rust Audio Parsers Plugin +Version: 0.15.2 +Libs: -L${libdir}/gstreamer-1.0 -lgstrsaudioparsers +Cflags: +Libs.private: -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgio-2.0 -lgobject-2.0 -lglib-2.0 -lintl -lgobject-2.0 -lglib-2.0 -lintl -lgobject-2.0 -lglib-2.0 -lintl -llegacy_stdio_definitions -lkernel32 -lntdll -luserenv -lws2_32 -ldbghelp +Requires.private: gstrsworkspace, gstreamer-1.0, gstreamer-base-1.0, gstreamer-audio-1.0, gobject-2.0, glib-2.0, gmodule-2.0 + diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstrsclosedcaption.pc b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstrsclosedcaption.pc new file mode 100644 index 000000000..717002645 --- /dev/null +++ b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstrsclosedcaption.pc @@ -0,0 +1,13 @@ +prefix=${pcfiledir}/../../.. +exec_prefix=${prefix} +libdir=${prefix}/lib +includedir=${prefix}/include + +Name: gstrsclosedcaption +Description: GStreamer Rust Closed Caption Plugin +Version: 0.15.2 +Libs: -L${libdir}/gstreamer-1.0 -lgstrsclosedcaption +Cflags: +Libs.private: -lpangocairo-1.0 -lpango-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lharfbuzz -lcairo -lcairo-gobject -lcairo -lgobject-2.0 -lglib-2.0 -lintl -lpango-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lharfbuzz -lgstvideo-1.0 -lgstbase-1.0 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgstbase-1.0 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgio-2.0 -lgobject-2.0 -lglib-2.0 -lintl -lgobject-2.0 -lglib-2.0 -lintl -lgobject-2.0 -lglib-2.0 -lintl -llegacy_stdio_definitions -lkernel32 -lntdll -luserenv -lws2_32 -ldbghelp +Requires.private: gstrsworkspace, gstreamer-1.0, gstreamer-base-1.0, gstreamer-video-1.0, gobject-2.0, glib-2.0, gmodule-2.0, pango, pangocairo, cairo-gobject + diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstrsinter.pc b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstrsinter.pc new file mode 100644 index 000000000..e81fbe388 --- /dev/null +++ b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstrsinter.pc @@ -0,0 +1,13 @@ +prefix=${pcfiledir}/../../.. +exec_prefix=${prefix} +libdir=${prefix}/lib +includedir=${prefix}/include + +Name: gstrsinter +Description: GStreamer Inter Plugin +Version: 0.15.2 +Libs: -L${libdir}/gstreamer-1.0 -lgstrsinter +Cflags: +Libs.private: -lgstvideo-1.0 -lgstbase-1.0 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgstapp-1.0 -lgstbase-1.0 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgstbase-1.0 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgio-2.0 -lgobject-2.0 -lglib-2.0 -lintl -lgobject-2.0 -lglib-2.0 -lintl -lgobject-2.0 -lglib-2.0 -lintl -llegacy_stdio_definitions -lkernel32 -lntdll -luserenv -lws2_32 -ldbghelp +Requires.private: gstrsworkspace, gstreamer-1.0, gobject-2.0, glib-2.0, gmodule-2.0 + diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstrsonvif.pc b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstrsonvif.pc new file mode 100644 index 000000000..bbb266024 --- /dev/null +++ b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstrsonvif.pc @@ -0,0 +1,13 @@ +prefix=${pcfiledir}/../../.. +exec_prefix=${prefix} +libdir=${prefix}/lib +includedir=${prefix}/include + +Name: gstrsonvif +Description: GStreamer Rust ONVIF Plugin +Version: 0.15.2 +Libs: -L${libdir}/gstreamer-1.0 -lgstrsonvif +Cflags: +Libs.private: -lpangocairo-1.0 -lpango-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lharfbuzz -lcairo -lcairo-gobject -lcairo -lgobject-2.0 -lglib-2.0 -lintl -lpango-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lharfbuzz -lgstvideo-1.0 -lgstbase-1.0 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgstrtp-1.0 -lgstbase-1.0 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgstbase-1.0 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgio-2.0 -lgobject-2.0 -lglib-2.0 -lintl -lgobject-2.0 -lglib-2.0 -lintl -lgobject-2.0 -lglib-2.0 -lintl -llegacy_stdio_definitions -lkernel32 -lntdll -luserenv -lws2_32 -ldbghelp +Requires.private: gstrsworkspace, gstreamer-1.0, gstreamer-base-1.0, gobject-2.0, glib-2.0, gmodule-2.0, pango, pangocairo + diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstrspng.pc b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstrspng.pc new file mode 100644 index 000000000..d824c8415 --- /dev/null +++ b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstrspng.pc @@ -0,0 +1,13 @@ +prefix=${pcfiledir}/../../.. +exec_prefix=${prefix} +libdir=${prefix}/lib +includedir=${prefix}/include + +Name: gstrspng +Description: GStreamer Rust PNG encoder/decoder +Version: 0.15.2 +Libs: -L${libdir}/gstreamer-1.0 -lgstrspng +Cflags: +Libs.private: -lgstvideo-1.0 -lgstbase-1.0 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgstbase-1.0 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgio-2.0 -lgobject-2.0 -lglib-2.0 -lintl -lgobject-2.0 -lglib-2.0 -lintl -lgobject-2.0 -lglib-2.0 -lintl -llegacy_stdio_definitions -lkernel32 -lntdll -luserenv -lws2_32 -ldbghelp +Requires.private: gstrsworkspace, gstreamer-1.0, gstreamer-video-1.0, gobject-2.0, glib-2.0, gmodule-2.0 + diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstrsrtp.pc b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstrsrtp.pc new file mode 100644 index 000000000..d7be3a15c --- /dev/null +++ b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstrsrtp.pc @@ -0,0 +1,13 @@ +prefix=${pcfiledir}/../../.. +exec_prefix=${prefix} +libdir=${prefix}/lib +includedir=${prefix}/include + +Name: gstrsrtp +Description: GStreamer Rust RTP Plugin +Version: 0.15.2 +Libs: -L${libdir}/gstreamer-1.0 -lgstrsrtp +Cflags: +Libs.private: -lgstvideo-1.0 -lgstbase-1.0 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgstnet-1.0 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgstaudio-1.0 -lgstbase-1.0 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgstrtp-1.0 -lgstbase-1.0 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgstbase-1.0 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgio-2.0 -lgobject-2.0 -lglib-2.0 -lintl -lgobject-2.0 -lglib-2.0 -lintl -lgobject-2.0 -lglib-2.0 -lintl -llegacy_stdio_definitions -lkernel32 -lntdll -luserenv -lws2_32 -ldbghelp +Requires.private: gstrsworkspace, gstreamer-1.0, gstreamer-base-1.0, gstreamer-rtp-1.0, gstreamer-net-1.0, gstreamer-video-1.0 gobject-2.0, glib-2.0, gmodule-2.0, gio-2.0 + diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstrsrtsp.pc b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstrsrtsp.pc new file mode 100644 index 000000000..42e8f1818 --- /dev/null +++ b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstrsrtsp.pc @@ -0,0 +1,13 @@ +prefix=${pcfiledir}/../../.. +exec_prefix=${prefix} +libdir=${prefix}/lib +includedir=${prefix}/include + +Name: gstrsrtsp +Description: GStreamer RTSP Client Plugin +Version: 0.15.2 +Libs: -L${libdir}/gstreamer-1.0 -lgstrsrtsp +Cflags: +Libs.private: -lgstpbutils-1.0 -lgstvideo-1.0 -lgstaudio-1.0 -lgstbase-1.0 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgstvideo-1.0 -lgstbase-1.0 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgstaudio-1.0 -lgstbase-1.0 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgstapp-1.0 -lgstbase-1.0 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgstbase-1.0 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgstnet-1.0 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgio-2.0 -lgobject-2.0 -lglib-2.0 -lintl -lgobject-2.0 -lglib-2.0 -lintl -lgobject-2.0 -lglib-2.0 -lintl -llegacy_stdio_definitions -lkernel32 -lntdll -luserenv -lws2_32 -ldbghelp +Requires.private: gstrsworkspace, gstreamer-1.0, gstreamer-net-1.0, gobject-2.0, glib-2.0, gmodule-2.0 + diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstrstracers.pc b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstrstracers.pc new file mode 100644 index 000000000..f5674318a --- /dev/null +++ b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstrstracers.pc @@ -0,0 +1,13 @@ +prefix=${pcfiledir}/../../.. +exec_prefix=${prefix} +libdir=${prefix}/lib +includedir=${prefix}/include + +Name: gstrstracers +Description: GStreamer Rust tracers plugin +Version: 0.15.2 +Libs: -L${libdir}/gstreamer-1.0 -lgstrstracers +Cflags: +Libs.private: -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgio-2.0 -lgobject-2.0 -lglib-2.0 -lintl -lgobject-2.0 -lglib-2.0 -lintl -lgobject-2.0 -lglib-2.0 -lintl -llegacy_stdio_definitions -lkernel32 -lntdll -luserenv -lws2_32 -ldbghelp +Requires.private: gstrsworkspace, gstreamer-1.0, gobject-2.0, glib-2.0, gmodule-2.0 + diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstrsvideofx.pc b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstrsvideofx.pc new file mode 100644 index 000000000..77048a43a --- /dev/null +++ b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstrsvideofx.pc @@ -0,0 +1,13 @@ +prefix=${pcfiledir}/../../.. +exec_prefix=${prefix} +libdir=${prefix}/lib +includedir=${prefix}/include + +Name: gstrsvideofx +Description: GStreamer Rust Video Effects Plugin +Version: 0.15.2 +Libs: -L${libdir}/gstreamer-1.0 -lgstrsvideofx +Cflags: +Libs.private: -lcairo-gobject -lcairo -lgobject-2.0 -lglib-2.0 -lintl -lgstvideo-1.0 -lgstbase-1.0 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgstbase-1.0 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgio-2.0 -lgobject-2.0 -lglib-2.0 -lintl -lgobject-2.0 -lglib-2.0 -lintl -lgobject-2.0 -lglib-2.0 -lintl -llegacy_stdio_definitions -lkernel32 -lntdll -luserenv -lws2_32 -ldbghelp +Requires.private: gstrsworkspace, gstreamer-1.0, gstreamer-base-1.0, gstreamer-video-1.0, gobject-2.0, glib-2.0, cairo-gobject + diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstrswebrtc.pc b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstrswebrtc.pc new file mode 100644 index 000000000..525a2f524 --- /dev/null +++ b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstrswebrtc.pc @@ -0,0 +1,13 @@ +prefix=${pcfiledir}/../../.. +exec_prefix=${prefix} +libdir=${prefix}/lib +includedir=${prefix}/include + +Name: gstrswebrtc +Description: GStreamer plugin for high level WebRTC elements and a simple signaling server +Version: 0.15.2 +Libs: -L${libdir}/gstreamer-1.0 -lgstrswebrtc +Cflags: +Libs.private: -lgstnet-1.0 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgstpbutils-1.0 -lgstvideo-1.0 -lgstaudio-1.0 -lgstbase-1.0 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgstaudio-1.0 -lgstbase-1.0 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgstwebrtc-1.0 -lgstsdp-1.0 -lgstbase-1.0 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgstsdp-1.0 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgstvideo-1.0 -lgstbase-1.0 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgstapp-1.0 -lgstbase-1.0 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgstrtp-1.0 -lgstbase-1.0 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgstbase-1.0 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lbcrypt -ladvapi32 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgio-2.0 -lgobject-2.0 -lglib-2.0 -lintl -lgobject-2.0 -lglib-2.0 -lintl -lgobject-2.0 -lglib-2.0 -lintl -llegacy_stdio_definitions -lkernel32 -lntdll -luserenv -lws2_32 -ldbghelp +Requires.private: gstrsworkspace, gstreamer-rtp-1.0 >= 1.20, gstreamer-webrtc-1.0 >= 1.20, gstreamer-1.0 >= 1.20, gstreamer-app-1.0 >= 1.20, gstreamer-video-1.0 >= 1.20, gstreamer-sdp-1.0 >= 1.20, gobject-2.0, glib-2.0, gmodule-2.0 + diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstspeechmatics.pc b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstspeechmatics.pc new file mode 100644 index 000000000..c991cd528 --- /dev/null +++ b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstspeechmatics.pc @@ -0,0 +1,13 @@ +prefix=${pcfiledir}/../../.. +exec_prefix=${prefix} +libdir=${prefix}/lib +includedir=${prefix}/include + +Name: gstspeechmatics +Description: GStreamer Speechmatics plugin +Version: 0.15.2 +Libs: -L${libdir}/gstreamer-1.0 -lgstspeechmatics +Cflags: +Libs.private: -lgstaudio-1.0 -lgstbase-1.0 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgstbase-1.0 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lbcrypt -ladvapi32 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgio-2.0 -lgobject-2.0 -lglib-2.0 -lintl -lgobject-2.0 -lglib-2.0 -lintl -lgobject-2.0 -lglib-2.0 -lintl -llegacy_stdio_definitions -lkernel32 -lntdll -luserenv -lws2_32 -ldbghelp +Requires.private: gstrsworkspace, gstreamer-1.0, gstreamer-base-1.0, gobject-2.0, glib-2.0, gmodule-2.0 + diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gststreamgrouper.pc b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gststreamgrouper.pc new file mode 100644 index 000000000..4cba88faf --- /dev/null +++ b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gststreamgrouper.pc @@ -0,0 +1,13 @@ +prefix=${pcfiledir}/../../.. +exec_prefix=${prefix} +libdir=${prefix}/lib +includedir=${prefix}/include + +Name: gststreamgrouper +Description: Filter element that makes all the incoming streams share a group-id +Version: 0.15.2 +Libs: -L${libdir}/gstreamer-1.0 -lgststreamgrouper +Cflags: +Libs.private: -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgio-2.0 -lgobject-2.0 -lglib-2.0 -lintl -lgobject-2.0 -lglib-2.0 -lintl -lgobject-2.0 -lglib-2.0 -lintl -llegacy_stdio_definitions -lkernel32 -lntdll -luserenv -lws2_32 -ldbghelp +Requires.private: gstrsworkspace, gstreamer-1.0, gobject-2.0, glib-2.0, gmodule-2.0 + diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gsttextahead.pc b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gsttextahead.pc new file mode 100644 index 000000000..67836656d --- /dev/null +++ b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gsttextahead.pc @@ -0,0 +1,13 @@ +prefix=${pcfiledir}/../../.. +exec_prefix=${prefix} +libdir=${prefix}/lib +includedir=${prefix}/include + +Name: gsttextahead +Description: GStreamer Plugin for displaying upcoming text buffers ahead of time +Version: 0.15.2 +Libs: -L${libdir}/gstreamer-1.0 -lgsttextahead +Cflags: +Libs.private: -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgio-2.0 -lgobject-2.0 -lglib-2.0 -lintl -lgobject-2.0 -lglib-2.0 -lintl -lgobject-2.0 -lglib-2.0 -lintl -llegacy_stdio_definitions -lkernel32 -lntdll -luserenv -lws2_32 -ldbghelp +Requires.private: gstrsworkspace, gstreamer-1.0, gstreamer-base-1.0, gobject-2.0, glib-2.0, gmodule-2.0 + diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gsttextwrap.pc b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gsttextwrap.pc new file mode 100644 index 000000000..f2bc6a06a --- /dev/null +++ b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gsttextwrap.pc @@ -0,0 +1,13 @@ +prefix=${pcfiledir}/../../.. +exec_prefix=${prefix} +libdir=${prefix}/lib +includedir=${prefix}/include + +Name: gsttextwrap +Description: GStreamer Text Wrap Plugin +Version: 0.15.2 +Libs: -L${libdir}/gstreamer-1.0 -lgsttextwrap +Cflags: +Libs.private: -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgio-2.0 -lgobject-2.0 -lglib-2.0 -lintl -lgobject-2.0 -lglib-2.0 -lintl -lgobject-2.0 -lglib-2.0 -lintl -llegacy_stdio_definitions -lkernel32 -lntdll -luserenv -lws2_32 -ldbghelp +Requires.private: gstrsworkspace, gstreamer-1.0, gobject-2.0, glib-2.0, gmodule-2.0 + diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstthreadshare.pc b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstthreadshare.pc new file mode 100644 index 000000000..9c208580e --- /dev/null +++ b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstthreadshare.pc @@ -0,0 +1,13 @@ +prefix=${pcfiledir}/../../.. +exec_prefix=${prefix} +libdir=${prefix}/lib +includedir=${prefix}/include + +Name: gstthreadshare +Description: GStreamer Threadshare Plugin +Version: 0.15.2 +Libs: -L${libdir}/gstreamer-1.0 -lgstthreadshare +Cflags: +Libs.private: -lgstnet-1.0 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgstaudio-1.0 -lgstbase-1.0 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgstbase-1.0 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgio-2.0 -lgobject-2.0 -lglib-2.0 -lintl -lgobject-2.0 -lglib-2.0 -lintl -lgobject-2.0 -lglib-2.0 -lintl -llegacy_stdio_definitions -lkernel32 -lntdll -luserenv -lws2_32 -ldbghelp +Requires.private: gstrsworkspace, gstreamer-1.0, gstreamer-net-1.0, gstreamer-rtp-1.0, gobject-2.0, glib-2.0, gmodule-2.0 + diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gsttogglerecord.pc b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gsttogglerecord.pc new file mode 100644 index 000000000..5b37fed7e --- /dev/null +++ b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gsttogglerecord.pc @@ -0,0 +1,13 @@ +prefix=${pcfiledir}/../../.. +exec_prefix=${prefix} +libdir=${prefix}/lib +includedir=${prefix}/include + +Name: gsttogglerecord +Description: GStreamer Toggle Record Plugin +Version: 0.15.2 +Libs: -L${libdir}/gstreamer-1.0 -lgsttogglerecord +Cflags: +Libs.private: -lgstvideo-1.0 -lgstbase-1.0 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgstaudio-1.0 -lgstbase-1.0 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgstbase-1.0 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgio-2.0 -lgobject-2.0 -lglib-2.0 -lintl -lgobject-2.0 -lglib-2.0 -lintl -lgobject-2.0 -lglib-2.0 -lintl -llegacy_stdio_definitions -lkernel32 -lntdll -luserenv -lws2_32 -ldbghelp +Requires.private: gstrsworkspace, gstreamer-1.0, gstreamer-audio-1.0, gstreamer-video-1.0, gobject-2.0, glib-2.0, gmodule-2.0 + diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gsturiplaylistbin.pc b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gsturiplaylistbin.pc new file mode 100644 index 000000000..de5d9ced1 --- /dev/null +++ b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gsturiplaylistbin.pc @@ -0,0 +1,13 @@ +prefix=${pcfiledir}/../../.. +exec_prefix=${prefix} +libdir=${prefix}/lib +includedir=${prefix}/include + +Name: gsturiplaylistbin +Description: GStreamer Playlist Playback Plugin +Version: 0.15.2 +Libs: -L${libdir}/gstreamer-1.0 -lgsturiplaylistbin +Cflags: +Libs.private: -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgio-2.0 -lgobject-2.0 -lglib-2.0 -lintl -lgobject-2.0 -lglib-2.0 -lintl -lgobject-2.0 -lglib-2.0 -lintl -llegacy_stdio_definitions -lkernel32 -lntdll -luserenv -lws2_32 -ldbghelp +Requires.private: gstrsworkspace, gstreamer-1.0, gobject-2.0, glib-2.0, gmodule-2.0 + diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstwebrtchttp.pc b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstwebrtchttp.pc new file mode 100644 index 000000000..6bc030959 --- /dev/null +++ b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstwebrtchttp.pc @@ -0,0 +1,13 @@ +prefix=${pcfiledir}/../../.. +exec_prefix=${prefix} +libdir=${prefix}/lib +includedir=${prefix}/include + +Name: gstwebrtchttp +Description: GStreamer WebRTC Plugin for WebRTC HTTP protocols (WHIP/WHEP) +Version: 0.15.2 +Libs: -L${libdir}/gstreamer-1.0 -lgstwebrtchttp +Cflags: +Libs.private: -lgstwebrtc-1.0 -lgstsdp-1.0 -lgstbase-1.0 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgstsdp-1.0 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lbcrypt -ladvapi32 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgio-2.0 -lgobject-2.0 -lglib-2.0 -lintl -lgobject-2.0 -lglib-2.0 -lintl -lgobject-2.0 -lglib-2.0 -lintl -llegacy_stdio_definitions -lkernel32 -lntdll -luserenv -lws2_32 -ldbghelp +Requires.private: gstrsworkspace, gstreamer-1.0, gstreamer-base-1.0, gobject-2.0, glib-2.0, gstreamer-sdp-1.0, gstreamer-webrtc-1.0 + diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/libexec/gstreamer-1.0/gst-plugin-scanner.exe b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/libexec/gstreamer-1.0/gst-plugin-scanner.exe new file mode 100644 index 000000000..3dc07a298 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/libexec/gstreamer-1.0/gst-plugin-scanner.exe differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/libexec/gstreamer-1.0/gst-ptp-helper.exe b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/libexec/gstreamer-1.0/gst-ptp-helper.exe new file mode 100644 index 000000000..9194b1603 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/libexec/gstreamer-1.0/gst-ptp-helper.exe differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/share/glib-2.0/schemas/gschema.dtd b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/share/glib-2.0/schemas/gschema.dtd new file mode 100644 index 000000000..9d7482db7 --- /dev/null +++ b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/share/glib-2.0/schemas/gschema.dtd @@ -0,0 +1,75 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/share/glib-2.0/schemas/gschemas.compiled b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/share/glib-2.0/schemas/gschemas.compiled new file mode 100644 index 000000000..b369f2b28 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/share/glib-2.0/schemas/gschemas.compiled differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/share/glib-2.0/schemas/org.gtk.Demo4.gschema.xml b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/share/glib-2.0/schemas/org.gtk.Demo4.gschema.xml new file mode 100644 index 000000000..3eaa6e8b4 --- /dev/null +++ b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/share/glib-2.0/schemas/org.gtk.Demo4.gschema.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + 'red' + + + (-1, -1) + + + false + + + false + + + + diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/share/glib-2.0/schemas/org.gtk.gtk4.Inspector.gschema.xml b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/share/glib-2.0/schemas/org.gtk.gtk4.Inspector.gschema.xml new file mode 100644 index 000000000..28fa5dd6e --- /dev/null +++ b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/share/glib-2.0/schemas/org.gtk.gtk4.Inspector.gschema.xml @@ -0,0 +1,40 @@ + + + + + + false + Insert debug nodes + + If this setting is true, the recorder will insert debug nodes + into the recording. + + + + false + Record events + + If this setting is true, the recorder will include events + in the recording. + + + + false + Highlight sequences + + If this setting is true, the recorder will highlight events + that are part of an event sequence. + + + + false + Set the recorder to dark + + If this setting is true, the recorder will display render nodes + on a dark background. + + + + + + diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/share/glib-2.0/schemas/org.gtk.gtk4.Settings.ColorChooser.gschema.xml b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/share/glib-2.0/schemas/org.gtk.gtk4.Settings.ColorChooser.gschema.xml new file mode 100644 index 000000000..bedc7030b --- /dev/null +++ b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/share/glib-2.0/schemas/org.gtk.gtk4.Settings.ColorChooser.gschema.xml @@ -0,0 +1,26 @@ + + + + + + [] + Custom colors + + An array of custom colors to show in the color chooser. Each color is + specified as a tuple of four doubles, specifying RGBA values between + 0 and 1. + + + + (false,1.0,1.0,1.0,1.0) + The selected color + + The selected color, described as a tuple whose first member is a + boolean that is true if a color was selected, and the remaining + four members are four doubles, specifying RGBA values between + 0 and 1. + + + + + diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/share/glib-2.0/schemas/org.gtk.gtk4.Settings.Debug.gschema.xml b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/share/glib-2.0/schemas/org.gtk.gtk4.Settings.Debug.gschema.xml new file mode 100644 index 000000000..89428d0c0 --- /dev/null +++ b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/share/glib-2.0/schemas/org.gtk.gtk4.Settings.Debug.gschema.xml @@ -0,0 +1,25 @@ + + + + + + true + Enable inspector keybinding + + If this setting is true, GTK lets the user open an interactive + debugging window with a keybinding. The default shortcuts for + the keybinding are Control-Shift-I and Control-Shift-D. + + + + true + Inspector warning + + If this setting is true, GTK shows a warning before letting + the user use the interactive debugger. + + + + + + diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/share/glib-2.0/schemas/org.gtk.gtk4.Settings.EmojiChooser.gschema.xml b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/share/glib-2.0/schemas/org.gtk.gtk4.Settings.EmojiChooser.gschema.xml new file mode 100644 index 000000000..28c0a62c0 --- /dev/null +++ b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/share/glib-2.0/schemas/org.gtk.gtk4.Settings.EmojiChooser.gschema.xml @@ -0,0 +1,17 @@ + + + + + + [] + Recently used Emoji + + An array of Emoji definitions to show in the Emoji chooser. Each Emoji is + specified as an array of codepoints, name and keywords. The extra + integer after this pair is the code of the Fitzpatrick modifier to use in + place of a modifier placeholder (0 or 0x1F3FB) in the codepoint array. + + + + + diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/share/glib-2.0/schemas/org.gtk.gtk4.Settings.FileChooser.gschema.xml b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/share/glib-2.0/schemas/org.gtk.gtk4.Settings.FileChooser.gschema.xml new file mode 100644 index 000000000..f1f0e054c --- /dev/null +++ b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/share/glib-2.0/schemas/org.gtk.gtk4.Settings.FileChooser.gschema.xml @@ -0,0 +1,186 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 'path-bar' + Location mode + + Controls whether the file chooser shows just a path bar, or a visible entry + for the filename as well, for the benefit of typing-oriented users. The + possible values for these modes are "path-bar" and "filename-entry". + + + + false + Show hidden files + + Controls whether the file chooser shows hidden files or not. + + + + true + Show folders first + + If set to true, then folders are shown before files in the list. + + + + false + Expand folders + This key is deprecated; do not use it. + + + true + Show file sizes + + Controls whether the file chooser shows a column with file sizes. + + + + true + Show file types + + Controls whether the file chooser shows a column with file types. + + + + 'name' + Sort column + + Can be one of "name", "modified", or "size". It controls + which of the columns in the file chooser is used for sorting + the list of files. + + + + 'ascending' + Sort order + + Can be one of the strings "ascending" or "descending". + + + + (-1, -1) + Window position + + This key is ignored. + + + + (-1, -1) + Window size + + The size (width, height) of the GtkFileChooserDialog's window, in pixels. + + + + 'recent' + Startup mode + + Either "recent" or "cwd"; controls whether the file chooser + starts up showing the list of recently-used files, or the + contents of the current working directory. + + + + -1 + Sidebar width + + Width in pixels of the file chooser's places sidebar. + + + + '24h' + Time format + + Whether the time is shown in 24h or 12h format. + + + + 'regular' + Date format + + The amount of detail to show in the Modified column. + + + + 'category' + Type format + + Different ways to show the 'Type' column information. + Example outputs for a video mp4 file: + 'mime' -> 'video/mp4' + 'description' -> 'MPEG-4 video' + 'category' -> 'Video' + + + + 'list' + View type + + Whether the files are shown in a list or in a grid. + + + + + diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/share/gstreamer-1.0/validate/scenarios/adaptive_video_framerate.scenario b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/share/gstreamer-1.0/validate/scenarios/adaptive_video_framerate.scenario new file mode 100644 index 000000000..a3043af08 --- /dev/null +++ b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/share/gstreamer-1.0/validate/scenarios/adaptive_video_framerate.scenario @@ -0,0 +1,5 @@ +description, duration=15.0 +set-restriction, playback-time=5.0, restriction-caps="video/x-raw,framerate=(fraction)5/1" +set-restriction, playback-time=10.0, restriction-caps="video/x-raw,framerate=(fraction)30/1" +eos, playback-time=15.0 +stop, playback-time=15.0 diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/share/gstreamer-1.0/validate/scenarios/adaptive_video_framerate_size.scenario b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/share/gstreamer-1.0/validate/scenarios/adaptive_video_framerate_size.scenario new file mode 100644 index 000000000..d5cf5963a --- /dev/null +++ b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/share/gstreamer-1.0/validate/scenarios/adaptive_video_framerate_size.scenario @@ -0,0 +1,7 @@ +description, duration=25.0 +set-restriction, playback-time=5.0, restriction-caps="video/x-raw,framerate=(fraction)5/1" +set-restriction, playback-time=10.0, restriction-caps="video/x-raw,height=20,width=20,framerate=(fraction)5/1" +set-restriction, playback-time=15.0, restriction-caps="video/x-raw,height=20,width=20,framerate=(fraction)30/1" +set-restriction, playback-time=20.0, restriction-caps="video/x-raw,height=720,width=1280,framerate=(fraction)30/1" +eos, playback-time=25.0 +stop, playback-time=25.0 diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/share/gstreamer-1.0/validate/scenarios/adaptive_video_size.scenario b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/share/gstreamer-1.0/validate/scenarios/adaptive_video_size.scenario new file mode 100644 index 000000000..c3b5d2ef9 --- /dev/null +++ b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/share/gstreamer-1.0/validate/scenarios/adaptive_video_size.scenario @@ -0,0 +1,5 @@ +description, duration=15.0 +set-restriction, playback-time=5.0, restriction-caps="video/x-raw,height=480,width=854" +set-restriction, playback-time=10.0, restriction-caps="video/x-raw,height=720,width=1280" +eos, playback-time=15.0 +stop, playback-time=15.0 diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/share/gstreamer-1.0/validate/scenarios/alternate_fast_backward_forward.scenario b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/share/gstreamer-1.0/validate/scenarios/alternate_fast_backward_forward.scenario new file mode 100644 index 000000000..59138982e --- /dev/null +++ b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/share/gstreamer-1.0/validate/scenarios/alternate_fast_backward_forward.scenario @@ -0,0 +1,14 @@ +description, duration=55.0, min-media-duration=470.0, seek=true, reverse-playback=true +include,location=includes/default-seek-flags.scenario +seek, name=backward-seek, playback-time=0.0, rate=-1.0, start=0.0, stop=310.0, flags="$(default_flags)" +seek, name=forward-seek, playback-time=305.0, rate=1.0, start=305.0, flags="$(default_flags)" +seek, name=Fast-forward-seek, playback-time=310.0, rate=2.0, start=310.0, flags="$(default_flags)" +seek, name=Fast-backward-seek, playback-time=320.0, rate=-2.0, start=0.0, stop=320.0, flags="$(default_flags)" +seek, name=Fast-forward-seek, playback-time=310.0, rate=4.0, start=310.0, flags="$(default_flags)" +seek, name=Fast-backward-seek, playback-time=330.0, rate=-4.0, start=0.0, stop=330.0, flags="$(default_flags)" +seek, name=Fast-forward-seek, playback-time=310.0, rate=8.0, start=310.0, flags="$(default_flags)" +seek, name=Fast-backward-seek, playback-time=350.0, rate=-8.0, start=0.0, stop=350.0, flags="$(default_flags)" +seek, name=Fast-forward-seek, playback-time=310.0, rate=16.0, start=310.0, flags="$(default_flags)" +seek, name=Fast-backward-seek, playback-time=390.0, rate=-16.0, start=0.0, stop=390.0, flags="$(default_flags)" +seek, name=Fast-forward-seek, playback-time=310.0, rate=32.0, start=310.0, flags="$(default_flags)" +seek, name=Fast-backward-seek, playback-time=470.0, rate=-32.0, start=310.0, stop=470.0, flags="$(default_flags)" diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/share/gstreamer-1.0/validate/scenarios/change_state_intensive.scenario b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/share/gstreamer-1.0/validate/scenarios/change_state_intensive.scenario new file mode 100644 index 000000000..042d6fbc1 --- /dev/null +++ b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/share/gstreamer-1.0/validate/scenarios/change_state_intensive.scenario @@ -0,0 +1,8 @@ +description, duration=0, summary="Set state to NULL->PLAYING->NULL 20 times", need-clock-sync=true, min-media-duration=1.0, live_content_compatible=True, handles-states=true, ignore-eos=true + +foreach, i=[0, 40], + actions = { + "set-state, state=playing", + "set-state, state=null", + } +stop; diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/share/gstreamer-1.0/validate/scenarios/disable_subtitle_track_while_paused.scenario b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/share/gstreamer-1.0/validate/scenarios/disable_subtitle_track_while_paused.scenario new file mode 100644 index 000000000..3c679c501 --- /dev/null +++ b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/share/gstreamer-1.0/validate/scenarios/disable_subtitle_track_while_paused.scenario @@ -0,0 +1,6 @@ +description, summary="Disable subtitle track while pipeline is PAUSED", min-subtitle-track=2, duration=5.0, handles-states=true, needs_preroll=true +pause; +switch-track, name="Disable subtitle", type=text, disable=true +wait, duration=0.5 +play; +stop, playback-time=2.0 diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/share/gstreamer-1.0/validate/scenarios/fast_backward.scenario b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/share/gstreamer-1.0/validate/scenarios/fast_backward.scenario new file mode 100644 index 000000000..f16072d50 --- /dev/null +++ b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/share/gstreamer-1.0/validate/scenarios/fast_backward.scenario @@ -0,0 +1,9 @@ +description, duration=30.0, minfo-media-duration=310.0, seek=true, reverse-playback=true, need-clock-sync=true, min-media-duration=310.0, ignore-eos=true +include,location=includes/default-seek-flags.scenario +seek, name=Fast-backward-seek, playback-time=0.0, rate=-2.0, start=0.0, stop=310.0, flags="$(default_flags)" +seek, name=Fast-backward-seek, playback-time=300.0, rate=-4.0, start=0.0, stop=300.0, flags="$(default_flags)" +seek, name=Fast-backward-seek, playback-time=280.0, rate=-8.0, start=0.0, stop=280.0, flags="$(default_flags)" +seek, name=Fast-backward-seek, playback-time=240.0, rate=-16.0, start=0.0, stop=240.0, flags="$(default_flags)" +seek, name=Fast-backward-seek, playback-time=160.0, rate=-32.0, start=0.0, stop=160.0, flags="$(default_flags)" +wait, message-type=eos +stop \ No newline at end of file diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/share/gstreamer-1.0/validate/scenarios/fast_forward.scenario b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/share/gstreamer-1.0/validate/scenarios/fast_forward.scenario new file mode 100644 index 000000000..89855a609 --- /dev/null +++ b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/share/gstreamer-1.0/validate/scenarios/fast_forward.scenario @@ -0,0 +1,8 @@ +description, duration=25.0, seek=true, need-clock-sync=true, min-media-duration=5.0, ignore-eos=true +include,location=includes/default-seek-flags.scenario +seek, name=Fast-forward-seek, playback-time=0.0, rate=2.0, start=0.0, flags="$(default_flags)" +seek, name=Fast-forward-seek, playback-time="min(10.0, $(duration) * 0.0625)", rate=4.0, start=0.0, flags="$(default_flags)" +seek, name=Fast-forward-seek, playback-time="min(20.0, $(duration) * 0.125)", rate=8.0, start=0.0, flags="$(default_flags)" +seek, name=Fast-forward-seek, playback-time="min(40.0, $(duration) * 0.25)", rate=16.0, start=0.0, flags="$(default_flags)" +seek, name=Fast-forward-seek, playback-time="min(80.0, $(duration) * 0.50)", rate=32.0, start=0.0, flags="$(default_flags)" +stop, playback-time="min($(duration) - 0.3, 160.0)", on-message="eos" diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/share/gstreamer-1.0/validate/scenarios/force_key_unit.scenario b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/share/gstreamer-1.0/validate/scenarios/force_key_unit.scenario new file mode 100644 index 000000000..2a9c8394d --- /dev/null +++ b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/share/gstreamer-1.0/validate/scenarios/force_key_unit.scenario @@ -0,0 +1,4 @@ +description, duration=2.0 +video-request-key-unit, playback-time=1.0, direction=upstream, running_time=-1.0, all-header=true, count=1 +eos, playback-time=2.0 +stop, playback-time=2.0 diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/share/gstreamer-1.0/validate/scenarios/force_rtsp2.scenario b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/share/gstreamer-1.0/validate/scenarios/force_rtsp2.scenario new file mode 100644 index 000000000..0d957b6d2 --- /dev/null +++ b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/share/gstreamer-1.0/validate/scenarios/force_rtsp2.scenario @@ -0,0 +1 @@ +set-property, target-element-factory-name="rtspsrc", property-name=default-rtsp-version, property-value=(string)"2-0" diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/share/gstreamer-1.0/validate/scenarios/pause_resume.scenario b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/share/gstreamer-1.0/validate/scenarios/pause_resume.scenario new file mode 100644 index 000000000..27bfc1090 --- /dev/null +++ b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/share/gstreamer-1.0/validate/scenarios/pause_resume.scenario @@ -0,0 +1,6 @@ +description, duration=14.0, min-media-duration=7.0 +pause, name=First-pause, playback-time=1.0, duration=1.0 +pause, name=Second-pause, playback-time=3.0, duration=5.0 +pause, name=Third-pause, playback-time=5.0, duration=1.0 +eos, name=Done-testing, playback-time=7.0 +stop, playback-time=7.0 diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/share/gstreamer-1.0/validate/scenarios/play_15s.scenario b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/share/gstreamer-1.0/validate/scenarios/play_15s.scenario new file mode 100644 index 000000000..af86cb35f --- /dev/null +++ b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/share/gstreamer-1.0/validate/scenarios/play_15s.scenario @@ -0,0 +1,3 @@ +description, duration=15.0 +eos, playback-time=15.0 +stop, playback-time=15.0 diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/share/gstreamer-1.0/validate/scenarios/play_5s.scenario b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/share/gstreamer-1.0/validate/scenarios/play_5s.scenario new file mode 100644 index 000000000..40186e8f0 --- /dev/null +++ b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/share/gstreamer-1.0/validate/scenarios/play_5s.scenario @@ -0,0 +1,3 @@ +description, duration=5.0 +eos, playback-time=5.0 +stop, playback-time=5.0 diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/share/gstreamer-1.0/validate/scenarios/reverse_playback.scenario b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/share/gstreamer-1.0/validate/scenarios/reverse_playback.scenario new file mode 100644 index 000000000..90e02cdce --- /dev/null +++ b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/share/gstreamer-1.0/validate/scenarios/reverse_playback.scenario @@ -0,0 +1,3 @@ +description, seek=true, reverse-playback=true +include,location=includes/default-seek-flags.scenario +seek, name=Reverse-seek, playback-time=0.0, rate=-1.0, start="max($(duration) - 15.0, 0.0)", stop="$(duration)", flags="$(default_flags)" diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/share/gstreamer-1.0/validate/scenarios/scrub_backward_seeking.scenario b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/share/gstreamer-1.0/validate/scenarios/scrub_backward_seeking.scenario new file mode 100644 index 000000000..3a8ff4784 --- /dev/null +++ b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/share/gstreamer-1.0/validate/scenarios/scrub_backward_seeking.scenario @@ -0,0 +1,8 @@ +description, seek=true, handles-states=true, needs_preroll=true +include,location=includes/default-seek-flags.scenario +pause, playback-time=0.0 +seek, playback-time=0.0, start="$(duration) - 0.5", flags="$(default_flags)" +seek, playback-time=0.0, start=position-0.1, repeat="min(10, ($(duration) - 0.6))/0.1", flags="$(default_flags)" +play, playback-time=0.0 +stop, playback-time=1.0 + diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/share/gstreamer-1.0/validate/scenarios/scrub_backward_seeking_full.scenario b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/share/gstreamer-1.0/validate/scenarios/scrub_backward_seeking_full.scenario new file mode 100644 index 000000000..7cb1f7abe --- /dev/null +++ b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/share/gstreamer-1.0/validate/scenarios/scrub_backward_seeking_full.scenario @@ -0,0 +1,8 @@ +description, seek=true, handles-states=true, needs_preroll=true +include,location=includes/default-seek-flags.scenario +pause, playback-time=0.0 +seek, playback-time=0.0, start="$(duration) - 0.5", flags="$(default_flags)" +seek, playback-time=0.0, start=position-0.1, repeat="($(duration) - 0.6)/0.1", flags="$(default_flags)" +play, playback-time=0.0 +stop, playback-time=1.0 + diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/share/gstreamer-1.0/validate/scenarios/scrub_forward_seeking.scenario b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/share/gstreamer-1.0/validate/scenarios/scrub_forward_seeking.scenario new file mode 100644 index 000000000..814dce4fe --- /dev/null +++ b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/share/gstreamer-1.0/validate/scenarios/scrub_forward_seeking.scenario @@ -0,0 +1,6 @@ +description, seek=true, handles-states=true, needs_preroll=true +include,location=includes/default-seek-flags.scenario +pause, playback-time=0.0 +seek, playback-time=0.0, start=position+0.1, repeat="min(10, ($(duration) - 0.5) / 0.1)", flags="$(default_flags)" +play, playback-time=0.0 +stop, playback-time=1.0 diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/share/gstreamer-1.0/validate/scenarios/scrub_forward_seeking_full.scenario b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/share/gstreamer-1.0/validate/scenarios/scrub_forward_seeking_full.scenario new file mode 100644 index 000000000..d83c8b1e9 --- /dev/null +++ b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/share/gstreamer-1.0/validate/scenarios/scrub_forward_seeking_full.scenario @@ -0,0 +1,6 @@ +description, seek=true, handles-states=true, needs_preroll=true +include,location=includes/default-seek-flags.scenario +pause, playback-time=0.0 +seek, playback-time=0.0, start=position+0.1, repeat="($(duration) - 0.5)/0.1", flags="$(default_flags)" +play, playback-time=0.0 +stop, playback-time=1.0 diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/share/gstreamer-1.0/validate/scenarios/seek_backward.scenario b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/share/gstreamer-1.0/validate/scenarios/seek_backward.scenario new file mode 100644 index 000000000..66c9cd3dd --- /dev/null +++ b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/share/gstreamer-1.0/validate/scenarios/seek_backward.scenario @@ -0,0 +1,6 @@ +description, seek=true, duration=30, need-clock-sync=true, ignore-eos=true +include,location=includes/default-seek-flags.scenario +seek, name=Backward-seek, playback-time="min(5.0, ($(duration) / 4))", rate=1.0, start=0.0, flags="$(default_flags)" +seek, name=Backward-seek, playback-time="min(10.0, 2*($(duration) / 4))", rate=1.0, start="min(5.0, $(duration) / 4)", flags="$(default_flags)" +seek, name=Backward-seek, playback-time="min(15.0, 3*($(duration) / 4))", rate=1.0, start="min(10.0, 2*($(duration) / 4))", flags="$(default_flags)" +stop, playback-time="min(15.0, 3*($(duration) / 4))" diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/share/gstreamer-1.0/validate/scenarios/seek_forward.scenario b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/share/gstreamer-1.0/validate/scenarios/seek_forward.scenario new file mode 100644 index 000000000..9949e4149 --- /dev/null +++ b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/share/gstreamer-1.0/validate/scenarios/seek_forward.scenario @@ -0,0 +1,6 @@ +description, seek=true, duration=20, need-clock-sync=true, ignore-eos=true +include,location=includes/default-seek-flags.scenario +seek, name=First-forward-seek, playback-time="min(5.0, ($(duration)/8))", start="min(10, 2*($(duration)/8))", flags="$(default_flags)" +seek, name=Second-forward-seek, playback-time="min(15.0, 3*($(duration)/8))", start="min(20, 4*($(duration)/8))", flags="$(default_flags)" +seek, name=Third-forward-seek, playback-time="min(25, 5*($(duration)/8))", start="min(30.0, 6*($(duration)/8))", flags="$(default_flags)" +stop, playback-time="min($(duration) - 1, 35)", on-message=eos \ No newline at end of file diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/share/gstreamer-1.0/validate/scenarios/seek_forward_backward.scenario b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/share/gstreamer-1.0/validate/scenarios/seek_forward_backward.scenario new file mode 100644 index 000000000..4b669aff7 --- /dev/null +++ b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/share/gstreamer-1.0/validate/scenarios/seek_forward_backward.scenario @@ -0,0 +1,10 @@ +description, seek=true, duration=40, min-media-duration=45.0 +include,location=includes/default-seek-flags.scenario +seek, name=Forward-seek, playback-time=0.0, rate=1.0, start=5.0, flags="$(default_flags)" +seek, name=Backward-seek, playback-time=10.0, rate=1.0, start=0.0, flags="$(default_flags)" +seek, name=Backward-seek, playback-time=5.0, rate=1.0, start=25.0, stop=-1, flags="$(default_flags)" +seek, name=Backward-seek, playback-time=30.0, rate=1.0, start=0.0, flags="$(default_flags)" +seek, name=Forward-seek, playback-time=5.0, rate=1.0, start=15.0, flags="$(default_flags)" +seek, name=Forward-seek, playback-time=20.0, rate=1.0, start=35.0, flags="$(default_flags)" +seek, name=Backward-seek, playback-time=40.0, rate=1.0, start=25.0, flags="$(default_flags)" +seek, name=Last-backward-seek, playback-time=30.0, rate=1.0, start=5.0, stop=10.0, flags="$(default_flags)" diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/share/gstreamer-1.0/validate/scenarios/seek_with_stop.scenario b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/share/gstreamer-1.0/validate/scenarios/seek_with_stop.scenario new file mode 100644 index 000000000..b4b7e3f60 --- /dev/null +++ b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/share/gstreamer-1.0/validate/scenarios/seek_with_stop.scenario @@ -0,0 +1,3 @@ +description, seek=true, duration=5.0, need_clock_sync=true, min-media-duration=2 +include,location=includes/default-seek-flags.scenario +seek, playback-time=1.0, start=0.0, stop="min(5.0, duration-1.0)", flags="$(default_flags)" diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/share/gstreamer-1.0/validate/scenarios/simple_seeks.scenario b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/share/gstreamer-1.0/validate/scenarios/simple_seeks.scenario new file mode 100644 index 000000000..ca41f6ca2 --- /dev/null +++ b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/share/gstreamer-1.0/validate/scenarios/simple_seeks.scenario @@ -0,0 +1,5 @@ +description, seek=true, duration=5.0 +include,location=includes/default-seek-flags.scenario +seek, playback-time=1.0, rate=1.0, start=2.0, flags="$(default_flags)" +seek, playback-time=3.0, rate=1.0, start=0.0, flags="$(default_flags)" +seek, playback-time=1.0, rate=1.0, start=2.0, stop=3.0, flags="$(default_flags)" diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/share/gstreamer-1.0/validate/scenarios/switch_audio_track.scenario b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/share/gstreamer-1.0/validate/scenarios/switch_audio_track.scenario new file mode 100644 index 000000000..b1a968b66 --- /dev/null +++ b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/share/gstreamer-1.0/validate/scenarios/switch_audio_track.scenario @@ -0,0 +1,3 @@ +description, summary="Change audio track at 5 second to the second audio track", min-audio-track=2, duration=10.0, min-media-duration=5.1 +switch-track, name=Next-audio-track, playback-time=5.0, type=audio, index=(string)+1 +stop, playback-time=10.0 diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/share/gstreamer-1.0/validate/scenarios/switch_audio_track_while_paused.scenario b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/share/gstreamer-1.0/validate/scenarios/switch_audio_track_while_paused.scenario new file mode 100644 index 000000000..fd4c36249 --- /dev/null +++ b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/share/gstreamer-1.0/validate/scenarios/switch_audio_track_while_paused.scenario @@ -0,0 +1,11 @@ +description, summary="Change audio track while pipeline is paused", min-audio-track=2, duration=6.0, need-clock-sync=true, needs_preroll=true +pause, playback-time=1.0; + +# Wait so that humans can see the pipeline is paused +wait, duration=0.5 +switch-track, name=Next-audio-track, type=audio, index=(string)+1 + +# Wait so that humans can see the pipeline is paused +wait, duration=0.5 +play; +stop, playback-time=5.0 diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/share/gstreamer-1.0/validate/scenarios/switch_subtitle_track.scenario b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/share/gstreamer-1.0/validate/scenarios/switch_subtitle_track.scenario new file mode 100644 index 000000000..216e7ce91 --- /dev/null +++ b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/share/gstreamer-1.0/validate/scenarios/switch_subtitle_track.scenario @@ -0,0 +1,3 @@ +description, summary="Change subtitle track at 1 second while playing back", min-subtitle-track=2, duration=5.0, need-clock-sync=true +switch-track, playback-time=1.0, type=text, index=(string)+1 +stop, playback-time=5.0 diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/share/gstreamer-1.0/validate/scenarios/switch_subtitle_track_while_paused.scenario b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/share/gstreamer-1.0/validate/scenarios/switch_subtitle_track_while_paused.scenario new file mode 100644 index 000000000..611914256 --- /dev/null +++ b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/share/gstreamer-1.0/validate/scenarios/switch_subtitle_track_while_paused.scenario @@ -0,0 +1,7 @@ +description, summary="Change subtitle track while pipeline is PAUSED", min-subtitle-track=2, duration=5.0, handles-states=true, need-clock-sync=true, needs_preroll=true +pause; +wait, duration=0.5 +switch-track, type=text, index=(string)+1 +wait, duration=0.5 +play; +stop, playback-time=5.0 diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gthread-2.0-0.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gthread-2.0-0.dll new file mode 100644 index 000000000..e13a20a88 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/gthread-2.0-0.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/intl-8.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/intl-8.dll new file mode 100644 index 000000000..d37f3a60f Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/intl-8.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/msvcp140.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/msvcp140.dll new file mode 100644 index 000000000..5153fb087 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/msvcp140.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/msvcp140_1.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/msvcp140_1.dll new file mode 100644 index 000000000..fe6169ee5 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/msvcp140_1.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/msvcp140_2.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/msvcp140_2.dll new file mode 100644 index 000000000..967be8237 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/msvcp140_2.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/msvcp140_atomic_wait.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/msvcp140_atomic_wait.dll new file mode 100644 index 000000000..01f8e9a36 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/msvcp140_atomic_wait.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/msvcp140_codecvt_ids.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/msvcp140_codecvt_ids.dll new file mode 100644 index 000000000..63214b8a8 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/msvcp140_codecvt_ids.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/orc-0.4-0.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/orc-0.4-0.dll new file mode 100644 index 000000000..5270f9f69 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/orc-0.4-0.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/pcre2-8-0.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/pcre2-8-0.dll new file mode 100644 index 000000000..092953e23 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/pcre2-8-0.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/vcruntime140.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/vcruntime140.dll new file mode 100644 index 000000000..c2f509d20 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/vcruntime140.dll differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/vcruntime140_1.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/vcruntime140_1.dll new file mode 100644 index 000000000..64cd24630 Binary files /dev/null and b/opennow-stable/native/opennow-streamer/bin/win32-x64/vcruntime140_1.dll differ diff --git a/opennow-stable/package-lock.json b/opennow-stable/package-lock.json index 55f7e6b4d..0c33fbd79 100644 --- a/opennow-stable/package-lock.json +++ b/opennow-stable/package-lock.json @@ -9,42 +9,94 @@ "version": "0.5.2", "hasInstallScript": true, "dependencies": { + "@formkit/auto-animate": "^0.10.0", + "@react-three/fiber": "^9.6.1", "discord-rpc": "^4.0.1", - "electron-updater": "^6.8.3", - "lucide-react": "^1.17.0", - "motion": "^12.42.0", + "electron-updater": "^6.8.9", + "lucide-react": "^1.25.0", + "motion": "^12.42.2", + "posthog-js": "^1.406.2", + "posthog-node": "^5.46.0", "qrcode": "^1.5.4", - "react": "^19.2.6", - "react-dom": "^19.2.6", - "ws": "^8.21.0" + "react": "^19.2.7", + "react-dom": "^19.2.7", + "three": "^0.185.1", + "ws": "^8.21.1" }, "devDependencies": { - "@types/discord-rpc": "^4.0.10", - "@types/node": "^22.19.17", + "@types/discord-rpc": "^4.0.11", + "@types/node": "^22.20.1", "@types/qrcode": "^1.5.6", - "@types/react": "^19.2.14", + "@types/react": "^19.2.17", "@types/react-dom": "^19.2.3", + "@types/three": "^0.185.1", "@types/ws": "^8.18.1", "@vitejs/plugin-react": "^5.2.0", "cross-env": "^10.1.0", - "electron": "^42.3.3", - "electron-builder": "^26.8.1", + "electron": "^43.1.1", + "electron-builder": "^26.15.3", "electron-vite": "^5.0.0", - "oxlint": "^1.67.0", - "react-scan": "^0.5.3", - "tsx": "^4.22.3", + "oxlint": "^1.74.0", + "react-scan": "^0.5.7", + "tsx": "^4.23.1", "typescript": "^6.0.3", - "vite": "^7.3.5" + "vite": "^7.3.6" + } + }, + "node_modules/@apm-js-collab/code-transformer": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/@apm-js-collab/code-transformer/-/code-transformer-0.18.0.tgz", + "integrity": "sha512-aN3Oq8r1J3gPJtCwErP664gM0+HhM1I1lujPr9TMTCcEl/joQQbpGpeMdts9B1+W2wHMsvioDMv5F4PvMWE6gw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/estree": "^1.0.8", + "astring": "^1.9.0", + "esquery": "^1.7.0", + "meriyah": "^6.1.4", + "semifies": "^1.0.0", + "source-map": "^0.6.0" + }, + "bin": { + "code-transformer": "cli.js" + } + }, + "node_modules/@apm-js-collab/code-transformer-bundler-plugins": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/@apm-js-collab/code-transformer-bundler-plugins/-/code-transformer-bundler-plugins-0.6.2.tgz", + "integrity": "sha512-5vBrtIEL+UVbO0YWWoyYG4QMgR+ZfnIL3xlteIkAmU7YaAPhc28k3md/NM14tnkfXKjKOn9yUzEA9AYUmNpvJg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@apm-js-collab/code-transformer": "^0.18.0", + "es-module-lexer": "^2.1.0", + "magic-string": "^0.30.21", + "module-details-from-path": "^1.0.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@apm-js-collab/tracing-hooks": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@apm-js-collab/tracing-hooks/-/tracing-hooks-0.13.0.tgz", + "integrity": "sha512-mTvWz9rnQwx1U3h0XPTHaX7bgfkpipLLTQyjlC2cdhQpQEuoLT0AGzoydeoq2NxfEVv6fWOOETcSbb2nptleyw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@apm-js-collab/code-transformer": "^0.18.0", + "debug": "^4.4.1", + "module-details-from-path": "^1.0.4" } }, "node_modules/@babel/code-frame": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", - "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-validator-identifier": "^7.28.5", + "@babel/helper-validator-identifier": "^7.29.7", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" }, @@ -53,9 +105,9 @@ } }, "node_modules/@babel/compat-data": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", - "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", "dev": true, "license": "MIT", "engines": { @@ -63,21 +115,21 @@ } }, "node_modules/@babel/core": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", - "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.29.0", - "@babel/generator": "^7.29.0", - "@babel/helper-compilation-targets": "^7.28.6", - "@babel/helper-module-transforms": "^7.28.6", - "@babel/helpers": "^7.28.6", - "@babel/parser": "^7.29.0", - "@babel/template": "^7.28.6", - "@babel/traverse": "^7.29.0", - "@babel/types": "^7.29.0", + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", @@ -93,25 +145,15 @@ "url": "https://opencollective.com/babel" } }, - "node_modules/@babel/core/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/@babel/generator": { - "version": "7.29.1", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", - "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz", + "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/parser": "^7.29.0", - "@babel/types": "^7.29.0", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" @@ -121,14 +163,14 @@ } }, "node_modules/@babel/helper-compilation-targets": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", - "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", "dev": true, "license": "MIT", "dependencies": { - "@babel/compat-data": "^7.28.6", - "@babel/helper-validator-option": "^7.27.1", + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" @@ -137,20 +179,10 @@ "node": ">=6.9.0" } }, - "node_modules/@babel/helper-compilation-targets/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/@babel/helper-globals": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", - "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", "dev": true, "license": "MIT", "engines": { @@ -158,29 +190,29 @@ } }, "node_modules/@babel/helper-module-imports": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", - "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", "dev": true, "license": "MIT", "dependencies": { - "@babel/traverse": "^7.28.6", - "@babel/types": "^7.28.6" + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-transforms": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", - "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-module-imports": "^7.28.6", - "@babel/helper-validator-identifier": "^7.28.5", - "@babel/traverse": "^7.28.6" + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -190,9 +222,9 @@ } }, "node_modules/@babel/helper-plugin-utils": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", - "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz", + "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==", "dev": true, "license": "MIT", "engines": { @@ -200,9 +232,9 @@ } }, "node_modules/@babel/helper-string-parser": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", - "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", "dev": true, "license": "MIT", "engines": { @@ -210,9 +242,9 @@ } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", - "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", "dev": true, "license": "MIT", "engines": { @@ -220,9 +252,9 @@ } }, "node_modules/@babel/helper-validator-option": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", - "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", "dev": true, "license": "MIT", "engines": { @@ -230,27 +262,27 @@ } }, "node_modules/@babel/helpers": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz", - "integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/template": "^7.28.6", - "@babel/types": "^7.28.6" + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/parser": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", - "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.29.0" + "@babel/types": "^7.29.7" }, "bin": { "parser": "bin/babel-parser.js" @@ -260,13 +292,13 @@ } }, "node_modules/@babel/plugin-transform-arrow-functions": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.27.1.tgz", - "integrity": "sha512-8Z4TGic6xW70FKThA5HYEKKyBpOOsucTOD1DjU3fZxDg+K3zBJcXMFnt/4yQiZnf5+MiOMSXQ9PaEK/Ilh1DeA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.29.7.tgz", + "integrity": "sha512-N7zArUXWzAMzm+/N0uPBeVB3Fam5lMxtUwMmDK5f/IBBS7a7p1qeUoxd/6CckXoxUdgsntq1Dh8xNW06maZbDQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -276,13 +308,13 @@ } }, "node_modules/@babel/plugin-transform-react-jsx-self": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", - "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.29.7.tgz", + "integrity": "sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -292,13 +324,13 @@ } }, "node_modules/@babel/plugin-transform-react-jsx-source": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", - "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.29.7.tgz", + "integrity": "sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -307,34 +339,43 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/runtime": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz", + "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/template": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", - "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.28.6", - "@babel/parser": "^7.28.6", - "@babel/types": "^7.28.6" + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", - "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz", + "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.29.0", - "@babel/generator": "^7.29.0", - "@babel/helper-globals": "^7.28.0", - "@babel/parser": "^7.29.0", - "@babel/template": "^7.28.6", - "@babel/types": "^7.29.0", + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7", "debug": "^4.3.1" }, "engines": { @@ -342,35 +383,34 @@ } }, "node_modules/@babel/types": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", - "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-string-parser": "^7.27.1", - "@babel/helper-validator-identifier": "^7.28.5" + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" }, "engines": { "node": ">=6.9.0" } }, - "node_modules/@develar/schema-utils": { - "version": "2.6.5", - "resolved": "https://registry.npmjs.org/@develar/schema-utils/-/schema-utils-2.6.5.tgz", - "integrity": "sha512-0cp4PsWQ/9avqTVMCtZ+GirikIA36ikvjtHweU4/j8yLtgObI0+JUPhYFScgwlteveGB1rt3Cm8UhN04XayDig==", + "node_modules/@dimforge/rapier3d-compat": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@dimforge/rapier3d-compat/-/rapier3d-compat-0.12.0.tgz", + "integrity": "sha512-uekIGetywIgopfD97oDL5PfeezkFpNhwlzlaEYNOA0N6ghdsOvh/HYjSMek5Q2O1PYvRSDFcqFVJl4r4ZBwOow==", "dev": true, - "license": "MIT", - "dependencies": { - "ajv": "^6.12.0", - "ajv-keywords": "^3.4.1" - }, + "license": "Apache-2.0" + }, + "node_modules/@electron-internal/extract-zip": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@electron-internal/extract-zip/-/extract-zip-1.0.4.tgz", + "integrity": "sha512-Zr1Vs7E9tpCNhZHDAbFVXc2gEVCG9RqPDjrno5+bdgB6LRAuvgyMHJut4NCVyYwtAieapMzc3fiQ3CSTi75ARg==", + "dev": true, + "license": "BSD-2-Clause", "engines": { - "node": ">= 8.9.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" + "node": ">=22.12.0" } }, "node_modules/@electron/asar": { @@ -391,20 +431,28 @@ "node": ">=10.12.0" } }, - "node_modules/@electron/asar/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==", + "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.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", "dev": true, "license": "MIT", - "engines": { - "node": ">= 6" + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" } }, "node_modules/@electron/asar/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "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": { @@ -414,24 +462,6 @@ "node": "*" } }, - "node_modules/@electron/asar/node_modules/minimatch/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/@electron/asar/node_modules/minimatch/node_modules/brace-expansion/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/fuses": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/@electron/fuses/-/fuses-1.8.0.tgz", @@ -484,6 +514,19 @@ "undici": "^7.24.4" } }, + "node_modules/@electron/get/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/@electron/notarize": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/@electron/notarize/-/notarize-2.5.0.tgz", @@ -551,25 +594,18 @@ } }, "node_modules/@electron/rebuild": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/@electron/rebuild/-/rebuild-4.0.3.tgz", - "integrity": "sha512-u9vpTHRMkOYCs/1FLiSVAFZ7FbjsXK+bQuzviJZa+lG7BHZl1nz52/IcGvwa3sk80/fc3llutBkbCq10Vh8WQA==", + "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", - "detect-libc": "^2.0.1", - "got": "^11.7.0", - "graceful-fs": "^4.2.11", "node-abi": "^4.2.0", "node-api-version": "^0.2.1", - "node-gyp": "^11.2.0", - "ora": "^5.1.0", - "read-binary-file-arch": "^1.0.6", - "semver": "^7.3.5", - "tar": "^7.5.6", - "yargs": "^17.0.1" + "node-gyp": "^12.2.0", + "read-binary-file-arch": "^1.0.6" }, "bin": { "electron-rebuild": "lib/cli.js" @@ -597,10 +633,27 @@ "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.0.3", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz", + "integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, "node_modules/@electron/universal/node_modules/fs-extra": { - "version": "11.3.3", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.3.tgz", - "integrity": "sha512-VWSRii4t0AFm6ixFFmLLx1t7wS1gh+ckoa84aOeapGum0h+EZd1EhEumSB+ZdDLnEPuucsVB9oB7cxJHap6Afg==", + "version": "11.3.6", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.6.tgz", + "integrity": "sha512-w8ZNZr2mKIc7qeNaQ9AVPT1+iFaI+Avd4xudVOvdDJ8VytREi1Ft5Ih7hd9jjehod8vAM5GMsfQ/TpPf4EyoEA==", "dev": true, "license": "MIT", "dependencies": { @@ -613,13 +666,13 @@ } }, "node_modules/@electron/universal/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "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.1" + "brace-expansion": "^2.0.2" }, "engines": { "node": ">=16 || 14 >=14.17" @@ -628,23 +681,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/@electron/universal/node_modules/minimatch/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/@electron/universal/node_modules/minimatch/node_modules/brace-expansion/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/windows-sign": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/@electron/windows-sign/-/windows-sign-1.2.2.tgz", @@ -668,9 +704,9 @@ } }, "node_modules/@electron/windows-sign/node_modules/fs-extra": { - "version": "11.3.5", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.5.tgz", - "integrity": "sha512-eKpRKAovdpZtR1WopLHxlBWvAgPny3c4gX1G5Jhwmmw4XJj0ifSD5qB5TOo8hmA0wlRKDAOAhEE1yVPgs6Fgcg==", + "version": "11.3.6", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.6.tgz", + "integrity": "sha512-w8ZNZr2mKIc7qeNaQ9AVPT1+iFaI+Avd4xudVOvdDJ8VytREi1Ft5Ih7hd9jjehod8vAM5GMsfQ/TpPf4EyoEA==", "dev": true, "license": "MIT", "optional": true, @@ -684,6 +720,40 @@ "node": ">=14.14" } }, + "node_modules/@emnapi/core": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz", + "integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.2", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz", + "integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz", + "integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@epic-web/invariant": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@epic-web/invariant/-/invariant-1.0.0.tgz", @@ -1133,82 +1203,271 @@ "node": ">=18" } }, - "node_modules/@isaacs/cliui": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-9.0.0.tgz", - "integrity": "sha512-AokJm4tuBHillT+FpMtxQ60n8ObyXBatq7jD2/JA9dxbDDokKQm8KMht5ibGzLVU9IJDIKK4TPKgMHEYMn3lMg==", + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", "dev": true, - "license": "BlueOak-1.0.0", + "license": "MIT", + "peer": true, + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, "engines": { - "node": ">=18" + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, - "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==", + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", "dev": true, - "license": "ISC", + "license": "Apache-2.0", + "peer": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.23.5", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.5.tgz", + "integrity": "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==", + "dev": true, + "license": "Apache-2.0", + "peer": true, "dependencies": { - "minipass": "^7.0.4" + "@eslint/object-schema": "^3.0.5", + "debug": "^4.3.1", + "minimatch": "^10.2.4" }, "engines": { - "node": ">=18.0.0" + "node": "^20.19.0 || ^22.13.0 || >=24" } }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.13", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", - "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "node_modules/@eslint/config-helpers": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.6.0.tgz", + "integrity": "sha512-ii6Bw9jJ2zi2cWA2Z+9/QZ/+3DX6kwaV5Q986D/CdP3Lap3w/pgQZ373FV7byY/i7L4IRH/G43I5dz1ClsCbpA==", "dev": true, - "license": "MIT", + "license": "Apache-2.0", + "peer": true, "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0", - "@jridgewell/trace-mapping": "^0.3.24" + "@eslint/core": "^1.2.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" } }, - "node_modules/@jridgewell/remapping": { - "version": "2.3.5", - "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", - "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "node_modules/@eslint/core": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.2.1.tgz", + "integrity": "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==", "dev": true, - "license": "MIT", + "license": "Apache-2.0", + "peer": true, "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" } }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "node_modules/@eslint/object-schema": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.5.tgz", + "integrity": "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==", "dev": true, - "license": "MIT", + "license": "Apache-2.0", + "peer": true, "engines": { - "node": ">=6.0.0" + "node": "^20.19.0 || ^22.13.0 || >=24" } }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", - "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "node_modules/@eslint/plugin-kit": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.7.2.tgz", + "integrity": "sha512-+CNAzxglkrpNf/kKywqQfk74QjtceuOE7Qm+AF8miRvPF/wmmK5+OJOgVh3AVTT3RP2mH3+FOaxlE5v72owk0A==", "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@eslint/core": "^1.2.1", + "levn": "^0.4.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@formkit/auto-animate": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@formkit/auto-animate/-/auto-animate-0.10.0.tgz", + "integrity": "sha512-KGomRttjUfORuPUaR/ZGQw+6xfMrTM+sxnILv7JAd9AmabU9rg9i6gF/iC0Ih+QpKCubJpCA/1DX9UHKE8cX+A==", "license": "MIT" }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.31", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", - "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "node_modules/@humanfs/core": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", "dev": true, - "license": "MIT", + "license": "Apache-2.0", + "peer": true, "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" + "@humanfs/types": "^0.15.0" + }, + "engines": { + "node": ">=18.18.0" } }, - "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", + "node_modules/@humanfs/node": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/types": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@iarna/toml": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/@iarna/toml/-/toml-2.2.5.tgz", + "integrity": "sha512-trnsAYxU3xnS1gPHPyU961coFyLkh4gAD/0zQ5mymY4yOZ+CYvsPqUbOFSw0aDM4y0tV7tiFxL/1XfXPNC6IPg==", + "dev": true, + "license": "ISC" + }, + "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/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_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": [ @@ -1261,47 +1520,201 @@ "node": ">=10" } }, - "node_modules/@npmcli/agent": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@npmcli/agent/-/agent-3.0.0.tgz", - "integrity": "sha512-S79NdEgDQd/NGCay6TCoVzXSj74skRZIKJcpJjC5lOq34SZzyI6MqtiiWoiVWoVrTcGjNeC4ipbh1VIHlpfF5Q==", + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.6.tgz", + "integrity": "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==", "dev": true, - "license": "ISC", + "license": "MIT", + "optional": true, "dependencies": { - "agent-base": "^7.1.0", - "http-proxy-agent": "^7.0.0", - "https-proxy-agent": "^7.0.1", - "lru-cache": "^10.0.1", - "socks-proxy-agent": "^8.0.3" + "@tybys/wasm-util": "^0.10.3" }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, + "node_modules/@noble/hashes": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.2.0.tgz", + "integrity": "sha512-IYqDGiTXab6FniAgnSdZwgWbomxpy9FtYvLKs7wCUs2a8RkITG+DFGO1DM9cr+E3/RgADRpFjrKVaJ1z6sjtEg==", + "dev": true, + "license": "MIT", "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" } }, - "node_modules/@npmcli/agent/node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", "dev": true, - "license": "ISC" + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } }, - "node_modules/@npmcli/fs": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-4.0.0.tgz", - "integrity": "sha512-/xGlezI6xfGO9NwuJlnwz/K14qD1kCSAGtacBHnGzeAIuJGazcp45KP5NuyARXoKb7cwulAGWVsbeSxdG/cb0Q==", + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", "dev": true, - "license": "ISC", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", "dependencies": { - "semver": "^7.3.5" + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" }, "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": ">= 8" } }, - "node_modules/@oxlint/binding-android-arm-eabi": { - "version": "1.67.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm-eabi/-/binding-android-arm-eabi-1.67.0.tgz", - "integrity": "sha512-VrSi571rDv1N8HaEDM+DEX8nmT0y9jJo8tzzW13vsOWTx59xQczCIJx68n2zWOXRT5YKZsOZXp4qkHN/10x4mw==", + "node_modules/@opentelemetry/api": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.1.tgz", + "integrity": "sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@opentelemetry/api-logs": { + "version": "0.220.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.220.0.tgz", + "integrity": "sha512-CmVa4ImJ+ynfrPMNaAXHET6Bhb44SwzmfyVJFq9ni2jgXJR/l7C6gfVFddNmHP+ZOkP9cf4f9DBe68qVLTHc9w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api": "^1.3.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@opentelemetry/core": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.9.0.tgz", + "integrity": "sha512-m2nckMT80NnmjTYSPjJQObBJ+8dgkoajEOUbznL8AHZ3T3yHRk2P7gI1PhEBc1+lOnrYE9UWrWHqJDsmqjmNbw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/instrumentation": { + "version": "0.220.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.220.0.tgz", + "integrity": "sha512-xQx3E2WxP1mDvKzxLxX+CTCtNLa560YJZ3087qYHerl2YmiKpv7AH+dAy7vmx+eVrZ5BwhfWUAVoKOoxCNHcpw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.220.0", + "import-in-the-middle": "^3.0.0", + "require-in-the-middle": "^8.0.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/resources": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.9.0.tgz", + "integrity": "sha512-jyA5MBLQ+Dkl3+JsZkUoUvL7yHvU64kLsvpXKarWm6347Sl1t1bXFTFykUePNpT5WH5pm9a2Qtt03iIYQhZ1Fg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.9.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-trace": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace/-/sdk-trace-2.9.0.tgz", + "integrity": "sha512-sGA19HvtrrSKYsseHphluH6j3p6Xa3fqc7c7y8f/7mYWejc1lyDFcpSdD1kYa50HCLUeEo4zA5bW0pniaPszuw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.9.0", + "@opentelemetry/resources": "2.9.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-trace-base": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.9.0.tgz", + "integrity": "sha512-cp9zmTl62R8PJrpvFcmc8N2JQU/xfa0S+61q511Nji+QxCfZ8Ifvg7H27G8cANe4crg4RTrWsVvanHiXjSp6ag==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.9.0", + "@opentelemetry/resources": "2.9.0", + "@opentelemetry/sdk-trace": "2.9.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/semantic-conventions": { + "version": "1.43.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.43.0.tgz", + "integrity": "sha512-eSYWTm620tTk45EKSedaUL8MFYI8hW164hIXsgIHyxu3VobUB3fFCu5t0hQby6OoWRPsG1KkKUG2M5UadiLiVg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, + "node_modules/@oxc-parser/binding-android-arm-eabi": { + "version": "0.138.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-android-arm-eabi/-/binding-android-arm-eabi-0.138.0.tgz", + "integrity": "sha512-hSYAD+F9W2Qh8SETMqBsQRx6YHvB4z+i/i36shlC7tfdZQauMs4vf3G/EQwKOkNlN7rkTiKINvsNmQb9q2MWcQ==", "cpu": [ "arm" ], @@ -1315,10 +1728,10 @@ "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@oxlint/binding-android-arm64": { - "version": "1.67.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm64/-/binding-android-arm64-1.67.0.tgz", - "integrity": "sha512-l6+NdYxMoRohix5r5bbigW16LPicceCwGcQ6LKKuE1kUdjgFfQolJjrJsQYPFetIs78Gxj/G/f5TEGoTCwj9nQ==", + "node_modules/@oxc-parser/binding-android-arm64": { + "version": "0.138.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-android-arm64/-/binding-android-arm64-0.138.0.tgz", + "integrity": "sha512-Ns5LLTp8cVyP8DsYqD482h0HE84xiGYRgtm7g4LtTinq209NAiMF768e/8r2NHaa0UMirS5mrT1m1VwiVmBi4Q==", "cpu": [ "arm64" ], @@ -1332,10 +1745,10 @@ "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@oxlint/binding-darwin-arm64": { - "version": "1.67.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-arm64/-/binding-darwin-arm64-1.67.0.tgz", - "integrity": "sha512-jOzXxS1AxFxhImLIRbtGIMrEwaXcgMw3gR57WB1cRk8ai+vpr6726kxXqVvlNsrXtJ/FrmOm8RxlC0m8SW24Qg==", + "node_modules/@oxc-parser/binding-darwin-arm64": { + "version": "0.138.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-darwin-arm64/-/binding-darwin-arm64-0.138.0.tgz", + "integrity": "sha512-Yka0m4YhKUHBIZufafSLAeO+DUrfHPtNXBlZSj7DxshquIl41x/a+i/MbRnbOy8heuLiYU1STa6h0FAAzT7Pbw==", "cpu": [ "arm64" ], @@ -1349,10 +1762,10 @@ "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@oxlint/binding-darwin-x64": { - "version": "1.67.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-x64/-/binding-darwin-x64-1.67.0.tgz", - "integrity": "sha512-3DFAVY94OqjIZHXIPz37yGRSWwOFTAqChQ64/M69GYLawzP0KiwdhDNfqdKKYT0bTR/DNxmMnQsj3ns+8+X/Lg==", + "node_modules/@oxc-parser/binding-darwin-x64": { + "version": "0.138.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-darwin-x64/-/binding-darwin-x64-0.138.0.tgz", + "integrity": "sha512-MWLUZZzmNRUqTWueZF27ncreaZ1wZ0gboWL2QMPxRQA2xgOmBPlGg2H9pAKJSPBlwEHcWa9TdWRiehAS+yls8w==", "cpu": [ "x64" ], @@ -1366,10 +1779,10 @@ "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@oxlint/binding-freebsd-x64": { - "version": "1.67.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-freebsd-x64/-/binding-freebsd-x64-1.67.0.tgz", - "integrity": "sha512-e4dDKZuLu8TR9DEBssWSDahlPgZBwojTTHZUvnjBRJfJJbpxYCjfjKfi0Z1+CSLMiJBwI2yCDtRM1XJQaARjmg==", + "node_modules/@oxc-parser/binding-freebsd-x64": { + "version": "0.138.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-freebsd-x64/-/binding-freebsd-x64-0.138.0.tgz", + "integrity": "sha512-Vae5tzsrzZ/lCDVCZUMi/vzSiiHEgcOEfsyIfWOHmjZ2ji+gT+n96T757yX5/f7/7JIJuiannAHJKV5ARaF6ng==", "cpu": [ "x64" ], @@ -1383,10 +1796,10 @@ "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@oxlint/binding-linux-arm-gnueabihf": { - "version": "1.67.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.67.0.tgz", - "integrity": "sha512-BKytFdcQzbITV3xlnzDUDTEDtbUMCCiC4EaNTDZ4FyT8gdNvBC4gfiLucXp/sQl0XU3p7syTlorUWVVVBZab2g==", + "node_modules/@oxc-parser/binding-linux-arm-gnueabihf": { + "version": "0.138.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-0.138.0.tgz", + "integrity": "sha512-qkU8wv5mYexrCw0X4DHFgxGbRScwGLIIKUkHXU7xXEiLoMnQzELak2gujxfa9GFrlEgPjbyLUDFHWm67Zs38ng==", "cpu": [ "arm" ], @@ -1400,10 +1813,10 @@ "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@oxlint/binding-linux-arm-musleabihf": { - "version": "1.67.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-1.67.0.tgz", - "integrity": "sha512-XYAv0esBDX7BpTzRDjVX2Vdj+zndd8ll2dFQiaeQ6zTZr7A8GRDTN7fH3FP3jU+O0vCDx85oH/EtG7BzPgAXuw==", + "node_modules/@oxc-parser/binding-linux-arm-musleabihf": { + "version": "0.138.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-0.138.0.tgz", + "integrity": "sha512-3HgULIvoDV7h2ZfVYzxQwOSOJnAjMwYmyUBzndNuLRGgBNI549ED0P6AGmN9y2TnSvrwJ+Q8zqdxqssMnGXitA==", "cpu": [ "arm" ], @@ -1417,10 +1830,10 @@ "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@oxlint/binding-linux-arm64-gnu": { - "version": "1.67.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.67.0.tgz", - "integrity": "sha512-zizRMjA0i6u/2B0evgda04iycu+MoNuf1pBy6Eh+1CjC5wMEG7qN5zdDKTCvFc0KSYSDM9QTG3gjZHirgtQuKg==", + "node_modules/@oxc-parser/binding-linux-arm64-gnu": { + "version": "0.138.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-0.138.0.tgz", + "integrity": "sha512-pIonbH2p0KLCwz4CNPCi0xGqci4numpMQDCLJwLfsrEky7NUuByKDFhCjzE0E7vR3aj/lBjyMoTskHBo/qSg8g==", "cpu": [ "arm64" ], @@ -1434,10 +1847,10 @@ "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@oxlint/binding-linux-arm64-musl": { - "version": "1.67.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.67.0.tgz", - "integrity": "sha512-zB/Tf6sUjmmvvbva9Gj3JTJ8rJ9t4I8/U0o6vSRtd0DRIsIuyegBwJAzhSUFQHdMijIRJkW0exs/yBhpw2S20w==", + "node_modules/@oxc-parser/binding-linux-arm64-musl": { + "version": "0.138.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm64-musl/-/binding-linux-arm64-musl-0.138.0.tgz", + "integrity": "sha512-cT5L1Xz/5m6Ga1hD3922gLc+fePOauJZJdApPTI/2Vu0EmYo62uHG9V5Dq65hhgU9TW10oDi2840y9cGdd7BIg==", "cpu": [ "arm64" ], @@ -1451,10 +1864,10 @@ "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@oxlint/binding-linux-ppc64-gnu": { - "version": "1.67.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.67.0.tgz", - "integrity": "sha512-kgU40Gt74CK0TCsF51KZymkIwN9U0BajKsMijB52zPqOeZU9NAHkA/NSQkZDHEaCakx42DxhXkODiAqf2b4Gug==", + "node_modules/@oxc-parser/binding-linux-ppc64-gnu": { + "version": "0.138.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-0.138.0.tgz", + "integrity": "sha512-hKy/vvejKk3LNE/FsRbekWejLa046//TnLWtSo7ur29NIsNbSIvnOVYIirSVC7fsd6NO8UFzwDdcoZfCyBvSBA==", "cpu": [ "ppc64" ], @@ -1468,10 +1881,10 @@ "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@oxlint/binding-linux-riscv64-gnu": { - "version": "1.67.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-1.67.0.tgz", - "integrity": "sha512-tOYhkk/iaG9aD3FvGpBFd1Lrw0x0RaVoJBxjUkfNzS50rC5NS5BteNCwgr8A2zCdADrIIoze6D7u6U5Ic++/iQ==", + "node_modules/@oxc-parser/binding-linux-riscv64-gnu": { + "version": "0.138.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-0.138.0.tgz", + "integrity": "sha512-bh6tjNGq0v0b9GAMu0pTv/YpTqepCFy0TIOtQHm8+41fZwLXTaB6xiEWVUSarNCXqc5kyzYcH6EOfwW1sJxJOw==", "cpu": [ "riscv64" ], @@ -1485,10 +1898,10 @@ "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@oxlint/binding-linux-riscv64-musl": { - "version": "1.67.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-1.67.0.tgz", - "integrity": "sha512-sEtywrPb+0b+tHYl1SDCrw903fiC4eyKoNqzP3v+f2JT3Xcv4NEYG+P8rj+eEnX7IWhqV/xj8/JmcmVj21CXaA==", + "node_modules/@oxc-parser/binding-linux-riscv64-musl": { + "version": "0.138.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-0.138.0.tgz", + "integrity": "sha512-HhOkddcClSTtTxY10f/mACblKcQdxWy4lYYwX12G23j+S5eiJ5y1kpo1r7kKng+2bdnCBO+lCDWOVVc9kVl9+g==", "cpu": [ "riscv64" ], @@ -1502,10 +1915,10 @@ "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@oxlint/binding-linux-s390x-gnu": { - "version": "1.67.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.67.0.tgz", - "integrity": "sha512-BvR8Moa0zCLxroOx4vZaZN9nUfwAUpSTwjZdxZyKy4bv3PrzrXrxKR/ZQ0L9wNSvlPhnMJeZfa3q5w6ZCTuN6Q==", + "node_modules/@oxc-parser/binding-linux-s390x-gnu": { + "version": "0.138.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-0.138.0.tgz", + "integrity": "sha512-5mi+wtbeJiEa4waGG88EcEGgJBBNJdDeIcayPPcrLNMXbCrgdtbb80q0Nrat7A8NglLUVzhuTAAp7K6PjmUO8Q==", "cpu": [ "s390x" ], @@ -1519,10 +1932,10 @@ "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@oxlint/binding-linux-x64-gnu": { - "version": "1.67.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.67.0.tgz", - "integrity": "sha512-mm2cxM6fksOpq6l0uFws8BUGKAR4dNa/cZCn37Npq7PFbhD5HDJqWfnoIvTaeRKMy5XdS2tO0MA0qbHDrnXAAA==", + "node_modules/@oxc-parser/binding-linux-x64-gnu": { + "version": "0.138.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-x64-gnu/-/binding-linux-x64-gnu-0.138.0.tgz", + "integrity": "sha512-ckbq3AMI7lI8AhQtE8KdqYRmzmzwKfCU12QN/PBKXO72PfWdvvZQN0hFShDX/XRNsPqjddLmvXaQMT3zfYtNlw==", "cpu": [ "x64" ], @@ -1536,10 +1949,10 @@ "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@oxlint/binding-linux-x64-musl": { - "version": "1.67.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-musl/-/binding-linux-x64-musl-1.67.0.tgz", - "integrity": "sha512-WmbMuLapKyDlobMkXAaAL0Y+Uczh4LETfIfQsUpbId4Ip8Ai82/jqeYTOoUCkuuhBFapgqP253+d83tLKOksJg==", + "node_modules/@oxc-parser/binding-linux-x64-musl": { + "version": "0.138.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-x64-musl/-/binding-linux-x64-musl-0.138.0.tgz", + "integrity": "sha512-JrCOzHO9BYEs5Xz5JHYBxSc/hYKxfXUj5QQb64sERSbkQot6+KEgMTOR2C9hLrhaqOui65OYcFyTTS+YxXDtnA==", "cpu": [ "x64" ], @@ -1553,10 +1966,10 @@ "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@oxlint/binding-openharmony-arm64": { - "version": "1.67.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-openharmony-arm64/-/binding-openharmony-arm64-1.67.0.tgz", - "integrity": "sha512-9g/PqxYJelzzTAOR5Y+RiRqdeydhEuXv2KxNeFcAKQ7UsvnWSY1OP4MsuPMbTO2Pf70tz7mFhl1j13H3fyh+8g==", + "node_modules/@oxc-parser/binding-openharmony-arm64": { + "version": "0.138.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-openharmony-arm64/-/binding-openharmony-arm64-0.138.0.tgz", + "integrity": "sha512-eASMMfOOIfLHkWJRPSu8llByvVRM+c1M/lh18KjsjELM3y10+7B5iBbbrht9LdtsJXQ+mRuP/lJ7UWe3Ok3ehw==", "cpu": [ "arm64" ], @@ -1570,29 +1983,31 @@ "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@oxlint/binding-win32-arm64-msvc": { - "version": "1.67.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.67.0.tgz", - "integrity": "sha512-2VhwE6Gatb0vJGnN0TBuQMbKCOiZlSQ/zJvVWYLK4a9d4iDiJOen/yVQkGpmsJ90MuH66fzi0kEKI0jRQMDxGA==", + "node_modules/@oxc-parser/binding-wasm32-wasi": { + "version": "0.138.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-wasm32-wasi/-/binding-wasm32-wasi-0.138.0.tgz", + "integrity": "sha512-BnTCO87Iwc57NufXS7vcrkrmpN+daeCeYr1+/xgPT6HjwNs0lBmJYeFrcOs4WkNN8yscdd6Rc4FxWh3+59hAFw==", "cpu": [ - "arm64" + "wasm32" ], "dev": true, "license": "MIT", "optional": true, - "os": [ - "win32" - ], + "dependencies": { + "@emnapi/core": "1.11.1", + "@emnapi/runtime": "1.11.1", + "@napi-rs/wasm-runtime": "^1.1.6" + }, "engines": { "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@oxlint/binding-win32-ia32-msvc": { - "version": "1.67.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-1.67.0.tgz", - "integrity": "sha512-EQ3VExXfeM1InbE5+JjufhZZTWy+kHUwgt3yZR7gQ47Je/mE0WspQPan0OJznh493L5anM210YNJtH1PXjTSFg==", + "node_modules/@oxc-parser/binding-win32-arm64-msvc": { + "version": "0.138.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-0.138.0.tgz", + "integrity": "sha512-+Zi47boD2wKNL0hOA47Vkwk6njMZ8sOsr4Geu/56EUtlooDh9crNOU41U6bXGS0UjC4Y72HtRA1iuB6qx1ARUw==", "cpu": [ - "ia32" + "arm64" ], "dev": true, "license": "MIT", @@ -1604,12 +2019,12 @@ "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@oxlint/binding-win32-x64-msvc": { - "version": "1.67.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.67.0.tgz", - "integrity": "sha512-bw24y+/1MHS4QDkons3YyHkPT9uCMoLHHgQhb+mb8NOjTYwub1CZ+K9Ngr8aO5DMrDrkqHwTzlTwFP2vS8Y/ZQ==", + "node_modules/@oxc-parser/binding-win32-ia32-msvc": { + "version": "0.138.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-0.138.0.tgz", + "integrity": "sha512-SYcV674Wi2WuoBefUFgf0PBMNlZe5IF0YZ0TnP7DK+EusMVpEWq6iz+7r64svjAb7vjthzlas0FUCSlz8YkqYg==", "cpu": [ - "x64" + "ia32" ], "dev": true, "license": "MIT", @@ -1621,89 +2036,40 @@ "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@pkgjs/parseargs": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", - "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "node_modules/@oxc-parser/binding-win32-x64-msvc": { + "version": "0.138.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-win32-x64-msvc/-/binding-win32-x64-msvc-0.138.0.tgz", + "integrity": "sha512-QZplnCxS4vPe4StAVBtvD2bW3pELlidf0Ek6iQ/HHiCjbEtrs5pFZZfLAoPhKLJyDzyxoGAdic9bSIYrJYTZcg==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", "optional": true, + "os": [ + "win32" + ], "engines": { - "node": ">=14" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@preact/signals": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/@preact/signals/-/signals-1.3.4.tgz", - "integrity": "sha512-TPMkStdT0QpSc8FpB63aOwXoSiZyIrPsP9Uj347KopdS6olZdAYeeird/5FZv/M1Yc1ge5qstub2o8VDbvkT4g==", + "node_modules/@oxc-project/types": { + "version": "0.138.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.138.0.tgz", + "integrity": "sha512-1a7ZKmrRTCoN1XMZ4L0PyyqrMnrNlLyPuOkdSX2MZg7IiIGRUyurNhAm73ptDOraoBcIordsIGKNPKUzy3ZmfA==", "dev": true, "license": "MIT", - "dependencies": { - "@preact/signals-core": "^1.7.0" - }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/preact" - }, - "peerDependencies": { - "preact": "10.x" + "url": "https://github.com/sponsors/Boshen" } }, - "node_modules/@preact/signals-core": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@preact/signals-core/-/signals-core-1.14.1.tgz", - "integrity": "sha512-vxPpfXqrwUe9lpjqfYNjAF/0RF/eFGeLgdJzdmIIZjpOnTmGmAB4BjWone562mJGMRP4frU6iZ6ei3PDsu52Ng==", - "dev": true, - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/preact" - } - }, - "node_modules/@rolldown/pluginutils": { - "version": "1.0.0-rc.3", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.3.tgz", - "integrity": "sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/@rollup/pluginutils": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.3.0.tgz", - "integrity": "sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/estree": "^1.0.0", - "estree-walker": "^2.0.2", - "picomatch": "^4.0.2" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" - }, - "peerDependenciesMeta": { - "rollup": { - "optional": true - } - } - }, - "node_modules/@rollup/pluginutils/node_modules/estree-walker": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", - "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", - "dev": true, - "license": "MIT" - }, - "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.57.1.tgz", - "integrity": "sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg==", - "cpu": [ - "arm" - ], + "node_modules/@oxc-resolver/binding-android-arm-eabi": { + "version": "11.24.2", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-android-arm-eabi/-/binding-android-arm-eabi-11.24.2.tgz", + "integrity": "sha512-y09e0L0SRI2OA2tUIrjBgoV3eH5hvUKXNkJqXmNo5V2WxIjyC7I7aJfRLMEVpA8yi95f90gFDvO0VMgrDw+vwA==", + "cpu": [ + "arm" + ], "dev": true, "license": "MIT", "optional": true, @@ -1711,10 +2077,10 @@ "android" ] }, - "node_modules/@rollup/rollup-android-arm64": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.57.1.tgz", - "integrity": "sha512-dQaAddCY9YgkFHZcFNS/606Exo8vcLHwArFZ7vxXq4rigo2bb494/xKMMwRRQW6ug7Js6yXmBZhSBRuBvCCQ3w==", + "node_modules/@oxc-resolver/binding-android-arm64": { + "version": "11.24.2", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-android-arm64/-/binding-android-arm64-11.24.2.tgz", + "integrity": "sha512-cl4icWaZFnLdg8m6qtnh5rBMuGbxc/ptStFHLeCNwr+2cZjkjNwQu/jYRS0CHlnPecOJMpuS5M6/BH+0J/YkEg==", "cpu": [ "arm64" ], @@ -1725,10 +2091,10 @@ "android" ] }, - "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.57.1.tgz", - "integrity": "sha512-crNPrwJOrRxagUYeMn/DZwqN88SDmwaJ8Cvi/TN1HnWBU7GwknckyosC2gd0IqYRsHDEnXf328o9/HC6OkPgOg==", + "node_modules/@oxc-resolver/binding-darwin-arm64": { + "version": "11.24.2", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-darwin-arm64/-/binding-darwin-arm64-11.24.2.tgz", + "integrity": "sha512-At29QEMF6HajbQvgY8K6OXnHD1x9rad74xBEfmCB6ZqCGsdq75aK7tOYcTbOanMy8qdIBrfL3SMr3p/lfSlb9w==", "cpu": [ "arm64" ], @@ -1739,10 +2105,10 @@ "darwin" ] }, - "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.57.1.tgz", - "integrity": "sha512-Ji8g8ChVbKrhFtig5QBV7iMaJrGtpHelkB3lsaKzadFBe58gmjfGXAOfI5FV0lYMH8wiqsxKQ1C9B0YTRXVy4w==", + "node_modules/@oxc-resolver/binding-darwin-x64": { + "version": "11.24.2", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-darwin-x64/-/binding-darwin-x64-11.24.2.tgz", + "integrity": "sha512-A5Kqr1EUj4oIL5CF4WRssq/o5P0Y11cwoFouMRmQ7YnC/A8V93nv1nb7aSU8HwcgmXropjLNkVTl4MN87cu28Q==", "cpu": [ "x64" ], @@ -1753,12 +2119,12 @@ "darwin" ] }, - "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.57.1.tgz", - "integrity": "sha512-R+/WwhsjmwodAcz65guCGFRkMb4gKWTcIeLy60JJQbXrJ97BOXHxnkPFrP+YwFlaS0m+uWJTstrUA9o+UchFug==", + "node_modules/@oxc-resolver/binding-freebsd-x64": { + "version": "11.24.2", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-freebsd-x64/-/binding-freebsd-x64-11.24.2.tgz", + "integrity": "sha512-R5xkRBRRz7ceH/P5Jrc6G7FmdUdgpLYyESFAUDVTNQ9K0sGPxcp4ljiwEwEqsvNcQ4sYbMRrWcHHBCu7ksAJVw==", "cpu": [ - "arm64" + "x64" ], "dev": true, "license": "MIT", @@ -1767,24 +2133,24 @@ "freebsd" ] }, - "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.57.1.tgz", - "integrity": "sha512-IEQTCHeiTOnAUC3IDQdzRAGj3jOAYNr9kBguI7MQAAZK3caezRrg0GxAb6Hchg4lxdZEI5Oq3iov/w/hnFWY9Q==", + "node_modules/@oxc-resolver/binding-linux-arm-gnueabihf": { + "version": "11.24.2", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-11.24.2.tgz", + "integrity": "sha512-k/RuYL4L/R58IBn3wT5ma3Wh4k62bp1eYCFRWCmMsasUOqL+H6sW0VGFadEzKWXFFlz+2uIMoeMk9ySSZJHgbg==", "cpu": [ - "x64" + "arm" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "freebsd" + "linux" ] }, - "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.57.1.tgz", - "integrity": "sha512-F8sWbhZ7tyuEfsmOxwc2giKDQzN3+kuBLPwwZGyVkLlKGdV1nvnNwYD0fKQ8+XS6hp9nY7B+ZeK01EBUE7aHaw==", + "node_modules/@oxc-resolver/binding-linux-arm-musleabihf": { + "version": "11.24.2", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-11.24.2.tgz", + "integrity": "sha512-bnHAak3ujYfH5pKk4NieFNbvYvernfoQDgwLddbZ3OtMYrem87/qjlA+u+aKG0oZcqSLGCful/6/CEA+aeAgaA==", "cpu": [ "arm" ], @@ -1795,12 +2161,12 @@ "linux" ] }, - "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.57.1.tgz", - "integrity": "sha512-rGfNUfn0GIeXtBP1wL5MnzSj98+PZe/AXaGBCRmT0ts80lU5CATYGxXukeTX39XBKsxzFpEeK+Mrp9faXOlmrw==", + "node_modules/@oxc-resolver/binding-linux-arm64-gnu": { + "version": "11.24.2", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-11.24.2.tgz", + "integrity": "sha512-vDT3KHgzYp47gmtNOqL2VNhCyl5Zv643eyxm//A68J8DeUGXrvD1pZFiaT4jSfe+RInfnn1R2yVHye4enx6RnA==", "cpu": [ - "arm" + "arm64" ], "dev": true, "license": "MIT", @@ -1809,10 +2175,10 @@ "linux" ] }, - "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.57.1.tgz", - "integrity": "sha512-MMtej3YHWeg/0klK2Qodf3yrNzz6CGjo2UntLvk2RSPlhzgLvYEB3frRvbEF2wRKh1Z2fDIg9KRPe1fawv7C+g==", + "node_modules/@oxc-resolver/binding-linux-arm64-musl": { + "version": "11.24.2", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-arm64-musl/-/binding-linux-arm64-musl-11.24.2.tgz", + "integrity": "sha512-+kMlQvbzfyEYtu5FcjE4p+ttBLpKW4d/AsAsuE69BxV6V4twZJeIQZFfD8gh/wqglY0MkPSezWXQH0jBV13MUw==", "cpu": [ "arm64" ], @@ -1823,12 +2189,12 @@ "linux" ] }, - "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.57.1.tgz", - "integrity": "sha512-1a/qhaaOXhqXGpMFMET9VqwZakkljWHLmZOX48R0I/YLbhdxr1m4gtG1Hq7++VhVUmf+L3sTAf9op4JlhQ5u1Q==", + "node_modules/@oxc-resolver/binding-linux-ppc64-gnu": { + "version": "11.24.2", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-11.24.2.tgz", + "integrity": "sha512-shjfMhmZ3gq9fv/w7bi3PnZlgOPG+2QAOFf0BJF0EgBSIGZ6PMLN2zbGEblTUYB/NKVDRyYhE2ff3dJ1QqNPkA==", "cpu": [ - "arm64" + "ppc64" ], "dev": true, "license": "MIT", @@ -1837,12 +2203,12 @@ "linux" ] }, - "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.57.1.tgz", - "integrity": "sha512-QWO6RQTZ/cqYtJMtxhkRkidoNGXc7ERPbZN7dVW5SdURuLeVU7lwKMpo18XdcmpWYd0qsP1bwKPf7DNSUinhvA==", + "node_modules/@oxc-resolver/binding-linux-riscv64-gnu": { + "version": "11.24.2", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-11.24.2.tgz", + "integrity": "sha512-zGelwFR5oRo+b69k8Lrzun86DyUHzfKN6cnjbR9l7Z7NIRznOE/2ZvPa1IUKqAL2PzAXOdwkfVqNvO1H2RlpAw==", "cpu": [ - "loong64" + "riscv64" ], "dev": true, "license": "MIT", @@ -1851,12 +2217,12 @@ "linux" ] }, - "node_modules/@rollup/rollup-linux-loong64-musl": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.57.1.tgz", - "integrity": "sha512-xpObYIf+8gprgWaPP32xiN5RVTi/s5FCR+XMXSKmhfoJjrpRAjCuuqQXyxUa/eJTdAE6eJ+KDKaoEqjZQxh3Gw==", + "node_modules/@oxc-resolver/binding-linux-riscv64-musl": { + "version": "11.24.2", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-11.24.2.tgz", + "integrity": "sha512-qxZ1SWCXJY0eyhAlP6Lmo9F2Nrtx7EkYj9oCgL8apDPCwXwCEDA2U697bbT81JIc2IrVjxO4KX6WU2N+oN9Z4w==", "cpu": [ - "loong64" + "riscv64" ], "dev": true, "license": "MIT", @@ -1865,12 +2231,12 @@ "linux" ] }, - "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.57.1.tgz", - "integrity": "sha512-4BrCgrpZo4hvzMDKRqEaW1zeecScDCR+2nZ86ATLhAoJ5FQ+lbHVD3ttKe74/c7tNT9c6F2viwB3ufwp01Oh2w==", + "node_modules/@oxc-resolver/binding-linux-s390x-gnu": { + "version": "11.24.2", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-11.24.2.tgz", + "integrity": "sha512-sGCecF3cx2DFlH4t/z7ApnOnXqN48p5p5mlHDEnHTAukQa2P+qMVE4CwyWE9W+q/m3QJ7kKfGrIjax31f44oFQ==", "cpu": [ - "ppc64" + "s390x" ], "dev": true, "license": "MIT", @@ -1879,12 +2245,12 @@ "linux" ] }, - "node_modules/@rollup/rollup-linux-ppc64-musl": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.57.1.tgz", - "integrity": "sha512-NOlUuzesGauESAyEYFSe3QTUguL+lvrN1HtwEEsU2rOwdUDeTMJdO5dUYl/2hKf9jWydJrO9OL/XSSf65R5+Xw==", + "node_modules/@oxc-resolver/binding-linux-x64-gnu": { + "version": "11.24.2", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-x64-gnu/-/binding-linux-x64-gnu-11.24.2.tgz", + "integrity": "sha512-k/VlMMcSzMlahb3/fENM4rTlsJ0s3fFROA0KXPBmKggqmTSaE383sl8F3KCOXPLmVsYfW6hCitMhXCEtNeZxxg==", "cpu": [ - "ppc64" + "x64" ], "dev": true, "license": "MIT", @@ -1893,12 +2259,12 @@ "linux" ] }, - "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.57.1.tgz", - "integrity": "sha512-ptA88htVp0AwUUqhVghwDIKlvJMD/fmL/wrQj99PRHFRAG6Z5nbWoWG4o81Nt9FT+IuqUQi+L31ZKAFeJ5Is+A==", + "node_modules/@oxc-resolver/binding-linux-x64-musl": { + "version": "11.24.2", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-x64-musl/-/binding-linux-x64-musl-11.24.2.tgz", + "integrity": "sha512-8hbnZyNi97b/8wapYaIF9+t9GmZKBW2vunaOc3h9HGJptH7b7XpvZqOTBSm/MpTjr7H497BlgOaSfLUdhmy2bw==", "cpu": [ - "riscv64" + "x64" ], "dev": true, "license": "MIT", @@ -1907,52 +2273,80 @@ "linux" ] }, - "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.57.1.tgz", - "integrity": "sha512-S51t7aMMTNdmAMPpBg7OOsTdn4tySRQvklmL3RpDRyknk87+Sp3xaumlatU+ppQ+5raY7sSTcC2beGgvhENfuw==", + "node_modules/@oxc-resolver/binding-openharmony-arm64": { + "version": "11.24.2", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-openharmony-arm64/-/binding-openharmony-arm64-11.24.2.tgz", + "integrity": "sha512-MvyGik3a6pVgZ0t/kWlbmFxFLmXQJwgLsY2eYFHLpy0wGwRbfzeIGgDwQ3kXqE30z+kSXennRkCrT7TUvkptNg==", "cpu": [ - "riscv64" + "arm64" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "linux" + "openharmony" ] }, - "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.57.1.tgz", - "integrity": "sha512-Bl00OFnVFkL82FHbEqy3k5CUCKH6OEJL54KCyx2oqsmZnFTR8IoNqBF+mjQVcRCT5sB6yOvK8A37LNm/kPJiZg==", + "node_modules/@oxc-resolver/binding-wasm32-wasi": { + "version": "11.24.2", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-wasm32-wasi/-/binding-wasm32-wasi-11.24.2.tgz", + "integrity": "sha512-vHcssMPwO08RTvj/c0iOBz90attxyG3wQJ0dTcyEQK43LRpcdLWZlV5feBhv6Isn6ahbQIzHbCgfa81+RiML0Q==", "cpu": [ - "s390x" + "wasm32" ], "dev": true, "license": "MIT", "optional": true, - "os": [ - "linux" - ] + "dependencies": { + "@emnapi/core": "1.11.2", + "@emnapi/runtime": "1.11.2", + "@napi-rs/wasm-runtime": "^1.1.6" + }, + "engines": { + "node": ">=14.0.0" + } }, - "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.57.1.tgz", - "integrity": "sha512-ABca4ceT4N+Tv/GtotnWAeXZUZuM/9AQyCyKYyKnpk4yoA7QIAuBt6Hkgpw8kActYlew2mvckXkvx0FfoInnLg==", + "node_modules/@oxc-resolver/binding-wasm32-wasi/node_modules/@emnapi/core": { + "version": "1.11.2", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.2.tgz", + "integrity": "sha512-TC8MkTuZUtcTSiFeuC0ksCh9QIJ5+F21MvZ4Wn4ORfYaFJ/0dsiudv5tVkejgwZlwQ39jL9WWDe2lz8x0WglOA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.2", + "tslib": "^2.4.0" + } + }, + "node_modules/@oxc-resolver/binding-wasm32-wasi/node_modules/@emnapi/runtime": { + "version": "1.11.2", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.2.tgz", + "integrity": "sha512-kyOl3X0DuTiT1h2ft8r2fYO8JYtU9a9Xis/zBSiGArNaagCOWx90N1k2wxp18czFDH+OgcWGb5ZP/XMt3dcyPA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@oxc-resolver/binding-win32-arm64-msvc": { + "version": "11.24.2", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-11.24.2.tgz", + "integrity": "sha512-uokJqro2iBqkFvJdKQLP7d8/BUmFwESQFVmIJUQKj1Xn1a/LysJoe1vmeECLF5b3jsV8CAL5sEMJXX6SdK9Nhg==", "cpu": [ - "x64" + "arm64" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "linux" + "win32" ] }, - "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.57.1.tgz", - "integrity": "sha512-HFps0JeGtuOR2convgRRkHCekD7j+gdAuXM+/i6kGzQtFhlCtQkpwtNzkNj6QhCDp7DRJ7+qC/1Vg2jt5iSOFw==", + "node_modules/@oxc-resolver/binding-win32-x64-msvc": { + "version": "11.24.2", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-win32-x64-msvc/-/binding-win32-x64-msvc-11.24.2.tgz", + "integrity": "sha512-UqGPmo56KDfLlfXFAFIrNflHT8tFxWGEivWg3Zeyp4Uy2NlKN1FGPr6/BxcLGG3+kZ6Wp14g5Uj+n71boqZfiw==", "cpu": [ "x64" ], @@ -1960,27 +2354,30 @@ "license": "MIT", "optional": true, "os": [ - "linux" + "win32" ] }, - "node_modules/@rollup/rollup-openbsd-x64": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.57.1.tgz", - "integrity": "sha512-H+hXEv9gdVQuDTgnqD+SQffoWoc0Of59AStSzTEj/feWTBAnSfSD3+Dql1ZruJQxmykT/JVY0dE8Ka7z0DH1hw==", + "node_modules/@oxlint/binding-android-arm-eabi": { + "version": "1.74.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm-eabi/-/binding-android-arm-eabi-1.74.0.tgz", + "integrity": "sha512-+gHd12muVI9ZLBaWLPkHt3Fj7jihFjgQ1MGtBaRL8vWrWrI0P7dLUty/cHrHS0oqPYIRgQUJsPu2CExQuMcwNw==", "cpu": [ - "x64" + "arm" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "openbsd" - ] + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.57.1.tgz", - "integrity": "sha512-4wYoDpNg6o/oPximyc/NG+mYUejZrCU2q+2w6YZqrAs2UcNUChIZXjtafAiiZSUc7On8v5NyNj34Kzj/Ltk6dQ==", + "node_modules/@oxlint/binding-android-arm64": { + "version": "1.74.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm64/-/binding-android-arm64-1.74.0.tgz", + "integrity": "sha512-xjKdoMB+H+RCOByv/7l7nfIGW9mlOisqYdcyC75UqYuQecLpReAeEYUf2CNeDEI3KtmUgxpRw/+c63y4AeF/Bw==", "cpu": [ "arm64" ], @@ -1988,13 +2385,16 @@ "license": "MIT", "optional": true, "os": [ - "openharmony" - ] + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.57.1.tgz", - "integrity": "sha512-O54mtsV/6LW3P8qdTcamQmuC990HDfR71lo44oZMZlXU4tzLrbvTii87Ni9opq60ds0YzuAlEr/GNwuNluZyMQ==", + "node_modules/@oxlint/binding-darwin-arm64": { + "version": "1.74.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-arm64/-/binding-darwin-arm64-1.74.0.tgz", + "integrity": "sha512-iUK7wvc6sejMKsC+Pt67mntoF5weFcyEunhZfLJceU6gL419mexz5wBkSx/EnkFBExMLNtOi9fnDSc5xfK0IzQ==", "cpu": [ "arm64" ], @@ -2002,27 +2402,33 @@ "license": "MIT", "optional": true, "os": [ - "win32" - ] + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.57.1.tgz", - "integrity": "sha512-P3dLS+IerxCT/7D2q2FYcRdWRl22dNbrbBEtxdWhXrfIMPP9lQhb5h4Du04mdl5Woq05jVCDPCMF7Ub0NAjIew==", + "node_modules/@oxlint/binding-darwin-x64": { + "version": "1.74.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-x64/-/binding-darwin-x64-1.74.0.tgz", + "integrity": "sha512-ggKc/tn5SJ1u2yG2izC6VKODfYKV8MQ2AicJlNzOjuyrC29udvOef6/JzK2r32xqCnBDLFouR1VCkjzEI0/N9Q==", "cpu": [ - "ia32" + "x64" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "win32" - ] + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.57.1.tgz", - "integrity": "sha512-VMBH2eOOaKGtIJYleXsi2B8CPVADrh+TyNxJ4mWPnKfLB/DBUmzW+5m1xUrcwWoMfSLagIRpjUFeW5CO5hyciQ==", + "node_modules/@oxlint/binding-freebsd-x64": { + "version": "1.74.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-freebsd-x64/-/binding-freebsd-x64-1.74.0.tgz", + "integrity": "sha512-u++dH/43jy9hTLbneaWlS0gla/Bp1JdwJ2zgevCl8nDFUh6qRCGMxcL0f0lb7By3A9p/LfFr+7cG4HU1hG856g==", "cpu": [ "x64" ], @@ -2030,3811 +2436,5651 @@ "license": "MIT", "optional": true, "os": [ - "win32" - ] + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.57.1.tgz", - "integrity": "sha512-mxRFDdHIWRxg3UfIIAwCm6NzvxG0jDX/wBN6KsQFTvKFqqg9vTrWUE68qEjHt19A5wwx5X5aUi2zuZT7YR0jrA==", + "node_modules/@oxlint/binding-linux-arm-gnueabihf": { + "version": "1.74.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.74.0.tgz", + "integrity": "sha512-Sj1zmtFDVTPeIbIz4ZfcXAbFHqCmKCXdCUlAJzvTF7I20NTH1RDpoF2PhkqNODutJzVhJYmm3oz0GwgY+tvE2g==", "cpu": [ - "x64" + "arm" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "win32" - ] + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.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==", + "node_modules/@oxlint/binding-linux-arm-musleabihf": { + "version": "1.74.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-1.74.0.tgz", + "integrity": "sha512-//PKyQb/tQXcHArx2f7z+oVI/eMS2Jpv+edNuAtOrgIhWdGcpHxogveAxzmF2rpH1AIHp4Hq04RF/rgJdiICnQ==", + "cpu": [ + "arm" + ], "dev": true, "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sindresorhus/is?sponsor=1" + "node": "^20.19.0 || >=22.12.0" } }, - "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==", + "node_modules/@oxlint/binding-linux-arm64-gnu": { + "version": "1.74.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.74.0.tgz", + "integrity": "sha512-/k1Me+aX2tjuH10K62mLS0y8cLkJBHX6Ce0xPK+eWeel4bSdEGZ8dv4+hYMzg0GrSmjwy4yAYsDPeEeKBft/2w==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", - "dependencies": { - "defer-to-connect": "^2.0.0" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=10" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@types/babel__core": { - "version": "7.20.5", - "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", - "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "node_modules/@oxlint/binding-linux-arm64-musl": { + "version": "1.74.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.74.0.tgz", + "integrity": "sha512-3tFSjBxc5D8/zvjEuLvOqcA8ZXKD0+6NuaVO/edeamNc49MoAsbfaC9s1UiwODwgF6slGaF8yJA2TPkukd77tg==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@babel/parser": "^7.20.7", - "@babel/types": "^7.20.7", - "@types/babel__generator": "*", - "@types/babel__template": "*", - "@types/babel__traverse": "*" + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@types/babel__generator": { - "version": "7.27.0", - "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", - "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "node_modules/@oxlint/binding-linux-ppc64-gnu": { + "version": "1.74.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.74.0.tgz", + "integrity": "sha512-9QggtPkSPXOCTu8Szis7auOK/sC7KdQaN+/TujP7YVVhzCAOhgdRfgv8uEz0r2tk5xdgus5rLYUrCDoZNtiRUw==", + "cpu": [ + "ppc64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@babel/types": "^7.0.0" + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@types/babel__template": { - "version": "7.4.4", - "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", - "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "node_modules/@oxlint/binding-linux-riscv64-gnu": { + "version": "1.74.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-1.74.0.tgz", + "integrity": "sha512-VM5VPUJ4DJIWiK+AZn8FScUqMr6OFrCAYybMYjEEi7W13ParI64MByiXTkKMqZpBmvQ9zxl9Ebq2VUOiZRJYUg==", + "cpu": [ + "riscv64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@babel/parser": "^7.1.0", - "@babel/types": "^7.0.0" + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@types/babel__traverse": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", - "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "node_modules/@oxlint/binding-linux-riscv64-musl": { + "version": "1.74.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-1.74.0.tgz", + "integrity": "sha512-SaDY1gh9rOA592J54g+gu5hkOFFQBZsMmIYHs+NRHG+Uq0OxtuuCXMWQ3vu1830Eugv5uMXyjG+bv2Z9y4IXjw==", + "cpu": [ + "riscv64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@babel/types": "^7.28.2" + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" } }, - "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==", + "node_modules/@oxlint/binding-linux-s390x-gnu": { + "version": "1.74.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.74.0.tgz", + "integrity": "sha512-ZATQeHZCyr6MbDveg0obD5sxLHFOghtOdC5jwVwYlvFWqtFOxctgFEG6Ef/64hYvZrWyhyCckB10AelqLopeDA==", + "cpu": [ + "s390x" + ], "dev": true, "license": "MIT", - "dependencies": { - "@types/http-cache-semantics": "*", - "@types/keyv": "^3.1.4", - "@types/node": "*", - "@types/responselike": "^1.0.0" + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@types/debug": { - "version": "4.1.12", - "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", - "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==", + "node_modules/@oxlint/binding-linux-x64-gnu": { + "version": "1.74.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.74.0.tgz", + "integrity": "sha512-+aIvJyrdeD7LwCQ2WYLMUWNmnbeDRSPb40aBYtPjD9+PTqUwgJnk+HK5yLfSMeqXrMrDhE9uTmtt2y50tvjhHw==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@types/ms": "*" + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@types/discord-rpc": { - "version": "4.0.10", - "resolved": "https://registry.npmjs.org/@types/discord-rpc/-/discord-rpc-4.0.10.tgz", - "integrity": "sha512-V7uQUUjYHwQ+8LyTcPZvExOi8yv310XZgZxzsVljTXhmik4apgRuDj+oqz15n0On0qXunEuvDdhD7VZyQgXm6g==", + "node_modules/@oxlint/binding-linux-x64-musl": { + "version": "1.74.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-musl/-/binding-linux-x64-musl-1.74.0.tgz", + "integrity": "sha512-XyktaR8lhK2qWiCK0Tk8oYD+/cgn+oHA6ddRnxSSXUKkkojkV78CmShZUxQF+yrBFs0SuW+JBOPG6hecyc/iZg==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@types/events": "*" + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@types/estree": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", - "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/events": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@types/events/-/events-3.0.3.tgz", - "integrity": "sha512-trOc4AAUThEz9hapPtSd7wf5tiQKvTtu5b371UxXdTuqzIh0ArcRspRP0i0Viu+LXstIQ1z96t1nsPxT9ol01g==", - "dev": true, - "license": "MIT" - }, - "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==", + "node_modules/@oxlint/binding-openharmony-arm64": { + "version": "1.74.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-openharmony-arm64/-/binding-openharmony-arm64-1.74.0.tgz", + "integrity": "sha512-mzbjrPl4neaVUiJ1fUiEUxTGaSZBoiKtaoB6jmIpz9S+VOA2vDYmJpihQ82w6178V5jxziclTg8Cgj5yF6tTDg==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@types/node": "*" + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" } }, - "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==", + "node_modules/@oxlint/binding-win32-arm64-msvc": { + "version": "1.74.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.74.0.tgz", + "integrity": "sha512-vUAe9okpS2Oa5+lX67lqHMuNUvfkleRKwrUDJ/WJBsgmddvZ1mrsh2HVmuFDRzqFELhaJhFaCNOuR6a7L3rtIA==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@types/node": "*" + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" } }, - "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": "22.19.17", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.17.tgz", - "integrity": "sha512-wGdMcf+vPYM6jikpS/qhg6WiqSV/OhG+jeeHT/KlVqxYfD40iYJf9/AE1uQxVWFvU7MipKRkRv8NSHiCGgPr8Q==", + "node_modules/@oxlint/binding-win32-ia32-msvc": { + "version": "1.74.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-1.74.0.tgz", + "integrity": "sha512-yyXXJyYYSXL4I8K8jAWjJs+J3fa9gH2JmEbo4f5adm+1tNC9itseicBNuwK7BDHvqQ5J534s+yDULu89vYL2ZQ==", + "cpu": [ + "ia32" + ], "dev": true, "license": "MIT", - "dependencies": { - "undici-types": "~6.21.0" + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@types/plist": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@types/plist/-/plist-3.0.5.tgz", - "integrity": "sha512-E6OCaRmAe4WDmWNsL/9RMqdkkzDCY1etutkflWk4c+AcjDU07Pcz1fQwTX0TQz+Pxqn9i4L1TU3UFpjnrcDgxA==", + "node_modules/@oxlint/binding-win32-x64-msvc": { + "version": "1.74.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.74.0.tgz", + "integrity": "sha512-VTC9IYTIMrVUk/i6Ms1ohzzDKZFkWn0KU2OBbPBzgmVZ2V30165T/zK4LztTr0Xgp9fZ1qQZ1rsZAu/rEmySlA==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", "optional": true, - "dependencies": { - "@types/node": "*", - "xmlbuilder": ">=11.0.1" + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@types/qrcode": { - "version": "1.5.6", - "resolved": "https://registry.npmjs.org/@types/qrcode/-/qrcode-1.5.6.tgz", - "integrity": "sha512-te7NQcV2BOvdj2b1hCAHzAoMNuj65kNBMz0KBaxM6c3VGBOhU0dURQKOtH8CFNI/dsKkwlv32p26qYQTWoB5bw==", + "node_modules/@peculiar/asn1-schema": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-schema/-/asn1-schema-2.8.0.tgz", + "integrity": "sha512-7YT0U/ze0tF2QOBbE15gKZwy5tvgGyLRiRHLzhlbOpf7BT032oBSd0haZqXn5W6l26WLlu3dyxzjM+2638/z2Q==", "dev": true, "license": "MIT", "dependencies": { - "@types/node": "*" + "@peculiar/utils": "^2.0.2", + "asn1js": "^3.0.10", + "tslib": "^2.8.1" } }, - "node_modules/@types/qrcode/node_modules/@types/node": { - "version": "24.10.13", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.13.tgz", - "integrity": "sha512-oH72nZRfDv9lADUBSo104Aq7gPHpQZc4BTx38r9xf9pg5LfP6EzSyH2n7qFmmxRQXh7YlUXODcYsg6PuTDSxGg==", + "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": { - "undici-types": "~7.16.0" + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=8.0.0" } }, - "node_modules/@types/qrcode/node_modules/@types/node/node_modules/undici-types": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", - "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/react": { - "version": "19.2.14", - "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", - "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", + "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": { - "csstype": "^3.2.2" - } - }, - "node_modules/@types/react-dom": { - "version": "19.2.3", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", - "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "@types/react": "^19.2.0" + "tslib": "^2.8.1" } }, - "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==", + "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": { - "@types/node": "*" + "@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/@types/verror": { - "version": "1.10.11", - "resolved": "https://registry.npmjs.org/@types/verror/-/verror-1.10.11.tgz", - "integrity": "sha512-RlDm9K7+o5stv0Co8i8ZRGxDbrTxhJtgjqjFyVh/tXQyl/rYtTKlnTvZ88oSTeYREWurwx20Js4kTuKCsFkUtg==", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/@types/ws": { - "version": "8.18.1", - "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", - "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", - "dev": true, + "node_modules/@posthog/browser-common": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@posthog/browser-common/-/browser-common-0.2.0.tgz", + "integrity": "sha512-w+/0/Be/x48uNM7d/M37ZimSGwhuh8WBSvx61xzKzFnuoV5HqmH7GNDhaM3E3exXoBZmWNPS8hM/Ufy8zoOQSg==", "license": "MIT", "dependencies": { - "@types/node": "*" + "@posthog/core": "^1.44.0", + "@posthog/types": "^1.397.1" } }, - "node_modules/@types/yauzl": { - "version": "2.10.3", - "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", - "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", - "dev": true, + "node_modules/@posthog/core": { + "version": "1.44.0", + "resolved": "https://registry.npmjs.org/@posthog/core/-/core-1.44.0.tgz", + "integrity": "sha512-uE+mdKvetxNQC6gWQf4MHIH8bGt+JN6z7ho0A4G3t9G1VGQTx9wHYHNwkKf3gzi+1oAXS9ej2VVY4pjWUU2PWg==", "license": "MIT", - "optional": true, "dependencies": { - "@types/node": "*" + "@posthog/types": "^1.397.0" } }, - "node_modules/@vitejs/plugin-react": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.2.0.tgz", - "integrity": "sha512-YmKkfhOAi3wsB1PhJq5Scj3GXMn3WvtQ/JC0xoopuHoXSdmtdStOpFrYaT1kie2YgFBcIe64ROzMYRjCrYOdYw==", + "node_modules/@posthog/types": { + "version": "1.397.1", + "resolved": "https://registry.npmjs.org/@posthog/types/-/types-1.397.1.tgz", + "integrity": "sha512-W/LpWbKVaaUnfZKuFuHa+Dg03D+fC87cM+PQbG+59JcSPW8F0JcBtSoXmpfrqbpuxUToMo+gktutrUkAb/KQBw==", + "license": "MIT" + }, + "node_modules/@preact/signals": { + "version": "2.9.4", + "resolved": "https://registry.npmjs.org/@preact/signals/-/signals-2.9.4.tgz", + "integrity": "sha512-JzAZXcRkmCNf9wVuxNI7UWseu3++Mm0dZ6UFyjFby44CyKIl7Y06oOsW3KA5Nx0L8tpE87OY5pLe0uQ0xcWKrg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/core": "^7.29.0", - "@babel/plugin-transform-react-jsx-self": "^7.27.1", - "@babel/plugin-transform-react-jsx-source": "^7.27.1", - "@rolldown/pluginutils": "1.0.0-rc.3", - "@types/babel__core": "^7.20.5", - "react-refresh": "^0.18.0" + "@preact/signals-core": "^1.14.4" }, - "engines": { - "node": "^20.19.0 || >=22.12.0" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/preact" }, "peerDependencies": { - "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" + "preact": ">= 10.25.0 || >=11.0.0-0" } }, - "node_modules/@xmldom/xmldom": { - "version": "0.8.11", - "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.11.tgz", - "integrity": "sha512-cQzWCtO6C8TQiYl1ruKNn2U6Ao4o4WBBcbL61yJl84x+j5sOWWFU9X7DpND8XZG3daDppSsigMdfAIl2upQBRw==", + "node_modules/@preact/signals-core": { + "version": "1.14.4", + "resolved": "https://registry.npmjs.org/@preact/signals-core/-/signals-core-1.14.4.tgz", + "integrity": "sha512-HNB6HYeYKhQbJ1aKl+YRjrS4+QWHLKX6qKoUsfS/m0vqzsVaEBiZiaKbG/e+NKk2ch5ALQr/ihWaMHxiCuuWHA==", "dev": true, "license": "MIT", - "engines": { - "node": ">=10.0.0" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/preact" } }, - "node_modules/7zip-bin": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/7zip-bin/-/7zip-bin-5.2.0.tgz", - "integrity": "sha512-ukTPVhqG4jNzMro2qA9HSCSSVJN3aN7tlb+hfqYCt3ER0yWroeA2VR38MNrOHLQ/cVj+DaIMad0kFCtWWowh/A==", - "dev": true, - "license": "MIT" - }, - "node_modules/abbrev": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-3.0.1.tgz", - "integrity": "sha512-AO2ac6pjRB3SJmGJo+v5/aK6Omggp6fsLrs6wN9bd35ulu4cCwaAU9+7ZhXjeqHVkaHThLuzH0nZr0YpCDhygg==", + "node_modules/@react-grab/cli": { + "version": "0.1.48", + "resolved": "https://registry.npmjs.org/@react-grab/cli/-/cli-0.1.48.tgz", + "integrity": "sha512-KXRZFN0b78BeVa4Tq1FC9kiXPpC5lS4pQp/mvQ1azy9dZUJ3zfc7Ei84+yvGh+WoYdceMCFxXfBp6qhU/G056g==", "dev": true, - "license": "ISC", - "engines": { - "node": "^18.17.0 || >=20.5.0" + "dependencies": { + "agent-install": "^0.0.6", + "commander": "^14.0.3", + "ignore": "^7.0.5", + "ora": "^9.4.0", + "package-manager-detector": "^1.6.0", + "picocolors": "^1.1.1", + "prompts": "^2.4.2", + "tinyexec": "^1.1.2" + }, + "bin": { + "react-grab": "bin/cli.js" } }, - "node_modules/acorn": { - "version": "8.16.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", - "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "node_modules/@react-grab/cli/node_modules/agent-install": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/agent-install/-/agent-install-0.0.6.tgz", + "integrity": "sha512-7NRMZ/ZDz2vHevQTgJsocBFpakB1/Wx5ip19YSJuj4VOXpraWztTerViNtdSyARKZT9e2yVwUUB5JXXCE7mNrA==", "dev": true, "license": "MIT", - "optional": true, - "bin": { - "acorn": "bin/acorn" + "dependencies": { + "@iarna/toml": "^2.2.5", + "commander": "^14.0.0", + "jsonc-parser": "^3.3.1", + "picocolors": "^1.1.1", + "prompts": "^2.4.2", + "yaml": "^2.8.3" }, - "engines": { - "node": ">=0.4.0" + "bin": { + "agent-install": "bin/agent-install.mjs" } }, - "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==", + "node_modules/@react-grab/cli/node_modules/commander": { + "version": "14.0.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz", + "integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==", "dev": true, "license": "MIT", "engines": { - "node": ">= 14" + "node": ">=20" } }, - "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "node_modules/@react-grab/cli/node_modules/ignore": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.6.tgz", + "integrity": "sha512-BAg6QkE8W+TuQLrrw0Ugr7HegXduRuuj8/ti2kSOc+jz1dmx8/WNcjr6XGnq5YpDWxFwwaavqD0+jIUOKelTsw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@react-three/fiber": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/@react-three/fiber/-/fiber-9.6.1.tgz", + "integrity": "sha512-zF0rsKcVYpcJwbFEnv2HkHX9cvOEgsfQo/X8lwmR2dn13S4qEQJXir9fxf5js2LQFoXqxOY7MDkOkYx2uZ4gSg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.17.8", + "@types/webxr": "*", + "base64-js": "^1.5.1", + "buffer": "^6.0.3", + "its-fine": "^2.0.0", + "react-use-measure": "^2.1.7", + "scheduler": "^0.27.0", + "suspend-react": "^0.1.3", + "use-sync-external-store": "^1.4.0", + "zustand": "^5.0.3" + }, + "peerDependencies": { + "expo": ">=43.0", + "expo-asset": ">=8.4", + "expo-file-system": ">=11.0", + "expo-gl": ">=11.0", + "react": ">=19 <19.3", + "react-dom": ">=19 <19.3", + "react-native": ">=0.78", + "three": ">=0.156" + }, + "peerDependenciesMeta": { + "expo": { + "optional": true + }, + "expo-asset": { + "optional": true + }, + "expo-file-system": { + "optional": true + }, + "expo-gl": { + "optional": true + }, + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + } + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.3", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.3.tgz", + "integrity": "sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/pluginutils": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.4.0.tgz", + "integrity": "sha512-MfPp06CjRLfXQ3wY0R8vJDYBy/MvVcc9OulEfR0B8Iv9ko+GCNaRZ+EpJYFl27LhKsZK0o420sYCRHCjfCgeUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.2.tgz", + "integrity": "sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.2.tgz", + "integrity": "sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.2.tgz", + "integrity": "sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.2.tgz", + "integrity": "sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.2.tgz", + "integrity": "sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.2.tgz", + "integrity": "sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.2.tgz", + "integrity": "sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.2.tgz", + "integrity": "sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.2.tgz", + "integrity": "sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.2.tgz", + "integrity": "sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.2.tgz", + "integrity": "sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.2.tgz", + "integrity": "sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.2.tgz", + "integrity": "sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.2.tgz", + "integrity": "sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.2.tgz", + "integrity": "sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.2.tgz", + "integrity": "sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.2.tgz", + "integrity": "sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.2.tgz", + "integrity": "sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.2.tgz", + "integrity": "sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.2.tgz", + "integrity": "sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.2.tgz", + "integrity": "sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.2.tgz", + "integrity": "sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.2.tgz", + "integrity": "sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.2.tgz", + "integrity": "sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.2.tgz", + "integrity": "sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@sentry/conventions": { + "version": "0.16.0", + "resolved": "https://registry.npmjs.org/@sentry/conventions/-/conventions-0.16.0.tgz", + "integrity": "sha512-fO9PLmHdVURcSPUpWCItWAtgKiMwGdJHbovoSEyLplX5sxs2ugvI4CBPTrkkgqhObnZOD0CnWBKDzSVQYBKEyQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/@sentry/core": { + "version": "10.66.0", + "resolved": "https://registry.npmjs.org/@sentry/core/-/core-10.66.0.tgz", + "integrity": "sha512-9UbgSvds7bMJsP561eWmeyMLcfOmnwxtnx2QuW3yLobzP2Ob7CyJCOzP4tGzlTAGDrzShkFEZhiyuBUKiEK2oQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sentry/conventions": "^0.16.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@sentry/node": { + "version": "10.66.0", + "resolved": "https://registry.npmjs.org/@sentry/node/-/node-10.66.0.tgz", + "integrity": "sha512-5Ow7iQiRjaSaEOmqEIkYV368hFFzShIZCXPoj+wX3JtOmKFrsNu6lr8/VJlQs7cyjFS6tzE50PrLrD8iAPS/8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@opentelemetry/api": "^1.9.1", + "@opentelemetry/instrumentation": "^0.220.0", + "@opentelemetry/sdk-trace-base": "^2.9.0", + "@sentry/conventions": "^0.16.0", + "@sentry/core": "10.66.0", + "@sentry/node-core": "10.66.0", + "@sentry/opentelemetry": "10.66.0", + "@sentry/server-utils": "10.66.0", + "import-in-the-middle": "^3.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@sentry/node-core": { + "version": "10.66.0", + "resolved": "https://registry.npmjs.org/@sentry/node-core/-/node-core-10.66.0.tgz", + "integrity": "sha512-SUnXHROqSdSetKgZC1goDEKCuMz3OmQ1h4rxzWeexLyqan+pensZXfLouP6jzZXlA8e/HP7uQg78LnfqYDcZlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sentry/conventions": "^0.16.0", + "@sentry/core": "10.66.0", + "@sentry/opentelemetry": "10.66.0", + "import-in-the-middle": "^3.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.9.0", + "@opentelemetry/core": "^1.30.1 || ^2.1.0", + "@opentelemetry/exporter-trace-otlp-http": ">=0.57.0 <1", + "@opentelemetry/instrumentation": ">=0.57.1 <1", + "@opentelemetry/sdk-trace-base": "^1.30.1 || ^2.1.0" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + }, + "@opentelemetry/core": { + "optional": true + }, + "@opentelemetry/exporter-trace-otlp-http": { + "optional": true + }, + "@opentelemetry/instrumentation": { + "optional": true + }, + "@opentelemetry/sdk-trace-base": { + "optional": true + } + } + }, + "node_modules/@sentry/opentelemetry": { + "version": "10.66.0", + "resolved": "https://registry.npmjs.org/@sentry/opentelemetry/-/opentelemetry-10.66.0.tgz", + "integrity": "sha512-K5Y9IettN9yIOnpqCs40HRLGqaGUoaQ50+ZsLqX2kPCk3TzJVpKZ9icKwaJ4Nxm72QgGVT5Ovfr/9FXbgd3b/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sentry/conventions": "^0.16.0", + "@sentry/core": "10.66.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.9.0", + "@opentelemetry/core": "^1.30.1 || ^2.1.0", + "@opentelemetry/sdk-trace-base": "^1.30.1 || ^2.1.0" + } + }, + "node_modules/@sentry/server-utils": { + "version": "10.66.0", + "resolved": "https://registry.npmjs.org/@sentry/server-utils/-/server-utils-10.66.0.tgz", + "integrity": "sha512-h9EM9Wz9Mc6w2Vn7Fyh6ozjy4JC2UbUvWf9Ra1HYA4KMeYqjFA5/o0oB4pg4w/TF+rb+9OF/HNqxjuczxpudpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@apm-js-collab/code-transformer": "^0.18.0", + "@apm-js-collab/code-transformer-bundler-plugins": "^0.6.1", + "@apm-js-collab/tracing-hooks": "^0.13.0", + "@sentry/conventions": "^0.16.0", + "@sentry/core": "10.66.0" + }, + "engines": { + "node": ">=18" + } + }, + "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/@tweenjs/tween.js": { + "version": "23.1.3", + "resolved": "https://registry.npmjs.org/@tweenjs/tween.js/-/tween.js-23.1.3.tgz", + "integrity": "sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz", + "integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "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/discord-rpc": { + "version": "4.0.11", + "resolved": "https://registry.npmjs.org/@types/discord-rpc/-/discord-rpc-4.0.11.tgz", + "integrity": "sha512-w2WgzgtyDNHMbmIeogN4f1uy1Mz2Woe+27XcvyjPRcCI9QdWyJifHHIJopwXm8f04fHia/vkhId/eyqxmbLoHw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/events": "*" + } + }, + "node_modules/@types/esrecurse": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz", + "integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/events": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/events/-/events-3.0.3.tgz", + "integrity": "sha512-trOc4AAUThEz9hapPtSd7wf5tiQKvTtu5b371UxXdTuqzIh0ArcRspRP0i0Viu+LXstIQ1z96t1nsPxT9ol01g==", + "dev": true, + "license": "MIT" + }, + "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/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT", + "peer": true + }, + "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": "22.20.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.1.tgz", + "integrity": "sha512-EANqOCF9QFyra+4pfxUcX9STKJpCLjMbObVzljIJomAWSnuSIEAvyzEU53GaajbXJEgdh0iEcPL+DGvpUd4k1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/qrcode": { + "version": "1.5.6", + "resolved": "https://registry.npmjs.org/@types/qrcode/-/qrcode-1.5.6.tgz", + "integrity": "sha512-te7NQcV2BOvdj2b1hCAHzAoMNuj65kNBMz0KBaxM6c3VGBOhU0dURQKOtH8CFNI/dsKkwlv32p26qYQTWoB5bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/react": { + "version": "19.2.17", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.17.tgz", + "integrity": "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==", + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@types/react-reconciler": { + "version": "0.28.9", + "resolved": "https://registry.npmjs.org/@types/react-reconciler/-/react-reconciler-0.28.9.tgz", + "integrity": "sha512-HHM3nxyUZ3zAylX8ZEyrDNd2XZOnQ0D5XfunJF5FLQnZbHHYq4UWvW1QfelQNXv1ICNkwYhfxjwfnqivYB6bFg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*" + } + }, + "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/@types/stats.js": { + "version": "0.17.4", + "resolved": "https://registry.npmjs.org/@types/stats.js/-/stats.js-0.17.4.tgz", + "integrity": "sha512-jIBvWWShCvlBqBNIZt0KAshWpvSjhkwkEu4ZUcASoAvhmrgAUI2t1dXrjSL4xXVLB4FznPrIsX3nKXFl/Dt4vA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/three": { + "version": "0.185.1", + "resolved": "https://registry.npmjs.org/@types/three/-/three-0.185.1.tgz", + "integrity": "sha512-db1xTb+EgYF2didW+eudSvVPtn75zo+fGsY8ShQrJY/B5ZBmC2Fiaykv3aImHAlCNEGuMPkPGXBJGLwzu5mC7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@dimforge/rapier3d-compat": "~0.12.0", + "@tweenjs/tween.js": "~23.1.3", + "@types/stats.js": "*", + "@types/webxr": ">=0.5.17", + "fflate": "~0.8.2", + "meshoptimizer": "~1.1.1" + } + }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "license": "MIT", + "optional": true + }, + "node_modules/@types/webxr": { + "version": "0.5.24", + "resolved": "https://registry.npmjs.org/@types/webxr/-/webxr-0.5.24.tgz", + "integrity": "sha512-h8fgEd/DpoS9CBrjEQXR+dIDraopAEfu4wYVNY2tEPwk60stPWhvZMf4Foo5FakuQ7HFZoa8WceaWFervK2Ovg==", + "license": "MIT" + }, + "node_modules/@types/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.64.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.64.0.tgz", + "integrity": "sha512-qjhfuTfLXjA4IOzXvz0rTjT01BqEiIgPoUeMwiEjnaHKJMTNo8rH5pYW1a2L/0Dnux2fPC85AeyJoWaGa8WxTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.2.0.tgz", + "integrity": "sha512-YmKkfhOAi3wsB1PhJq5Scj3GXMn3WvtQ/JC0xoopuHoXSdmtdStOpFrYaT1kie2YgFBcIe64ROzMYRjCrYOdYw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.29.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-rc.3", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.18.0" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/@xmldom/xmldom": { + "version": "0.8.13", + "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.13.tgz", + "integrity": "sha512-KRYzxepc14G/CEpEGc3Yn+JKaAeT63smlDr+vjB8jRfgTBBI9wRj/nkQEO+ucV8p8I9bfKLWp37uHgFrbntPvw==", + "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/acorn": { + "version": "8.17.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz", + "integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peer": true, + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.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/agent-install": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/agent-install/-/agent-install-0.0.5.tgz", + "integrity": "sha512-nHlms9BkP8ZiY79HrwCGiA2DcNaXrAaJrCM/BEqQ7MEsSKyCk+2A76xPGylIfASZSZE0SaU3T0bNSg4rBPIJAQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@iarna/toml": "^2.2.5", + "commander": "^14.0.0", + "jsonc-parser": "^3.3.1", + "picocolors": "^1.1.1", + "prompts": "^2.4.2", + "yaml": "^2.8.3" + }, + "bin": { + "agent-install": "bin/agent-install.mjs" + } + }, + "node_modules/agent-install/node_modules/commander": { + "version": "14.0.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz", + "integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "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/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "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/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/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/app-builder-lib/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/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "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/astring": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/astring/-/astring-1.9.0.tgz", + "integrity": "sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg==", + "dev": true, + "license": "MIT", + "bin": { + "astring": "bin/astring" + } + }, + "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/atomically": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/atomically/-/atomically-2.1.1.tgz", + "integrity": "sha512-P4w9o2dqARji6P7MHprklbfiArZAWvo07yW7qs3pdljb3BWr12FIB7W+p0zJiuiVsUpRO0iZn1kFFcpPegg0tQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "stubborn-fs": "^2.0.0", + "when-exit": "^2.1.4" + } + }, + "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==", + "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/baseline-browser-mapping": { + "version": "2.10.43", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.43.tgz", + "integrity": "sha512-AjYpR78kDWAY3Efj+cDTFH9t9SCoL7OoTp1BOb0mQV7S+6CiLwnWM3FyxhJtdPufDFKzmCSFoUncKjWgJEZTCQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, + "node_modules/bippy": { + "version": "0.5.43", + "resolved": "https://registry.npmjs.org/bippy/-/bippy-0.5.43.tgz", + "integrity": "sha512-Tvu7b1M7+d8b9/YHaCeODEsi2CgbuoBql+dWSBrNnCuqJ1gMUeY3i0r+319hvjjl5GVBP6FFWxrKnq3fhZER0w==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "react": ">=17.0.1" + } + }, + "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/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.7", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.7.tgz", + "integrity": "sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.28.6", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.6.tgz", + "integrity": "sha512-FQBYNK15VMslhLHpA7+n+n1GOlF1kId2xcCg7/j95f24AOF6VDYMNH4mFxF7KuaTdv627faazpOAjFzMrfJOUw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.10.42", + "caniuse-lite": "^1.0.30001803", + "electron-to-chromium": "^1.5.389", + "node-releases": "^2.0.51", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "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", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "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==", + "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/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "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": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" + "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" }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" + "engines": { + "node": ">=8" } }, - "node_modules/ajv-keywords": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", - "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "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", - "peerDependencies": { - "ajv": "^6.9.1" + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" } }, - "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==", + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", "license": "MIT", "engines": { - "node": ">=8" + "node": ">=6" } }, - "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==", + "node_modules/caniuse-lite": { + "version": "1.0.30001806", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001806.tgz", + "integrity": "sha512-72Cuvd95zbSYPKq6Fhg8eDJRlzgWDf7/mtoZv6Qe/DYNCEBdNxoA3+rZAU2ZhGCpZlns3EssFavaZomckT5Uuw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, "license": "MIT", "dependencies": { - "color-convert": "^2.0.1" + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" }, "engines": { - "node": ">=8" + "node": ">=10" }, "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "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/app-builder-bin": { - "version": "5.0.0-alpha.12", - "resolved": "https://registry.npmjs.org/app-builder-bin/-/app-builder-bin-5.0.0-alpha.12.tgz", - "integrity": "sha512-j87o0j6LqPL3QRr8yid6c+Tt5gC7xNfYo6uQIQkorAC6MpeayVMZrEDzKmJJ/Hlv7EnOQpaRm53k6ktDYZyB6w==", + "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/app-builder-lib": { - "version": "26.8.1", - "resolved": "https://registry.npmjs.org/app-builder-lib/-/app-builder-lib-26.8.1.tgz", - "integrity": "sha512-p0Im/Dx5C4tmz8QEE1Yn4MkuPC8PrnlRneMhWJj7BBXQfNTJUshM/bp3lusdEsDbvvfJZpXWnYesgSLvwtM2Zw==", + "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/cjs-module-lexer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-2.2.0.tgz", + "integrity": "sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/cli-cursor": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", + "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", "dev": true, "license": "MIT", "dependencies": { - "@develar/schema-utils": "~2.6.5", - "@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.3", - "@electron/universal": "2.0.3", - "@malept/flatpak-bundler": "^0.4.0", - "@types/fs-extra": "9.0.13", - "async-exit-hook": "^2.0.1", - "builder-util": "26.8.1", - "builder-util-runtime": "9.5.1", - "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.8.1", - "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.0.3", - "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", - "which": "^5.0.0" + "restore-cursor": "^5.0.0" }, "engines": { - "node": ">=14.0.0" + "node": ">=18" }, - "peerDependencies": { - "dmg-builder": "26.8.1", - "electron-builder-squirrel-windows": "26.8.1" + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "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==", + "node_modules/cli-spinners": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-3.4.0.tgz", + "integrity": "sha512-bXfOC4QcT1tKXGorxL3wbJm6XJPDqEnij2gQ2m7ESQuE+/z9YFIWnl/5RpTiKWbMq3EVKR4fRLJGn6DVfu0mpw==", "dev": true, "license": "MIT", + "engines": { + "node": ">=18.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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": { - "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" + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" }, "engines": { - "node": ">=14" + "node": ">=12" + } + }, + "node_modules/cliui/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/cliui/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" }, - "optionalDependencies": { - "global-agent": "^3.0.0" + "engines": { + "node": ">=8" } }, - "node_modules/app-builder-lib/node_modules/@electron/get/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==", + "node_modules/cliui/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/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==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, "engines": { - "node": ">=6" + "node": ">=7.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==", + "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==", + "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": { - "graceful-fs": "^4.2.0", - "jsonfile": "^4.0.0", - "universalify": "^0.1.0" + "delayed-stream": "~1.0.0" }, "engines": { - "node": ">=6 <7 || >=8" + "node": ">= 0.8" } }, - "node_modules/app-builder-lib/node_modules/@electron/get/node_modules/jsonfile": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", - "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "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", - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "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" + "engines": { + "node": ">= 6" } }, - "node_modules/app-builder-lib/node_modules/@electron/get/node_modules/universalify": { + "node_modules/compare-version": { "version": "0.1.2", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", - "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "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": ">= 4.0.0" + "node": ">=0.10.0" } }, - "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==", + "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/conf": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/conf/-/conf-15.1.0.tgz", + "integrity": "sha512-Uy5YN9KEu0WWDaZAVJ5FAmZoaJt9rdK6kH+utItPyGsCqCgaTKkrmZx3zoE0/3q6S3bcp3Ihkk+ZqPxWxFK5og==", "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/sibiraj-s" - } - ], "license": "MIT", + "dependencies": { + "ajv": "^8.17.1", + "ajv-formats": "^3.0.1", + "atomically": "^2.0.3", + "debounce-fn": "^6.0.0", + "dot-prop": "^10.0.0", + "env-paths": "^3.0.0", + "json-schema-typed": "^8.0.1", + "semver": "^7.7.2", + "uint8array-extras": "^1.5.0" + }, "engines": { - "node": ">=8" + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/app-builder-lib/node_modules/which": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/which/-/which-5.0.0.tgz", - "integrity": "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ==", + "node_modules/conf/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", - "dependencies": { - "isexe": "^3.1.1" - }, "bin": { - "node-which": "bin/which.js" + "semver": "bin/semver.js" }, "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": ">=10" } }, - "node_modules/app-builder-lib/node_modules/which/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==", + "node_modules/confbox": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.4.tgz", + "integrity": "sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==", "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=18" + "license": "MIT" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/core-js": { + "version": "3.49.0", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.49.0.tgz", + "integrity": "sha512-es1U2+YTtzpwkxVLwAFdSpaIMyQaq0PBgm3YD1W3Qpsn1NAmO3KSgZfu+oGSWVu6NvLHoHCV/aYcsE5wiB7ALg==", + "hasInstallScript": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" } }, - "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==", - "license": "Python-2.0" + "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/assert-plus": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==", + "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, - "engines": { - "node": ">=0.8" - } + "peer": true }, - "node_modules/astral-regex": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", - "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", + "node_modules/cross-env": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-10.1.0.tgz", + "integrity": "sha512-GsYosgnACZTADcmEyJctkJIoqAhHjttw7RsFrVoJNXbsWWqaq6Ym+7kZjq6mS45O0jij6vtiReppKQEtqWy6Dw==", "dev": true, "license": "MIT", - "optional": true, + "dependencies": { + "@epic-web/invariant": "^1.0.0", + "cross-spawn": "^7.0.6" + }, + "bin": { + "cross-env": "dist/bin/cross-env.js", + "cross-env-shell": "dist/bin/cross-env-shell.js" + }, "engines": { - "node": ">=8" + "node": ">=20" } }, - "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==", + "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": ">=0.12.0" + "node": ">= 8" } }, - "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, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", "license": "MIT" }, - "node_modules/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==", + "node_modules/debounce-fn": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/debounce-fn/-/debounce-fn-6.0.0.tgz", + "integrity": "sha512-rBMW+F2TXryBwB54Q0d8drNEI+TfoS9JpNTAoVpukbWEhjXQq4rySFYLaqXMFXwdv61Zb2OHtj5bviSoimqxRQ==", "dev": true, - "license": "ISC", + "license": "MIT", + "dependencies": { + "mimic-function": "^5.0.0" + }, "engines": { - "node": ">= 4.0.0" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/balanced-match": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.2.tgz", - "integrity": "sha512-x0K50QvKQ97fdEz2kPehIerj+YTeptKF9hyYkKf6egnwmMWAkADiO0QCzSp0R5xN8FTZgYaBfSaue46Ej62nMg==", - "dev": true, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "license": "MIT", "dependencies": { - "jackspeak": "^4.2.3" + "ms": "^2.1.3" }, "engines": { - "node": "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" + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true } - ], - "license": "MIT" - }, - "node_modules/baseline-browser-mapping": { - "version": "2.9.19", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.19.tgz", - "integrity": "sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "baseline-browser-mapping": "dist/cli.js" } }, - "node_modules/bindings": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", - "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "node_modules/decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", "license": "MIT", - "optional": true, - "dependencies": { - "file-uri-to-path": "1.0.0" + "engines": { + "node": ">=0.10.0" } }, - "node_modules/bippy": { - "version": "0.5.39", - "resolved": "https://registry.npmjs.org/bippy/-/bippy-0.5.39.tgz", - "integrity": "sha512-8hE8rKSl8JWyeaY+JjpnmceWAZPpLEyzOZQpWXM5Rc7861c5WotMJHy2aRZKZrGA8nMpvLNF01t4yQQ+HcZG3w==", + "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", - "peerDependencies": { - "react": ">=17.0.1" + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/bl": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", - "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "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", - "dependencies": { - "buffer": "^5.5.0", - "inherits": "^2.0.4", - "readable-stream": "^3.4.0" + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/boolean": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/boolean/-/boolean-3.2.0.tgz", - "integrity": "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==", + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", "dev": true, "license": "MIT", - "optional": true + "peer": true }, - "node_modules/brace-expansion": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.2.tgz", - "integrity": "sha512-Pdk8c9poy+YhOgVWw1JNN22/HcivgKWwpxKq04M/jTmHyCZn12WPJebZxdjSa5TmBqISrUSgNYU3eRORljfCCw==", + "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", - "dependencies": { - "balanced-match": "^4.0.2" - }, "engines": { - "node": "20 || >=22" + "node": ">=10" } }, - "node_modules/browserslist": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", - "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "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, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], "license": "MIT", + "optional": true, "dependencies": { - "baseline-browser-mapping": "^2.9.0", - "caniuse-lite": "^1.0.30001759", - "electron-to-chromium": "^1.5.263", - "node-releases": "^2.0.27", - "update-browserslist-db": "^1.2.0" - }, - "bin": { - "browserslist": "cli.js" + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" }, "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/buffer": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", - "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "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, - "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", + "optional": true, "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.1.13" + "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/buffer-crc32": { - "version": "0.2.13", - "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", - "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "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": "*" + "node": ">=0.4.0" } }, - "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.8.1", - "resolved": "https://registry.npmjs.org/builder-util/-/builder-util-26.8.1.tgz", - "integrity": "sha512-pm1lTYbGyc90DHgCDO7eo8Rl4EqKLciayNbZqGziqnH9jrlKe8ZANGdityLZU+pJh16dfzjAx2xQq9McuIPEtw==", + "node_modules/deslop-js": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/deslop-js/-/deslop-js-0.8.1.tgz", + "integrity": "sha512-a8wpwOPo6HsYyRndn17C88mNzeQD6t17yhZ8qpyFWTxj5jQeWtXDj+zJfnuT8++5A4LaNeNAnKs1edVWuadNdQ==", "dev": true, - "license": "MIT", + "license": "SEE LICENSE IN LICENSE", "dependencies": { - "@types/debug": "^4.1.6", - "7zip-bin": "~5.2.0", - "app-builder-bin": "5.0.0-alpha.12", - "builder-util-runtime": "9.5.1", - "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" + "@oxc-project/types": "^0.138.0", + "fast-glob": "^3.3.3", + "minimatch": "^10.2.5", + "oxc-parser": "^0.138.0", + "oxc-resolver": "^11.23.0", + "typescript": ">=5.0.4 <6" } }, - "node_modules/builder-util-runtime": { - "version": "9.5.1", - "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.5.1.tgz", - "integrity": "sha512-qt41tMfgHTllhResqM5DcnHyDIWNgzHvuY2jDcYP9iaGpkWxTUzV6GQjDeLnlR1/DtdlcsWQbA7sByMpmJFTLQ==", - "license": "MIT", - "dependencies": { - "debug": "^4.3.4", - "sax": "^1.2.4" + "node_modules/deslop-js/node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" }, "engines": { - "node": ">=12.0.0" + "node": ">=14.17" } }, - "node_modules/cac": { - "version": "6.7.14", - "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", - "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "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", - "engines": { - "node": ">=8" - } + "optional": true + }, + "node_modules/dijkstrajs": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz", + "integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==", + "license": "MIT" }, - "node_modules/cacache": { - "version": "19.0.1", - "resolved": "https://registry.npmjs.org/cacache/-/cacache-19.0.1.tgz", - "integrity": "sha512-hdsUxulXCi5STId78vRVYEtDAjq99ICAUktLTeTYsLoTE6Z8dS0c8pWNCxwdrk9YfJeobDZc2Y186hD/5ZQgFQ==", + "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": "ISC", + "license": "MIT", "dependencies": { - "@npmcli/fs": "^4.0.0", - "fs-minipass": "^3.0.0", - "glob": "^10.2.2", - "lru-cache": "^10.0.1", - "minipass": "^7.0.3", - "minipass-collect": "^2.0.1", - "minipass-flush": "^1.0.5", - "minipass-pipeline": "^1.2.4", - "p-map": "^7.0.2", - "ssri": "^12.0.0", - "tar": "^7.4.3", - "unique-filename": "^4.0.0" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" + "minimatch": "^3.0.5", + "p-limit": "^3.1.0 " } }, - "node_modules/cacache/node_modules/balanced-match": { + "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/cacache/node_modules/brace-expansion": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz", - "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==", + "node_modules/dir-compare/node_modules/brace-expansion": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", "dev": true, "license": "MIT", "dependencies": { - "balanced-match": "^1.0.0" + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" } }, - "node_modules/cacache/node_modules/fs-minipass": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-3.0.3.tgz", - "integrity": "sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw==", + "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": { - "minipass": "^7.0.3" + "brace-expansion": "^1.1.7" }, "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + "node": "*" } }, - "node_modules/cacache/node_modules/glob": { - "version": "10.5.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", - "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", - "dev": true, - "license": "ISC", + "node_modules/discord-rpc": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/discord-rpc/-/discord-rpc-4.0.1.tgz", + "integrity": "sha512-HOvHpbq5STRZJjQIBzwoKnQ0jHplbEWFWlPDwXXKm/bILh4nzjcg7mNqll0UY7RsjFoaXA7e/oYb/4lvpda2zA==", + "license": "MIT", "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.4", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^1.11.1" - }, - "bin": { - "glob": "dist/esm/bin.mjs" + "node-fetch": "^2.6.1", + "ws": "^7.3.1" }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "optionalDependencies": { + "register-scheme": "github:devsnek/node-register-scheme" } }, - "node_modules/cacache/node_modules/glob/node_modules/jackspeak": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", - "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "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": "BlueOak-1.0.0", + "license": "MIT", "dependencies": { - "@isaacs/cliui": "^8.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - }, - "optionalDependencies": { - "@pkgjs/parseargs": "^0.11.0" + "app-builder-lib": "26.15.3", + "builder-util": "26.15.3", + "fs-extra": "^10.1.0", + "js-yaml": "^4.1.0" } }, - "node_modules/cacache/node_modules/glob/node_modules/jackspeak/node_modules/@isaacs/cliui": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", - "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", - "dev": true, - "license": "ISC", - "dependencies": { - "string-width": "^5.1.2", - "string-width-cjs": "npm:string-width@^4.2.0", - "strip-ansi": "^7.0.1", - "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", - "wrap-ansi": "^8.1.0", - "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" - }, - "engines": { - "node": ">=12" + "node_modules/dompurify": { + "version": "3.4.12", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.12.tgz", + "integrity": "sha512-zQvGet8Z2sWbQhCmfFz/T5QWH2oBmjnqK3qvOjaqaNLrLEF912WamU+ohnTp0TCep/MFVHpdJuCZEdFOdTnEFg==", + "license": "(MPL-2.0 OR Apache-2.0)", + "optionalDependencies": { + "@types/trusted-types": "^2.0.7" } }, - "node_modules/cacache/node_modules/glob/node_modules/jackspeak/node_modules/@isaacs/cliui/node_modules/string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "node_modules/dot-prop": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-10.1.0.tgz", + "integrity": "sha512-MVUtAugQMOff5RnBy2d9N31iG0lNwg1qAoAOn7pOK5wf94WIaE3My2p3uwTQuvS2AcqchkcR3bHByjaM0mmi7Q==", "dev": true, "license": "MIT", "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" + "type-fest": "^5.0.0" }, "engines": { - "node": ">=12" + "node": ">=20" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/cacache/node_modules/glob/node_modules/jackspeak/node_modules/@isaacs/cliui/node_modules/string-width/node_modules/emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "dev": true, - "license": "MIT" - }, - "node_modules/cacache/node_modules/glob/node_modules/jackspeak/node_modules/@isaacs/cliui/node_modules/strip-ansi": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", - "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.0.1" - }, + "license": "BSD-2-Clause", "engines": { "node": ">=12" }, "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" + "url": "https://dotenvx.com" } }, - "node_modules/cacache/node_modules/glob/node_modules/jackspeak/node_modules/@isaacs/cliui/node_modules/strip-ansi/node_modules/ansi-regex": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", - "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "node_modules/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": "MIT", + "license": "BSD-2-Clause", + "dependencies": { + "dotenv": "^16.4.5" + }, "engines": { "node": ">=12" }, "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" + "url": "https://dotenvx.com" } }, - "node_modules/cacache/node_modules/glob/node_modules/jackspeak/node_modules/@isaacs/cliui/node_modules/wrap-ansi": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", - "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "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": { - "ansi-styles": "^6.1.0", - "string-width": "^5.0.1", - "strip-ansi": "^7.0.1" + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" }, "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + "node": ">= 0.4" } }, - "node_modules/cacache/node_modules/glob/node_modules/jackspeak/node_modules/@isaacs/cliui/node_modules/wrap-ansi/node_modules/ansi-styles": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", - "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "node_modules/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": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "license": "BSD-3-Clause", + "dependencies": { + "readable-stream": "^2.0.2" } }, - "node_modules/cacache/node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/cacache/node_modules/minimatch": { - "version": "9.0.9", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", - "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "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": "ISC", + "license": "Apache-2.0", "dependencies": { - "brace-expansion": "^2.0.2" + "jake": "^10.8.5" }, - "engines": { - "node": ">=16 || 14 >=14.17" + "bin": { + "ejs": "bin/cli.js" }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "engines": { + "node": ">=0.10.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==", + "node_modules/electron": { + "version": "43.1.1", + "resolved": "https://registry.npmjs.org/electron/-/electron-43.1.1.tgz", + "integrity": "sha512-I5c5vfuVvaXpWx3IZdwvXgxQW44+e7OP1wXGVQkogLeSFSkUZ6sLCcWV05AdEcs65AO5tAIJJwbp7ixw+LdarA==", "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": ">=10.6.0" + "node": ">= 22.12.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==", + "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": { - "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" + "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": ">=8" + "node": ">=14.0.0" } }, - "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==", + "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": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" + "app-builder-lib": "26.15.3", + "builder-util": "26.15.3", + "electron-winstaller": "5.4.0" } }, - "node_modules/camelcase": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "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", - "engines": { - "node": ">=6" + "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/caniuse-lite": { - "version": "1.0.30001770", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001770.tgz", - "integrity": "sha512-x/2CLQ1jHENRbHg5PSId2sXq1CIO1CISvwWAj027ltMVG2UNgW+w9oH2+HzgEIRFembL8bUlXtfbBHR1fCg2xw==", + "node_modules/electron-to-chromium": { + "version": "1.5.393", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.393.tgz", + "integrity": "sha512-kiDJdIUawuEIcp9XoICKp1iTYDEbgguIPq526N1Q7jIQDeQ3CqoMx71025PI/7E48Ddtw2HuWsVjY7afEgNxmg==", "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/caniuse-lite" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "CC-BY-4.0" + "license": "ISC" }, - "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, + "node_modules/electron-updater": { + "version": "6.8.9", + "resolved": "https://registry.npmjs.org/electron-updater/-/electron-updater-6.8.9.tgz", + "integrity": "sha512-ZhVxM9iGONUpZGI1FxdMRgJjUFXi7AYGVa5PwKlO1tV1/4zDxQmfKpXOHVztKrd6L9rLcFjERvi1Mf2vxyTkig==", "license": "MIT", "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" + "builder-util-runtime": "9.7.0", + "fs-extra": "^10.1.0", + "js-yaml": "^4.1.0", + "lazy-val": "^1.0.5", + "lodash.escaperegexp": "^4.1.2", + "lodash.isequal": "^4.5.0", + "semver": "~7.7.3", + "tiny-typed-emitter": "^2.1.0" + } + }, + "node_modules/electron-updater/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" }, "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==", + "node_modules/electron-vite": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/electron-vite/-/electron-vite-5.0.0.tgz", + "integrity": "sha512-OHp/vjdlubNlhNkPkL/+3JD34ii5ov7M0GpuXEVdQeqdQ3ulvVR7Dg/rNBLfS5XPIFwgoBLDf9sjjrL+CuDyRQ==", "dev": true, - "license": "BlueOak-1.0.0", + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.4", + "@babel/plugin-transform-arrow-functions": "^7.27.1", + "cac": "^6.7.14", + "esbuild": "^0.25.11", + "magic-string": "^0.30.19", + "picocolors": "^1.1.1" + }, + "bin": { + "electron-vite": "bin/electron-vite.js" + }, "engines": { - "node": ">=18" + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "@swc/core": "^1.0.0", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + } } }, - "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==", + "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, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/sibiraj-s" - } - ], + "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" + "node": ">=8.0.0" + }, + "optionalDependencies": { + "@electron/windows-sign": "^1.1.2" } }, - "node_modules/cli-cursor": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", - "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "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": { - "restore-cursor": "^3.1.0" + "graceful-fs": "^4.1.2", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" }, "engines": { - "node": ">=8" + "node": ">=6 <7 || >=8" } }, - "node_modules/cli-spinners": { - "version": "2.9.2", - "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", - "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", + "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", - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "peer": true, + "optionalDependencies": { + "graceful-fs": "^4.1.6" } }, - "node_modules/cli-truncate": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-2.1.0.tgz", - "integrity": "sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==", + "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", - "optional": true, - "dependencies": { - "slice-ansi": "^3.0.0", - "string-width": "^4.2.0" - }, + "peer": true, "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">= 4.0.0" } }, - "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==", + "node_modules/electron/node_modules/@types/node": { + "version": "24.13.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.13.3.tgz", + "integrity": "sha512-Dh8vAsV36ig5wa9OX4pXvMc9D3Veibfw2wix0CUwYODLD8nkj9UsLjASr49nPg+2eKzxhBV+v7L8pXvT4e639Q==", "dev": true, - "license": "ISC", + "license": "MIT", "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^7.0.0" - }, - "engines": { - "node": ">=12" + "undici-types": "~7.18.0" } }, - "node_modules/clone": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", - "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", + "node_modules/electron/node_modules/undici-types": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "dev": true, + "license": "MIT" + }, + "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==", + "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", - "engines": { - "node": ">=0.8" + "dependencies": { + "once": "^1.4.0" } }, - "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==", + "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", - "dependencies": { - "mimic-response": "^1.0.0" + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/clone-response/node_modules/mimic-response": { + "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/mimic-response/-/mimic-response-1.0.1.tgz", - "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==", + "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": ">=4" + "node": ">= 0.4" } }, - "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==", + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, "engines": { - "node": ">=7.0.0" + "node": ">= 0.4" } }, - "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==", + "node_modules/es-module-lexer": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.1.tgz", + "integrity": "sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==", + "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==", + "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": { - "delayed-stream": "~1.0.0" + "es-errors": "^1.3.0" }, "engines": { - "node": ">= 0.8" + "node": ">= 0.4" } }, - "node_modules/commander": { - "version": "14.0.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz", - "integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==", + "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": ">=20" + "node": ">= 0.4" } }, - "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==", + "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", - "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/convert-source-map": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", - "dev": true, - "license": "MIT" + "optional": true }, - "node_modules/core-util-is": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==", + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", "dev": true, + "hasInstallScript": true, "license": "MIT", - "optional": true + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } }, - "node_modules/crc": { - "version": "3.8.0", - "resolved": "https://registry.npmjs.org/crc/-/crc-3.8.0.tgz", - "integrity": "sha512-iX3mfgcTMIq3ZKLIsVFAbv7+Mc10kxabAGQb8HvjA1o3T1PIYprbakQ65d3I+2HGHt6nSKkM9PYjgoJO2KcFBQ==", + "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", - "optional": true, - "dependencies": { - "buffer": "^5.1.0" + "engines": { + "node": ">=6" } }, - "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==", + "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, - "peer": true + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, - "node_modules/cross-env": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-10.1.0.tgz", - "integrity": "sha512-GsYosgnACZTADcmEyJctkJIoqAhHjttw7RsFrVoJNXbsWWqaq6Ym+7kZjq6mS45O0jij6vtiReppKQEtqWy6Dw==", + "node_modules/eslint": { + "version": "10.7.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.7.0.tgz", + "integrity": "sha512-GVTD7s1vdIl6UYvAfriOPeY1Df8LIZjfofLvHwde+erDHGGuHyuM6xoxRxmHiebhYuD2p1vN4wWh0XzPARSGDQ==", "dev": true, "license": "MIT", - "dependencies": { - "@epic-web/invariant": "^1.0.0", - "cross-spawn": "^7.0.6" + "peer": true, + "workspaces": [ + "packages/*" + ], + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.2", + "@eslint/config-array": "^0.23.5", + "@eslint/config-helpers": "^0.6.0", + "@eslint/core": "^1.2.1", + "@eslint/plugin-kit": "^0.7.2", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^9.1.2", + "eslint-visitor-keys": "^5.0.1", + "espree": "^11.2.0", + "esquery": "^1.7.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "minimatch": "^10.2.4", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" }, "bin": { - "cross-env": "dist/bin/cross-env.js", - "cross-env-shell": "dist/bin/cross-env-shell.js" + "eslint": "bin/eslint.js" }, "engines": { - "node": ">=20" + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": 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==", + "node_modules/eslint-plugin-react-hooks": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.1.1.tgz", + "integrity": "sha512-f2I7Gw6JbvCexzIInuSbZpfdQ44D7iqdWX01FKLvrPgqxoE7oMj8clOfto8U6vYiz4yd5oKu39rRSVOe1zRu0g==", "dev": true, "license": "MIT", "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" + "@babel/core": "^7.24.4", + "@babel/parser": "^7.24.4", + "hermes-parser": "^0.25.1", + "zod": "^3.25.0 || ^4.0.0", + "zod-validation-error": "^3.5.0 || ^4.0.0" }, "engines": { - "node": ">= 8" + "node": ">=18" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 || ^10.0.0" } }, - "node_modules/csstype": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", - "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "node_modules/eslint-scope": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz", + "integrity": "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==", "dev": true, - "license": "MIT" - }, - "node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "license": "MIT", + "license": "BSD-2-Clause", "dependencies": { - "ms": "^2.1.3" + "@types/esrecurse": "^4.3.1", + "@types/estree": "^1.0.8", + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" }, "engines": { - "node": ">=6.0" + "node": "^20.19.0 || ^22.13.0 || >=24" }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/decamelize": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" + "funding": { + "url": "https://opencollective.com/eslint" } }, - "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==", + "node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", "dev": true, - "license": "MIT", - "dependencies": { - "mimic-response": "^3.1.0" - }, + "license": "Apache-2.0", "engines": { - "node": ">=10" + "node": "^20.19.0 || ^22.13.0 || >=24" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://opencollective.com/eslint" } }, - "node_modules/defaults": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", - "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==", + "node_modules/eslint/node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { - "clone": "^1.0.2" + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" } }, - "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==", + "node_modules/eslint/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", "dev": true, "license": "MIT", - "engines": { - "node": ">=10" - } + "peer": true }, - "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==", + "node_modules/espree": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz", + "integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==", "dev": true, - "license": "MIT", - "optional": true, + "license": "BSD-2-Clause", + "peer": true, "dependencies": { - "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "gopd": "^1.0.1" + "acorn": "^8.16.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^5.0.1" }, "engines": { - "node": ">= 0.4" + "node": "^20.19.0 || ^22.13.0 || >=24" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://opencollective.com/eslint" } }, - "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==", + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", "dev": true, - "license": "MIT", - "optional": true, + "license": "BSD-3-Clause", "dependencies": { - "define-data-property": "^1.0.1", - "has-property-descriptors": "^1.0.0", - "object-keys": "^1.1.1" + "estraverse": "^5.1.0" }, "engines": { - "node": ">= 0.4" + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "engines": { + "node": ">=4.0" } }, - "node_modules/delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", "dev": true, - "license": "MIT", + "license": "BSD-2-Clause", "engines": { - "node": ">=0.4.0" + "node": ">=4.0" } }, - "node_modules/detect-libc": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", - "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", "dev": true, - "license": "Apache-2.0", + "license": "MIT" + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "peer": true, "engines": { - "node": ">=8" + "node": ">=0.10.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==", + "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": "MIT", - "optional": true + "license": "Apache-2.0" }, - "node_modules/dijkstrajs": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz", - "integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==", + "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==", + "dev": true, "license": "MIT" }, - "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==", + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", "dev": true, "license": "MIT", "dependencies": { - "minimatch": "^3.0.5", - "p-limit": "^3.1.0 " + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" } }, - "node_modules/dir-compare/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", "dev": true, "license": "ISC", "dependencies": { - "brace-expansion": "^1.1.7" + "is-glob": "^4.0.1" }, "engines": { - "node": "*" + "node": ">= 6" } }, - "node_modules/dir-compare/node_modules/minimatch/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", "dev": true, "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } + "peer": true }, - "node_modules/dir-compare/node_modules/minimatch/node_modules/brace-expansion/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==", + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", "dev": true, - "license": "MIT" - }, - "node_modules/discord-rpc": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/discord-rpc/-/discord-rpc-4.0.1.tgz", - "integrity": "sha512-HOvHpbq5STRZJjQIBzwoKnQ0jHplbEWFWlPDwXXKm/bILh4nzjcg7mNqll0UY7RsjFoaXA7e/oYb/4lvpda2zA==", "license": "MIT", + "peer": true + }, + "node_modules/fast-uri": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.4.tgz", + "integrity": "sha512-8JnbkQ4juDyvYs4mgFGQqg4yCYtFDtUtmp2QIQq11ZZe5CFQ5wcqm1rqDgAh/QdMySuBnPzMUiJUNZG5N/AiQw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "dev": true, + "license": "ISC", "dependencies": { - "node-fetch": "^2.6.1", - "ws": "^7.3.1" - }, - "optionalDependencies": { - "register-scheme": "github:devsnek/node-register-scheme" + "reusify": "^1.0.4" } }, - "node_modules/discord-rpc/node_modules/ws": { - "version": "7.5.10", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", - "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", + "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": ">=8.3.0" + "node": ">=12.0.0" }, "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": "^5.0.2" + "picomatch": "^3 || ^4" }, "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { + "picomatch": { "optional": true } } }, - "node_modules/dmg-builder": { - "version": "26.8.1", - "resolved": "https://registry.npmjs.org/dmg-builder/-/dmg-builder-26.8.1.tgz", - "integrity": "sha512-glMJgnTreo8CFINujtAhCgN96QAqApDMZ8Vl1r8f0QT8QprvC1UCltV4CcWj20YoIyLZx6IUskaJZ0NV8fokcg==", + "node_modules/fflate": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.3.tgz", + "integrity": "sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==", "dev": true, - "license": "MIT", - "dependencies": { - "app-builder-lib": "26.8.1", - "builder-util": "26.8.1", - "fs-extra": "^10.1.0", - "iconv-lite": "^0.6.2", - "js-yaml": "^4.1.0" - }, - "optionalDependencies": { - "dmg-license": "^1.0.11" - } + "license": "MIT" }, - "node_modules/dmg-license": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/dmg-license/-/dmg-license-1.0.11.tgz", - "integrity": "sha512-ZdzmqwKmECOWJpqefloC5OJy1+WZBBse5+MR88z9g9Zn4VY+WYUkAyojmhzJckH5YbbZGcYIuGAkY5/Ys5OM2Q==", + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], + "peer": true, "dependencies": { - "@types/plist": "^3.0.1", - "@types/verror": "^1.10.3", - "ajv": "^6.10.0", - "crc": "^3.8.0", - "iconv-corefoundation": "^1.1.7", - "plist": "^3.0.4", - "smart-buffer": "^4.0.2", - "verror": "^1.10.0" - }, - "bin": { - "dmg-license": "bin/dmg-license.js" + "flat-cache": "^4.0.0" }, "engines": { - "node": ">=8" - } - }, - "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": ">=16.0.0" } }, - "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/file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "license": "MIT", + "optional": true }, - "node_modules/dunder-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", - "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "node_modules/filelist": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.6.tgz", + "integrity": "sha512-5giy2PkLYY1cP39p17Ech+2xlpTRL9HLspOfEgm0L6CwBXBTgsK5ou0JtzYuepxkaQ/tvhCFIJ5uXo0OrM2DxA==", "dev": true, - "license": "MIT", + "license": "Apache-2.0", "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "es-errors": "^1.3.0", - "gopd": "^1.2.0" - }, - "engines": { - "node": ">= 0.4" + "minimatch": "^5.0.1" } }, - "node_modules/eastasianwidth": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", - "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "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/ejs": { - "version": "3.1.10", - "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", - "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", + "node_modules/filelist/node_modules/brace-expansion": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz", + "integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==", "dev": true, - "license": "Apache-2.0", + "license": "MIT", "dependencies": { - "jake": "^10.8.5" - }, - "bin": { - "ejs": "bin/cli.js" - }, - "engines": { - "node": ">=0.10.0" + "balanced-match": "^1.0.0" } }, - "node_modules/electron": { - "version": "42.3.3", - "resolved": "https://registry.npmjs.org/electron/-/electron-42.3.3.tgz", - "integrity": "sha512-0MwYp9wTb7TrtTalOYqeW+suqd9T/Znstr/nDLKqFGIjHdBZX339guo3mQqTPURRZ/UQmYM4uMpzKpI5wLptfQ==", + "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": "MIT", + "license": "ISC", "dependencies": { - "@electron/get": "^5.0.0", - "@types/node": "^24.9.0", - "extract-zip": "^2.0.1" - }, - "bin": { - "electron": "cli.js", - "install-electron": "install.js" + "brace-expansion": "^2.0.1" }, "engines": { - "node": ">= 22.12.0" + "node": ">=10" } }, - "node_modules/electron-builder": { - "version": "26.8.1", - "resolved": "https://registry.npmjs.org/electron-builder/-/electron-builder-26.8.1.tgz", - "integrity": "sha512-uWhx1r74NGpCagG0ULs/P9Nqv2nsoo+7eo4fLUOB8L8MdWltq9odW/uuLXMFCDGnPafknYLZgjNX0ZIFRzOQAw==", + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dev": true, "license": "MIT", - "dependencies": { - "app-builder-lib": "26.8.1", - "builder-util": "26.8.1", - "builder-util-runtime": "9.5.1", - "chalk": "^4.1.2", - "ci-info": "^4.2.0", - "dmg-builder": "26.8.1", - "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" + "dependencies": { + "to-regex-range": "^5.0.1" }, "engines": { - "node": ">=14.0.0" + "node": ">=8" } }, - "node_modules/electron-builder-squirrel-windows": { - "version": "26.8.1", - "resolved": "https://registry.npmjs.org/electron-builder-squirrel-windows/-/electron-builder-squirrel-windows-26.8.1.tgz", - "integrity": "sha512-o288fIdgPLHA76eDrFADHPoo7VyGkDCYbLV1GzndaMSAVBoZrGvM9m2IehdcVMzdAZJ2eV9bgyissQXHv5tGzA==", + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", "dev": true, "license": "MIT", "peer": true, "dependencies": { - "app-builder-lib": "26.8.1", - "builder-util": "26.8.1", - "electron-winstaller": "5.4.0" + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/electron-publish": { - "version": "26.8.1", - "resolved": "https://registry.npmjs.org/electron-publish/-/electron-publish-26.8.1.tgz", - "integrity": "sha512-q+jrSTIh/Cv4eGZa7oVR+grEJo/FoLMYBAnSL5GCtqwUpr1T+VgKB/dn1pnzxIxqD8S/jP1yilT9VrwCqINR4w==", + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { - "@types/fs-extra": "^9.0.11", - "builder-util": "26.8.1", - "builder-util-runtime": "9.5.1", - "chalk": "^4.1.2", - "form-data": "^4.0.5", - "fs-extra": "^10.1.0", - "lazy-val": "^1.0.5", - "mime": "^2.5.2" + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" } }, - "node_modules/electron-to-chromium": { - "version": "1.5.286", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.286.tgz", - "integrity": "sha512-9tfDXhJ4RKFNerfjdCcZfufu49vg620741MNs26a9+bhLThdB+plgMeou98CAaHu/WATj2iHOOHTp1hWtABj2A==", + "node_modules/flatted": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", "dev": true, - "license": "ISC" + "license": "ISC", + "peer": true }, - "node_modules/electron-updater": { - "version": "6.8.3", - "resolved": "https://registry.npmjs.org/electron-updater/-/electron-updater-6.8.3.tgz", - "integrity": "sha512-Z6sgw3jgbikWKXei1ENdqFOxBP0WlXg3TtKfz0rgw2vIZFJUyI4pD7ZN7jrkm7EoMK+tcm/qTnPUdqfZukBlBQ==", + "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": { - "builder-util-runtime": "9.5.1", - "fs-extra": "^10.1.0", - "js-yaml": "^4.1.0", - "lazy-val": "^1.0.5", - "lodash.escaperegexp": "^4.1.2", - "lodash.isequal": "^4.5.0", - "semver": "~7.7.3", - "tiny-typed-emitter": "^2.1.0" + "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/electron-vite": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/electron-vite/-/electron-vite-5.0.0.tgz", - "integrity": "sha512-OHp/vjdlubNlhNkPkL/+3JD34ii5ov7M0GpuXEVdQeqdQ3ulvVR7Dg/rNBLfS5XPIFwgoBLDf9sjjrL+CuDyRQ==", - "dev": true, + "node_modules/framer-motion": { + "version": "12.42.2", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.42.2.tgz", + "integrity": "sha512-5XY9luDiu0oHfHBjpDthFMh0ES+122w6p/papSJBweMkO8Sn+PW2QaEgRblQBpWFnuvZS5qvarpt/hO2pjGmnw==", "license": "MIT", "dependencies": { - "@babel/core": "^7.28.4", - "@babel/plugin-transform-arrow-functions": "^7.27.1", - "cac": "^6.7.14", - "esbuild": "^0.25.11", - "magic-string": "^0.30.19", - "picocolors": "^1.1.1" - }, - "bin": { - "electron-vite": "bin/electron-vite.js" - }, - "engines": { - "node": "^20.19.0 || >=22.12.0" + "motion-dom": "^12.42.2", + "motion-utils": "^12.39.0", + "tslib": "^2.4.0" }, "peerDependencies": { - "@swc/core": "^1.0.0", - "vite": "^5.0.0 || ^6.0.0 || ^7.0.0" + "@emotion/is-prop-valid": "*", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" }, "peerDependenciesMeta": { - "@swc/core": { + "@emotion/is-prop-valid": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { "optional": true } } }, - "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, + "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==", "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" + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" }, "engines": { - "node": ">=8.0.0" - }, - "optionalDependencies": { - "@electron/windows-sign": "^1.1.2" + "node": ">=12" } }, - "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==", + "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/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", "dev": true, + "hasInstallScript": true, "license": "MIT", - "peer": true, - "dependencies": { - "graceful-fs": "^4.1.2", - "jsonfile": "^4.0.0", - "universalify": "^0.1.0" - }, + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": ">=6 <7 || >=8" + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, - "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==", + "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", - "peer": true, - "optionalDependencies": { - "graceful-fs": "^4.1.6" + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "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==", + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", "dev": true, "license": "MIT", - "peer": true, "engines": { - "node": ">= 4.0.0" + "node": ">=6.9.0" } }, - "node_modules/electron/node_modules/@types/node": { - "version": "24.10.13", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.13.tgz", - "integrity": "sha512-oH72nZRfDv9lADUBSo104Aq7gPHpQZc4BTx38r9xf9pg5LfP6EzSyH2n7qFmmxRQXh7YlUXODcYsg6PuTDSxGg==", + "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==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-east-asian-width": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.6.0.tgz", + "integrity": "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==", "dev": true, "license": "MIT", - "dependencies": { - "undici-types": "~7.16.0" + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/electron/node_modules/@types/node/node_modules/undici-types": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", - "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "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" - }, - "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==", - "license": "MIT" - }, - "node_modules/encoding": { - "version": "0.1.13", - "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", - "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", - "optional": true, + "license": "MIT", "dependencies": { - "iconv-lite": "^0.6.2" + "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/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==", + "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": { - "once": "^1.4.0" + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" } }, - "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==", + "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": "^12.20.0 || ^14.13.1 || >=16.0.0" + "node": ">=8" }, "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==", + "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": "MIT" + "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/es-define-property": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", "dev": true, - "license": "MIT", + "license": "ISC", + "peer": true, + "dependencies": { + "is-glob": "^4.0.3" + }, "engines": { - "node": ">= 0.4" + "node": ">=10.13.0" } }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "node_modules/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.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", "dev": true, "license": "MIT", - "engines": { - "node": ">= 0.4" + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" } }, - "node_modules/es-object-atoms": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "node_modules/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": "MIT", + "license": "ISC", "dependencies": { - "es-errors": "^1.3.0" + "brace-expansion": "^1.1.7" }, "engines": { - "node": ">= 0.4" + "node": "*" } }, - "node_modules/es-set-tostringtag": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", - "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "node_modules/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": "MIT", + "license": "BSD-3-Clause", + "optional": true, "dependencies": { - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.6", - "has-tostringtag": "^1.0.2", - "hasown": "^2.0.2" + "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": ">= 0.4" + "node": ">=10.0" } }, - "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/esbuild": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", - "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "node_modules/global-agent/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, - "hasInstallScript": true, - "license": "MIT", + "license": "ISC", + "optional": true, "bin": { - "esbuild": "bin/esbuild" + "semver": "bin/semver.js" }, "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.12", - "@esbuild/android-arm": "0.25.12", - "@esbuild/android-arm64": "0.25.12", - "@esbuild/android-x64": "0.25.12", - "@esbuild/darwin-arm64": "0.25.12", - "@esbuild/darwin-x64": "0.25.12", - "@esbuild/freebsd-arm64": "0.25.12", - "@esbuild/freebsd-x64": "0.25.12", - "@esbuild/linux-arm": "0.25.12", - "@esbuild/linux-arm64": "0.25.12", - "@esbuild/linux-ia32": "0.25.12", - "@esbuild/linux-loong64": "0.25.12", - "@esbuild/linux-mips64el": "0.25.12", - "@esbuild/linux-ppc64": "0.25.12", - "@esbuild/linux-riscv64": "0.25.12", - "@esbuild/linux-s390x": "0.25.12", - "@esbuild/linux-x64": "0.25.12", - "@esbuild/netbsd-arm64": "0.25.12", - "@esbuild/netbsd-x64": "0.25.12", - "@esbuild/openbsd-arm64": "0.25.12", - "@esbuild/openbsd-x64": "0.25.12", - "@esbuild/openharmony-arm64": "0.25.12", - "@esbuild/sunos-x64": "0.25.12", - "@esbuild/win32-arm64": "0.25.12", - "@esbuild/win32-ia32": "0.25.12", - "@esbuild/win32-x64": "0.25.12" + "node": ">=10" } }, - "node_modules/escalade": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", - "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "node_modules/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": ">=6" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "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==", + "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", - "optional": true, "engines": { - "node": ">=10" + "node": ">= 0.4" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/estree-walker": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", - "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "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": { - "@types/estree": "^1.0.0" - } - }, - "node_modules/exponential-backoff": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.3.tgz", - "integrity": "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==", - "dev": true, - "license": "Apache-2.0" - }, - "node_modules/extract-zip": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", - "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "debug": "^4.1.1", - "get-stream": "^5.1.0", - "yauzl": "^2.10.0" - }, - "bin": { - "extract-zip": "cli.js" + "@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.17.0" + "node": ">=10.19.0" }, - "optionalDependencies": { - "@types/yauzl": "^2.9.1" + "funding": { + "url": "https://github.com/sindresorhus/got?sponsor=1" } }, - "node_modules/extsprintf": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.4.1.tgz", - "integrity": "sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA==", - "dev": true, - "engines": [ - "node >=0.6.0" - ], - "license": "MIT", - "optional": true - }, - "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==", - "dev": true, - "license": "MIT" + "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==", + "license": "ISC" }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "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" + "license": "MIT", + "engines": { + "node": ">=8" + } }, - "node_modules/fd-slicer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", - "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", + "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": { - "pend": "~1.2.0" + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/fdir": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "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": ">=12.0.0" - }, - "peerDependencies": { - "picomatch": "^3 || ^4" + "node": ">= 0.4" }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/file-uri-to-path": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", - "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", - "license": "MIT", - "optional": true - }, - "node_modules/filelist": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", - "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==", + "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": "Apache-2.0", + "license": "MIT", "dependencies": { - "minimatch": "^5.0.1" + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/filelist/node_modules/minimatch": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", - "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "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": "ISC", + "license": "MIT", "dependencies": { - "brace-expansion": "^2.0.1" + "function-bind": "^1.1.2" }, "engines": { - "node": ">=10" + "node": ">= 0.4" } }, - "node_modules/filelist/node_modules/minimatch/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "node_modules/hermes-estree": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", + "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==", + "dev": true, + "license": "MIT" + }, + "node_modules/hermes-parser": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz", + "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==", "dev": true, "license": "MIT", "dependencies": { - "balanced-match": "^1.0.0" + "hermes-estree": "0.25.1" } }, - "node_modules/filelist/node_modules/minimatch/node_modules/brace-expansion/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/find-up": { + "node_modules/hosted-git-info": { "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "license": "MIT", + "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": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" + "lru-cache": "^6.0.0" }, "engines": { - "node": ">=8" + "node": ">=10" } }, - "node_modules/foreground-child": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", - "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "node_modules/hosted-git-info/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": { - "cross-spawn": "^7.0.6", - "signal-exit": "^4.0.1" + "yallist": "^4.0.0" }, "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "node": ">=10" } }, - "node_modules/foreground-child/node_modules/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "node_modules/hosted-git-info/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", - "engines": { - "node": ">=14" + "license": "ISC" + }, + "node_modules/http-cache-semantics": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", + "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" }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "engines": { + "node": ">= 14" } }, - "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==", + "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": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "es-set-tostringtag": "^2.1.0", - "hasown": "^2.0.4", - "mime-types": "^2.1.35" + "quick-lru": "^5.1.1", + "resolve-alpn": "^1.0.0" }, "engines": { - "node": ">= 6" + "node": ">=10.19.0" } }, - "node_modules/framer-motion": { - "version": "12.42.0", - "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.42.0.tgz", - "integrity": "sha512-wp7EJnfWaaEScVygKv3e20udoRz+LbtxScsuTkakAxfXmt+ReC6WyPW2nINRAGvd+hG9odwcjBLyOTPjH5pBRA==", + "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": { - "motion-dom": "^12.42.0", - "motion-utils": "^12.39.0", - "tslib": "^2.4.0" - }, - "peerDependencies": { - "@emotion/is-prop-valid": "*", - "react": "^18.0.0 || ^19.0.0", - "react-dom": "^18.0.0 || ^19.0.0" + "agent-base": "^7.1.2", + "debug": "4" }, - "peerDependenciesMeta": { - "@emotion/is-prop-valid": { - "optional": true + "engines": { + "node": ">= 14" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" }, - "react": { - "optional": true + { + "type": "patreon", + "url": "https://www.patreon.com/feross" }, - "react-dom": { - "optional": true + { + "type": "consulting", + "url": "https://feross.org/support" } - } + ], + "license": "BSD-3-Clause" }, - "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==", + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, + "peer": true, "engines": { - "node": ">=12" + "node": ">= 4" } }, - "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==", + "node_modules/import-in-the-middle": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-3.3.1.tgz", + "integrity": "sha512-0rymlHSFLwZ0ixx8DaQkoIyZojJPY2a0K2nEYslhKJ6jIYO/m0IcCb7iQsFPmS7WmKwISZiIrv5Icstrw/CmqA==", "dev": true, - "license": "ISC" + "license": "Apache-2.0", + "dependencies": { + "cjs-module-lexer": "^2.2.0", + "es-module-lexer": "^2.2.0", + "module-details-from-path": "^1.0.4" + }, + "engines": { + "node": ">=18" + } }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], + "peer": true, "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + "node": ">=0.8.19" } }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "node_modules/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": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" } }, - "node_modules/gensync": { - "version": "1.0.0-beta.2", - "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", - "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "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-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", "dev": true, "license": "MIT", "engines": { - "node": ">=6.9.0" + "node": ">=0.10.0" } }, - "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==", - "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==", + "license": "MIT", "engines": { - "node": "6.* || 8.* || >= 10.*" + "node": ">=8" } }, - "node_modules/get-intrinsic": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", - "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", "dev": true, "license": "MIT", "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" + "is-extglob": "^2.1.1" }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=0.10.0" } }, - "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==", + "node_modules/is-interactive": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz", + "integrity": "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", "dev": true, "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.1", - "es-object-atoms": "^1.0.0" - }, "engines": { - "node": ">= 0.4" + "node": ">=0.12.0" } }, - "node_modules/get-stream": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", - "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "node_modules/is-unicode-supported": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz", + "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==", "dev": true, "license": "MIT", - "dependencies": { - "pump": "^3.0.0" - }, "engines": { - "node": ">=8" + "node": ">=18" }, "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==", + "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": "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" - }, + "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": "*" + "node": ">= 18.0.0" }, "funding": { - "url": "https://github.com/sponsors/isaacs" + "url": "https://github.com/sponsors/gjtorikian/" } }, - "node_modules/glob/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } + "license": "ISC" }, - "node_modules/glob/node_modules/minimatch/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "dev": true, + "node_modules/its-fine": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/its-fine/-/its-fine-2.0.0.tgz", + "integrity": "sha512-KLViCmWx94zOvpLwSlsx6yOCeMhZYaxrJV87Po5k/FoZzcPSahvK5qJ7fYhS61sZi5ikmh2S3Hz55A2l3U69ng==", "license": "MIT", "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" + "@types/react-reconciler": "^0.28.9" + }, + "peerDependencies": { + "react": "^19.0.0" } }, - "node_modules/glob/node_modules/minimatch/node_modules/brace-expansion/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/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==", + "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": "BSD-3-Clause", - "optional": true, + "license": "Apache-2.0", "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" + "async": "^3.2.6", + "filelist": "^1.0.4", + "picocolors": "^1.1.1" + }, + "bin": { + "jake": "bin/cli.js" }, "engines": { - "node": ">=10.0" + "node": ">=10" } }, - "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==", + "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", - "optional": true, - "dependencies": { - "define-properties": "^1.2.1", - "gopd": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "bin": { + "jiti": "lib/jiti-cli.mjs" } }, - "node_modules/gopd": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.0.tgz", + "integrity": "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], "license": "MIT", - "engines": { - "node": ">= 0.4" + "dependencies": { + "argparse": "^2.0.1" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "bin": { + "js-yaml": "bin/js-yaml.js" } }, - "node_modules/got": { - "version": "11.8.6", - "resolved": "https://registry.npmjs.org/got/-/got-11.8.6.tgz", - "integrity": "sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==", + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", "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" + "bin": { + "jsesc": "bin/jsesc" }, "engines": { - "node": ">=10.19.0" - }, - "funding": { - "url": "https://github.com/sindresorhus/got?sponsor=1" + "node": ">=6" } }, - "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==", - "license": "ISC" + "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/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "node_modules/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", - "engines": { - "node": ">=8" - } + "license": "MIT" }, - "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==", + "node_modules/json-schema-typed": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.2.tgz", + "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", "dev": true, "license": "MIT", - "optional": true, - "dependencies": { - "es-define-property": "^1.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } + "peer": true }, - "node_modules/has-symbols": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "node_modules/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", - "engines": { - "node": ">= 0.4" + "bin": { + "json5": "lib/cli.js" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "engines": { + "node": ">=6" } }, - "node_modules/has-tostringtag": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", - "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "node_modules/jsonc-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz", + "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==", "dev": true, + "license": "MIT" + }, + "node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", "license": "MIT", "dependencies": { - "has-symbols": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" + "universalify": "^2.0.0" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "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/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==", + "node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", "dev": true, "license": "MIT", - "dependencies": { - "function-bind": "^1.1.2" - }, "engines": { - "node": ">= 0.4" + "node": ">=6" } }, - "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==", + "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==", + "license": "MIT" + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", "dev": true, - "license": "ISC", + "license": "MIT", + "peer": true, "dependencies": { - "lru-cache": "^6.0.0" + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" }, "engines": { - "node": ">=10" + "node": ">= 0.8.0" } }, - "node_modules/hosted-git-info/node_modules/lru-cache": { + "node_modules/locate-path": { "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", "dev": true, - "license": "ISC", + "license": "MIT", + "peer": true, "dependencies": { - "yallist": "^4.0.0" + "p-locate": "^5.0.0" }, "engines": { "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/hosted-git-info/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "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": "ISC" + "license": "MIT" }, - "node_modules/http-cache-semantics": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", - "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", - "dev": true, - "license": "BSD-2-Clause" + "node_modules/lodash.escaperegexp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz", + "integrity": "sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw==", + "license": "MIT" }, - "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==", + "node_modules/lodash.isequal": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", + "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==", + "deprecated": "This package is deprecated. Use require('node:util').isDeepStrictEqual instead.", + "license": "MIT" + }, + "node_modules/log-symbols": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-7.0.1.tgz", + "integrity": "sha512-ja1E3yCr9i/0hmBVaM0bfwDjnGy8I/s6PP4DFp+yP+a+mrHO4Rm7DtmnqROTUkHIkqffC84YY7AeqX6oFk0WFg==", "dev": true, "license": "MIT", "dependencies": { - "agent-base": "^7.1.0", - "debug": "^4.3.4" + "is-unicode-supported": "^2.0.0", + "yoctocolors": "^2.1.1" }, "engines": { - "node": ">= 14" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "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==", + "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", - "dependencies": { - "quick-lru": "^5.1.1", - "resolve-alpn": "^1.0.0" - }, "engines": { - "node": ">=10.19.0" + "node": ">=8" } }, - "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==", + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/lucide-react": { + "version": "1.25.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.25.0.tgz", + "integrity": "sha512-/mdJTRbiwcLOQ1NZZK1amZF9rIZyvO18D6r9TngE6TG1NmqHgFuT4eE7Xrkm9UsXMbBJD1NlfwHVltCDWHrOTw==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", "dev": true, "license": "MIT", "dependencies": { - "agent-base": "^7.1.2", - "debug": "4" - }, - "engines": { - "node": ">= 14" + "@jridgewell/sourcemap-codec": "^1.5.5" } }, - "node_modules/iconv-corefoundation": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/iconv-corefoundation/-/iconv-corefoundation-1.1.7.tgz", - "integrity": "sha512-T10qvkw0zz4wnm560lOEg0PovVqUXuOFhhHAkixw8/sycy7TJt7v/RrkEKEQnAw2viPSJu6iAkErxnzR0g8PpQ==", + "node_modules/magicast": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.5.3.tgz", + "integrity": "sha512-pVKE4UdSQ7DvHzivsCIFx2BJn1mHG6KsyrFcaxFx6tONdneEuThrDx0Cj3AMg58KyN4pzYT+LHOotxDQDjNvkw==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], "dependencies": { - "cli-truncate": "^2.1.0", - "node-addon-api": "^1.6.3" - }, - "engines": { - "node": "^8.11.2 || >=10" + "@babel/parser": "^7.29.3", + "@babel/types": "^7.29.0", + "source-map-js": "^1.2.1" } }, - "node_modules/iconv-lite": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "devOptional": true, + "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": { - "safer-buffer": ">= 2.1.2 < 3.0.0" + "escape-string-regexp": "^4.0.0" }, "engines": { - "node": ">=0.10.0" + "node": ">=10" } }, - "node_modules/ieee754": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "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, - "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": "BSD-3-Clause" + "license": "MIT", + "engines": { + "node": ">= 0.4" + } }, - "node_modules/imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", "dev": true, "license": "MIT", "engines": { - "node": ">=0.8.19" + "node": ">= 8" } }, - "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==", + "node_modules/meriyah": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/meriyah/-/meriyah-6.1.4.tgz", + "integrity": "sha512-Sz8FzjzI0kN13GK/6MVEsVzMZEPvOhnmmI1lU5+/1cGOiK3QUahntrNNtdVeihrO7t9JpoH75iMNXg6R6uWflQ==", "dev": true, "license": "ISC", - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" + "engines": { + "node": ">=18.0.0" } }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "node_modules/meshoptimizer": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/meshoptimizer/-/meshoptimizer-1.1.1.tgz", + "integrity": "sha512-oRFNWJRDA/WTrVj7NWvqa5HqE1t9MYDj2VaWirQCzCCrAd2GHrqR/sQezCxiWATPNlKTcRaPRHPJwIRoPBAp5g==", "dev": true, - "license": "ISC" + "license": "MIT" }, - "node_modules/ip-address": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz", - "integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==", + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "dev": true, "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, "engines": { - "node": ">= 12" + "node": ">=8.6" } }, - "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "node_modules/micromatch/node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, "license": "MIT", "engines": { - "node": ">=8" + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/is-interactive": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", - "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", + "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": ">=8" + "node": ">=4.0.0" } }, - "node_modules/is-unicode-supported": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", - "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "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": ">=10" + "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" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "engines": { + "node": ">= 0.6" } }, - "node_modules/isbinaryfile": { - "version": "5.0.7", - "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-5.0.7.tgz", - "integrity": "sha512-gnWD14Jh3FzS3CPhF0AxNOJ8CxqeblPTADzI38r0wt8ZyQl5edpy75myt08EG2oKvpyiqSqsx+Wkz9vtkbTqYQ==", + "node_modules/mimic-function": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", + "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", "dev": true, "license": "MIT", "engines": { - "node": ">= 18.0.0" + "node": ">=18" }, "funding": { - "url": "https://github.com/sponsors/gjtorikian/" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "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": "ISC" + "license": "MIT", + "engines": { + "node": ">=4" + } }, - "node_modules/jackspeak": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.2.3.tgz", - "integrity": "sha512-ykkVRwrYvFm1nb2AJfKKYPr0emF6IiXDYUaFx4Zn9ZuIH7MrzEZ3sD5RlqGXNRpHtvUHJyOnCEFxOlNDtGo7wg==", + "node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { - "@isaacs/cliui": "^9.0.0" + "brace-expansion": "^5.0.5" }, "engines": { - "node": "20 || >=22" + "node": "18 || 20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, - "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==", + "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": "Apache-2.0", + "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": { - "async": "^3.2.6", - "filelist": "^1.0.4", - "picocolors": "^1.1.1" - }, - "bin": { - "jake": "bin/cli.js" + "minipass": "^7.1.2" }, "engines": { - "node": ">=10" + "node": ">= 18" } }, - "node_modules/jiti": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", - "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", + "node_modules/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": { - "jiti": "lib/jiti-cli.mjs" + "mkdirp": "bin/cmd.js" } }, - "node_modules/js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "node_modules/module-details-from-path": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/module-details-from-path/-/module-details-from-path-1.0.4.tgz", + "integrity": "sha512-EGWKgxALGMgzvxYF1UyGTy0HXX/2vHLkw6+NvDKW2jypWbHpjQuj4UMcqQWXHERJhVGKikolT06G3bcKe4fi7w==", "dev": true, "license": "MIT" }, - "node_modules/js-yaml": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", - "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "node_modules/motion": { + "version": "12.42.2", + "resolved": "https://registry.npmjs.org/motion/-/motion-12.42.2.tgz", + "integrity": "sha512-Atvv11yUKIid41cVrRBDVX5m8tF8kNpExRSlbpt6APClhDjtwQssgFHhQzejxw7/7YYbjHSPKBVbHo05BuJT5Q==", "license": "MIT", "dependencies": { - "argparse": "^2.0.1" + "framer-motion": "^12.42.2", + "tslib": "^2.4.0" }, - "bin": { - "js-yaml": "bin/js-yaml.js" + "peerDependencies": { + "@emotion/is-prop-valid": "*", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/is-prop-valid": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } } }, - "node_modules/jsesc": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", - "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "node_modules/motion-dom": { + "version": "12.42.2", + "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.42.2.tgz", + "integrity": "sha512-5gIMWLp/PycBtJRJWRgjxke5n8dlvkSn2DrYW+tr3XcqAZY1xZh6BJyooJXCM8wdfM7wfMjkBJNLge1CKPUIRA==", + "license": "MIT", + "dependencies": { + "motion-utils": "^12.39.0" + } + }, + "node_modules/motion-utils": { + "version": "12.39.0", + "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.39.0.tgz", + "integrity": "sha512-8nadJAJjTtqRkmRF36FoJTrywK9nnFmnPwnSMyxaOCU7GDjN9RTMJIxx9De8ErM+vpPhMccr/6fo5WciyQLnMQ==", + "license": "MIT" + }, + "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==", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.16", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz", + "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==", "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], "license": "MIT", "bin": { - "jsesc": "bin/jsesc" + "nanoid": "bin/nanoid.cjs" }, "engines": { - "node": ">=6" + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, - "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==", + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, - "node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "node_modules/node-abi": { + "version": "4.33.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-4.33.0.tgz", + "integrity": "sha512-vLBWCKb+7LWsX+TbfzWOkw0W81m377tyx3hOweBTjO43CXZnRGS1/JPWs20fr0PgZyDXk6ROYrylsEycK8raDA==", "dev": true, - "license": "MIT" + "license": "MIT", + "dependencies": { + "semver": "^7.6.3" + }, + "engines": { + "node": ">=22.12.0" + } }, - "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==", + "node_modules/node-abi/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", - "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" + "semver": "bin/semver.js" }, "engines": { - "node": ">=6" + "node": ">=10" } }, - "node_modules/jsonfile": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", - "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "node_modules/node-addon-api": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-1.7.2.tgz", + "integrity": "sha512-ibPK3iA+vaY1eEjESkQkM0BbCqFOaZMiXRTtdB0u7b4djtY6JnsjvPdUHVMg6xQt3B8fpTTWHI9A+ADjM9frzg==", "license": "MIT", - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } + "optional": true }, - "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==", + "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": { - "json-buffer": "3.0.1" + "semver": "^7.3.5" } }, - "node_modules/kleur": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", - "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "node_modules/node-api-version/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": "MIT", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, "engines": { - "node": ">=6" + "node": ">=10" } }, - "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==", - "license": "MIT" - }, - "node_modules/locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", "license": "MIT", "dependencies": { - "p-locate": "^4.1.0" + "whatwg-url": "^5.0.0" }, "engines": { - "node": ">=8" + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } } }, - "node_modules/lodash": { - "version": "4.17.23", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", - "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", - "dev": true, - "license": "MIT" - }, - "node_modules/lodash.escaperegexp": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz", - "integrity": "sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw==", - "license": "MIT" - }, - "node_modules/lodash.isequal": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", - "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==", - "license": "MIT" - }, - "node_modules/log-symbols": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", - "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "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": { - "chalk": "^4.1.0", - "is-unicode-supported": "^0.1.0" + "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" }, - "engines": { - "node": ">=10" + "bin": { + "node-gyp": "bin/node-gyp.js" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "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/lowercase-keys": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", - "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==", + "node_modules/node-gyp/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": "MIT", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, "engines": { - "node": ">=8" + "node": ">=10" } }, - "node_modules/lru-cache": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "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": { - "yallist": "^3.0.2" + "isexe": "^4.0.0" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" } }, - "node_modules/lru-cache/node_modules/yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "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": "ISC" - }, - "node_modules/lucide-react": { - "version": "1.17.0", - "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.17.0.tgz", - "integrity": "sha512-9FA9evdox/JQL5PT57fdA1x/yg8T7knJ98+zjTL3UfKza6pflQUUh3XtaQIHKvnsJw1lmsEyHVlt5jchYxOQ5w==", - "license": "ISC", - "peerDependencies": { - "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" - } + "license": "MIT" }, - "node_modules/magic-string": { - "version": "0.30.21", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", - "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "node_modules/node-releases": { + "version": "2.0.51", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.51.tgz", + "integrity": "sha512-wRNIrw4DmVLKQlbgOMdkMx27Wrpzes2hh5Jtbi2bjPd+4wJstWIqP5A+lscnqbm0xxmT5Bpg8Lec5ItEBwx6BQ==", "dev": true, "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.5" + "engines": { + "node": ">=18" } }, - "node_modules/make-fetch-happen": { - "version": "14.0.3", - "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-14.0.3.tgz", - "integrity": "sha512-QMjGbFTP0blj97EeidG5hk/QhKQ3T4ICckQGLgz38QF7Vgbk6e6FTARN8KhKxyBbWn8R0HU+bnw8aSoFPD4qtQ==", + "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": { - "@npmcli/agent": "^3.0.0", - "cacache": "^19.0.1", - "http-cache-semantics": "^4.1.1", - "minipass": "^7.0.2", - "minipass-fetch": "^4.0.0", - "minipass-flush": "^1.0.5", - "minipass-pipeline": "^1.2.4", - "negotiator": "^1.0.0", - "proc-log": "^5.0.0", - "promise-retry": "^2.0.1", - "ssri": "^12.0.0" + "abbrev": "^4.0.0" + }, + "bin": { + "nopt": "bin/nopt.js" }, "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^20.17.0 || >=22.9.0" } }, - "node_modules/matcher": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz", - "integrity": "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==", + "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", - "optional": true, - "dependencies": { - "escape-string-regexp": "^4.0.0" - }, "engines": { "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "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==", + "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/mime": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", - "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "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": "MIT", - "bin": { - "mime": "cli.js" - }, - "engines": { - "node": ">=4.0.0" + "license": "ISC", + "dependencies": { + "wrappy": "1" } }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "node_modules/onetime": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", + "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", "dev": true, "license": "MIT", + "dependencies": { + "mimic-function": "^5.0.0" + }, "engines": { - "node": ">= 0.6" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { - "mime-db": "1.52.0" + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" }, "engines": { - "node": ">= 0.6" + "node": ">= 0.8.0" } }, - "node_modules/mimic-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", - "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "node_modules/ora": { + "version": "9.4.1", + "resolved": "https://registry.npmjs.org/ora/-/ora-9.4.1.tgz", + "integrity": "sha512-6VlU9MLXbjVQD04AZCMX28hVtA5bUoadvUqO76MUCVA0ilwJbMiHsITRPfyVm6p/BC0Av/BXMujx39WCe1LEqw==", "dev": true, "license": "MIT", + "dependencies": { + "chalk": "^5.6.2", + "cli-cursor": "^5.0.0", + "cli-spinners": "^3.2.0", + "is-interactive": "^2.0.0", + "is-unicode-supported": "^2.1.0", + "log-symbols": "^7.0.1", + "stdin-discarder": "^0.3.2", + "string-width": "^8.1.0" + }, "engines": { - "node": ">=6" + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "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==", + "node_modules/ora/node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", "dev": true, "license": "MIT", "engines": { - "node": ">=10" + "node": "^12.17.0 || ^14.13 || >=16.0.0" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/minimatch": { - "version": "10.2.0", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.0.tgz", - "integrity": "sha512-ugkC31VaVg9cF0DFVoADH12k6061zNZkZON+aX8AWsR9GhPcErkcMBceb6znR8wLERM2AkkOxy2nWRLpT9Jq5w==", + "node_modules/oxc-parser": { + "version": "0.138.0", + "resolved": "https://registry.npmjs.org/oxc-parser/-/oxc-parser-0.138.0.tgz", + "integrity": "sha512-c25lvfpZ2+WY1yk6NkP0X0RTQg0ZxgSVaZHDa7lt6fEe1jwZjPWkRWvTyZ1xyaM7roVJMdtRCfbhUj/d4ims3Q==", "dev": true, - "license": "BlueOak-1.0.0", + "license": "MIT", "dependencies": { - "brace-expansion": "^5.0.2" + "@oxc-project/types": "^0.138.0" }, "engines": { - "node": "20 || >=22" + "node": "^20.19.0 || >=22.12.0" }, "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==", + "url": "https://github.com/sponsors/Boshen" + }, + "optionalDependencies": { + "@oxc-parser/binding-android-arm-eabi": "0.138.0", + "@oxc-parser/binding-android-arm64": "0.138.0", + "@oxc-parser/binding-darwin-arm64": "0.138.0", + "@oxc-parser/binding-darwin-x64": "0.138.0", + "@oxc-parser/binding-freebsd-x64": "0.138.0", + "@oxc-parser/binding-linux-arm-gnueabihf": "0.138.0", + "@oxc-parser/binding-linux-arm-musleabihf": "0.138.0", + "@oxc-parser/binding-linux-arm64-gnu": "0.138.0", + "@oxc-parser/binding-linux-arm64-musl": "0.138.0", + "@oxc-parser/binding-linux-ppc64-gnu": "0.138.0", + "@oxc-parser/binding-linux-riscv64-gnu": "0.138.0", + "@oxc-parser/binding-linux-riscv64-musl": "0.138.0", + "@oxc-parser/binding-linux-s390x-gnu": "0.138.0", + "@oxc-parser/binding-linux-x64-gnu": "0.138.0", + "@oxc-parser/binding-linux-x64-musl": "0.138.0", + "@oxc-parser/binding-openharmony-arm64": "0.138.0", + "@oxc-parser/binding-wasm32-wasi": "0.138.0", + "@oxc-parser/binding-win32-arm64-msvc": "0.138.0", + "@oxc-parser/binding-win32-ia32-msvc": "0.138.0", + "@oxc-parser/binding-win32-x64-msvc": "0.138.0" + } + }, + "node_modules/oxc-resolver": { + "version": "11.24.2", + "resolved": "https://registry.npmjs.org/oxc-resolver/-/oxc-resolver-11.24.2.tgz", + "integrity": "sha512-FY91FiDBj7ls5MsFS9jN3tjz2o0/zsdSsymlakySaBwVJZorHhkWyICLZMKxlu1R9vYo+sd3z1jwb4J8x7bNDw==", "dev": true, "license": "MIT", "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/sponsors/Boshen" + }, + "optionalDependencies": { + "@oxc-resolver/binding-android-arm-eabi": "11.24.2", + "@oxc-resolver/binding-android-arm64": "11.24.2", + "@oxc-resolver/binding-darwin-arm64": "11.24.2", + "@oxc-resolver/binding-darwin-x64": "11.24.2", + "@oxc-resolver/binding-freebsd-x64": "11.24.2", + "@oxc-resolver/binding-linux-arm-gnueabihf": "11.24.2", + "@oxc-resolver/binding-linux-arm-musleabihf": "11.24.2", + "@oxc-resolver/binding-linux-arm64-gnu": "11.24.2", + "@oxc-resolver/binding-linux-arm64-musl": "11.24.2", + "@oxc-resolver/binding-linux-ppc64-gnu": "11.24.2", + "@oxc-resolver/binding-linux-riscv64-gnu": "11.24.2", + "@oxc-resolver/binding-linux-riscv64-musl": "11.24.2", + "@oxc-resolver/binding-linux-s390x-gnu": "11.24.2", + "@oxc-resolver/binding-linux-x64-gnu": "11.24.2", + "@oxc-resolver/binding-linux-x64-musl": "11.24.2", + "@oxc-resolver/binding-openharmony-arm64": "11.24.2", + "@oxc-resolver/binding-wasm32-wasi": "11.24.2", + "@oxc-resolver/binding-win32-arm64-msvc": "11.24.2", + "@oxc-resolver/binding-win32-x64-msvc": "11.24.2" } }, - "node_modules/minipass": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", - "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "node_modules/oxlint": { + "version": "1.74.0", + "resolved": "https://registry.npmjs.org/oxlint/-/oxlint-1.74.0.tgz", + "integrity": "sha512-odGl2s2x5IOJoj3A0v1k0PGBXVFBZeZ2+AK/+K2MJur7Ghi3bkyX5NuLUWHKqa4js1wjep3hJeuTQJOlr+4+dA==", "dev": true, - "license": "ISC", + "license": "MIT", + "bin": { + "oxlint": "bin/oxlint" + }, "engines": { - "node": ">=16 || 14 >=14.17" + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/sponsors/Boshen" + }, + "optionalDependencies": { + "@oxlint/binding-android-arm-eabi": "1.74.0", + "@oxlint/binding-android-arm64": "1.74.0", + "@oxlint/binding-darwin-arm64": "1.74.0", + "@oxlint/binding-darwin-x64": "1.74.0", + "@oxlint/binding-freebsd-x64": "1.74.0", + "@oxlint/binding-linux-arm-gnueabihf": "1.74.0", + "@oxlint/binding-linux-arm-musleabihf": "1.74.0", + "@oxlint/binding-linux-arm64-gnu": "1.74.0", + "@oxlint/binding-linux-arm64-musl": "1.74.0", + "@oxlint/binding-linux-ppc64-gnu": "1.74.0", + "@oxlint/binding-linux-riscv64-gnu": "1.74.0", + "@oxlint/binding-linux-riscv64-musl": "1.74.0", + "@oxlint/binding-linux-s390x-gnu": "1.74.0", + "@oxlint/binding-linux-x64-gnu": "1.74.0", + "@oxlint/binding-linux-x64-musl": "1.74.0", + "@oxlint/binding-openharmony-arm64": "1.74.0", + "@oxlint/binding-win32-arm64-msvc": "1.74.0", + "@oxlint/binding-win32-ia32-msvc": "1.74.0", + "@oxlint/binding-win32-x64-msvc": "1.74.0" + }, + "peerDependencies": { + "oxlint-tsgolint": ">=0.24.0", + "vite-plus": "*" + }, + "peerDependenciesMeta": { + "oxlint-tsgolint": { + "optional": true + }, + "vite-plus": { + "optional": true + } } }, - "node_modules/minipass-collect": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-2.0.1.tgz", - "integrity": "sha512-D7V8PO9oaz7PWGLbCACuI1qEOsq7UKfLotx/C0Aet43fCUB/wfQ7DYeq2oR/svFJGYDHPr38SHATeaj/ZoKHKw==", + "node_modules/oxlint-plugin-react-doctor": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/oxlint-plugin-react-doctor/-/oxlint-plugin-react-doctor-0.8.1.tgz", + "integrity": "sha512-ETjgoKrY0EYpK51BsbvgpWySkWaLgJ6z7yB/BfOosi+TUY4+GiDmhWfjJCeul+tpDJEFKR5MnpENjlUrmtZ5Dw==", "dev": true, - "license": "ISC", + "license": "SEE LICENSE IN LICENSE", "dependencies": { - "minipass": "^7.0.3" + "@typescript-eslint/types": "^8.59.3", + "eslint-scope": "^9.1.2", + "eslint-visitor-keys": "^5.0.1", + "oxc-parser": "^0.138.0" }, "engines": { - "node": ">=16 || 14 >=14.17" + "node": "^20.19.0 || >=22.13.0" } }, - "node_modules/minipass-fetch": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-4.0.1.tgz", - "integrity": "sha512-j7U11C5HXigVuutxebFadoYBbd7VSdZWggSe64NVdvWNBqGAiXPL2QVCehjmw7lY1oF9gOllYbORh+hiNgfPgQ==", + "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", - "dependencies": { - "minipass": "^7.0.3", - "minipass-sized": "^1.0.3", - "minizlib": "^3.0.1" - }, "engines": { - "node": "^18.17.0 || >=20.5.0" - }, - "optionalDependencies": { - "encoding": "^0.1.13" + "node": ">=8" } }, - "node_modules/minipass-flush": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz", - "integrity": "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==", + "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": "ISC", + "license": "MIT", "dependencies": { - "minipass": "^3.0.0" + "yocto-queue": "^0.1.0" }, "engines": { - "node": ">= 8" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/minipass-flush/node_modules/minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", "dev": true, - "license": "ISC", + "license": "MIT", + "peer": true, "dependencies": { - "yallist": "^4.0.0" + "p-limit": "^3.0.2" }, "engines": { - "node": ">=8" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/minipass-flush/node_modules/minipass/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/minipass-pipeline": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", - "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==", - "dev": true, - "license": "ISC", - "dependencies": { - "minipass": "^3.0.0" - }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "license": "MIT", "engines": { - "node": ">=8" + "node": ">=6" } }, - "node_modules/minipass-pipeline/node_modules/minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "node_modules/package-manager-detector": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-1.7.0.tgz", + "integrity": "sha512-xg1eHpwYL/D/HEdWw2goFZP6vV0FH7W+PZ5rFkGjdIDLtxq7EkzBUeT3m+lndYCt8wKbmofUu1MUdMCXkCk9ZQ==", "dev": true, - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, + "license": "MIT" + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "license": "MIT", "engines": { "node": ">=8" } }, - "node_modules/minipass-pipeline/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "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": "ISC" + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } }, - "node_modules/minipass-sized": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-1.0.3.tgz", - "integrity": "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==", + "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": "ISC", - "dependencies": { - "minipass": "^3.0.0" - }, + "license": "MIT", "engines": { "node": ">=8" } }, - "node_modules/minipass-sized/node_modules/minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "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": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, + "license": "MIT", "engines": { - "node": ">=8" + "node": ">=12", + "npm": ">=6" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/jet2jet" } }, - "node_modules/minipass-sized/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "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/minizlib": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", - "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", + "node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", "dev": true, "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pkijs": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/pkijs/-/pkijs-3.4.0.tgz", + "integrity": "sha512-emEcLuomt2j03vxD54giVB4SxTjnsqkU692xZOZXHDVoYyypEm+b3jpiTcc+Cf+myooc+/Ly0z01jqeNHVgJGw==", + "dev": true, + "license": "BSD-3-Clause", "dependencies": { - "minipass": "^7.1.2" + "@noble/hashes": "1.4.0", + "asn1js": "^3.0.6", + "bytestreamjs": "^2.0.1", + "pvtsutils": "^1.3.6", + "pvutils": "^1.1.3", + "tslib": "^2.8.1" }, "engines": { - "node": ">= 18" + "node": ">=16.0.0" } }, - "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==", + "node_modules/pkijs/node_modules/@noble/hashes": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.4.0.tgz", + "integrity": "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==", "dev": true, "license": "MIT", - "peer": true, - "dependencies": { - "minimist": "^1.2.6" + "engines": { + "node": ">= 16" }, - "bin": { - "mkdirp": "bin/cmd.js" + "funding": { + "url": "https://paulmillr.com/funding/" } }, - "node_modules/motion": { - "version": "12.42.0", - "resolved": "https://registry.npmjs.org/motion/-/motion-12.42.0.tgz", - "integrity": "sha512-Qhwvu9sVl5/URSq5CNzwMCpSKK8Uhnrwb6VO977kZyj/wOCS7mWebJUnBoHx5cZU1Zv8a9BD5CSICWKAlrLJgA==", + "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": { - "framer-motion": "^12.42.0", - "tslib": "^2.4.0" - }, - "peerDependencies": { - "@emotion/is-prop-valid": "*", - "react": "^18.0.0 || ^19.0.0", - "react-dom": "^18.0.0 || ^19.0.0" + "@xmldom/xmldom": "^0.8.8", + "base64-js": "^1.5.1", + "xmlbuilder": "^15.1.1" }, - "peerDependenciesMeta": { - "@emotion/is-prop-valid": { - "optional": true - }, - "react": { - "optional": true - }, - "react-dom": { - "optional": true - } + "engines": { + "node": ">=10.4.0" } }, - "node_modules/motion-dom": { - "version": "12.42.0", - "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.42.0.tgz", - "integrity": "sha512-M63h4n8R+quJdNhBwuLlgxM+OLYa9+I/T2pzDRboB9fLXRdbou+Gw7Zury+SkpaCyACP1JHSjHgZ1EgTkBr30w==", + "node_modules/pngjs": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz", + "integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==", "license": "MIT", - "dependencies": { - "motion-utils": "^12.39.0" + "engines": { + "node": ">=10.13.0" } }, - "node_modules/motion-utils": { - "version": "12.39.0", - "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.39.0.tgz", - "integrity": "sha512-8nadJAJjTtqRkmRF36FoJTrywK9nnFmnPwnSMyxaOCU7GDjN9RTMJIxx9De8ErM+vpPhMccr/6fo5WciyQLnMQ==", - "license": "MIT" - }, - "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==", - "license": "MIT" - }, - "node_modules/nanoid": { - "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "node_modules/postcss": { + "version": "8.5.20", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.20.tgz", + "integrity": "sha512-lW616l85ucIQL+FocMmL7pQFPqBmwejrCMg+iPxyImlrANNJG9NHq/RkyCZopDhd8C3LA03PHRJDjkbGu8vvug==", "dev": true, "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, { "type": "github", "url": "https://github.com/sponsors/ai" } ], "license": "MIT", - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, - "node_modules/negotiator": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", - "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/node-abi": { - "version": "4.28.0", - "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-4.28.0.tgz", - "integrity": "sha512-Qfp5XZL1cJDOabOT8H5gnqMTmM4NjvYzHp4I/Kt/Sl76OVkOBBHRFlPspGV0hYvMoqQsypFjT/Yp7Km0beXW9g==", - "dev": true, - "license": "MIT", "dependencies": { - "semver": "^7.6.3" + "nanoid": "^3.3.16", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" }, "engines": { - "node": ">=22.12.0" + "node": "^10 || ^12 || >=14" } }, - "node_modules/node-addon-api": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-1.7.2.tgz", - "integrity": "sha512-ibPK3iA+vaY1eEjESkQkM0BbCqFOaZMiXRTtdB0u7b4djtY6JnsjvPdUHVMg6xQt3B8fpTTWHI9A+ADjM9frzg==", - "license": "MIT", - "optional": true - }, - "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", + "node_modules/posthog-js": { + "version": "1.406.2", + "resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.406.2.tgz", + "integrity": "sha512-HNJO6Llro79s3x/ScxGY7i+Tf/o+ctJkOc79vrnS6R1I3TzucfzfwgAip1JVnQHnkBPV4JE8hI6nUXMJdU113Q==", + "license": "(Apache-2.0 AND MIT)", "dependencies": { - "semver": "^7.3.5" + "@posthog/browser-common": "^0.2.0", + "@posthog/core": "^1.44.0", + "@posthog/types": "^1.397.1", + "core-js": "^3.49.0", + "dompurify": "^3.3.2", + "fflate": "^0.4.8", + "preact": "^10.29.3", + "query-selector-shadow-dom": "^1.0.1", + "web-vitals": "^5.3.0" } }, - "node_modules/node-fetch": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", - "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "node_modules/posthog-js/node_modules/fflate": { + "version": "0.4.9", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.4.9.tgz", + "integrity": "sha512-zdxgIEddhfsyCaWpJ2SdXEP8ZMrKJ6+5jl4OupODcywU0IhRk6gdXuVGcPICyfx2H97hVK7xmJtRLPjkxAX8Vw==", + "license": "MIT" + }, + "node_modules/posthog-node": { + "version": "5.46.0", + "resolved": "https://registry.npmjs.org/posthog-node/-/posthog-node-5.46.0.tgz", + "integrity": "sha512-Uzkth327Qxho9X55UygGUjVKCF9oaox90HQpa0o9YNjwLjbQmXttgHChzAtjAcsMw/ZKr3NnHC3xcAaS5dXwEQ==", "license": "MIT", "dependencies": { - "whatwg-url": "^5.0.0" + "@posthog/core": "^1.44.0" }, "engines": { - "node": "4.x || >=6.0.0" + "node": "^20.20.0 || >=22.22.0" }, "peerDependencies": { - "encoding": "^0.1.0" + "rxjs": "^7.0.0" }, "peerDependenciesMeta": { - "encoding": { + "rxjs": { "optional": true } } }, - "node_modules/node-gyp": { - "version": "11.5.0", - "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-11.5.0.tgz", - "integrity": "sha512-ra7Kvlhxn5V9Slyus0ygMa2h+UqExPqUIkfk7Pc8QTLT956JLSy51uWFwHtIYy0vI8cB4BDhc/S03+880My/LQ==", + "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": { - "env-paths": "^2.2.0", - "exponential-backoff": "^3.1.1", - "graceful-fs": "^4.2.6", - "make-fetch-happen": "^14.0.3", - "nopt": "^8.0.0", - "proc-log": "^5.0.0", - "semver": "^7.3.5", - "tar": "^7.4.3", - "tinyglobby": "^0.2.12", - "which": "^5.0.0" + "commander": "^9.4.0" }, "bin": { - "node-gyp": "bin/node-gyp.js" + "postject": "dist/cli.js" }, "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": ">=14.0.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==", + "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": ">=6" + "node": "^12.20.0 || >=14" } }, - "node_modules/node-gyp/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" + "node_modules/preact": { + "version": "10.29.7", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.29.7.tgz", + "integrity": "sha512-DCHYrK/B10yUD3ZjLfhZ3WIE/9Vf9VFUODcRE2dRomTYDpJk6z6L9wecSfhfE6M9ZTHUdyQkoC46arIDhEV84Q==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/preact" }, - "bin": { - "node-which": "bin/which.js" + "peerDependencies": { + "preact-render-to-string": ">=5" }, + "peerDependenciesMeta": { + "preact-render-to-string": { + "optional": true + } + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "peer": true, "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": ">= 0.8.0" } }, - "node_modules/node-gyp/node_modules/which/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==", + "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": "BlueOak-1.0.0", + "license": "ISC", "engines": { - "node": ">=18" + "node": "^20.17.0 || >=22.9.0" } }, - "node_modules/node-releases": { - "version": "2.0.27", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", - "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "node_modules/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/nopt": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-8.1.0.tgz", - "integrity": "sha512-ieGu42u/Qsa4TFktmaKEwM6MQH0pOWnaB3htzh0JRtx84+Mebc0cbZYN5bC+6WTZ4+77xrL9Pn5m7CV6VIkV7A==", + "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": "ISC", - "dependencies": { - "abbrev": "^3.0.0" - }, - "bin": { - "nopt": "bin/nopt.js" - }, + "license": "MIT", "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": ">=0.4.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==", + "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" - }, - "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==", + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", "dev": true, "license": "MIT", - "optional": true, + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, "engines": { - "node": ">= 0.4" + "node": ">= 6" } }, - "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==", + "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": "ISC", + "license": "MIT", "dependencies": { - "wrappy": "1" + "graceful-fs": "^4.2.4", + "retry": "^0.12.0", + "signal-exit": "^3.0.2" } }, - "node_modules/onetime": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", - "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "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": { - "mimic-fn": "^2.1.0" - }, + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "peer": true, "engines": { "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/ora": { - "version": "5.4.1", - "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", - "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", + "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": { - "bl": "^4.1.0", - "chalk": "^4.1.0", - "cli-cursor": "^3.1.0", - "cli-spinners": "^2.5.0", - "is-interactive": "^1.0.0", - "is-unicode-supported": "^0.1.0", - "log-symbols": "^4.1.0", - "strip-ansi": "^6.0.0", - "wcwidth": "^1.0.1" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "tslib": "^2.8.1" } }, - "node_modules/oxlint": { - "version": "1.67.0", - "resolved": "https://registry.npmjs.org/oxlint/-/oxlint-1.67.0.tgz", - "integrity": "sha512-blwwaHPdoH8piQ5/z0KHeoHFR7FZgl12WluKJfu4qFLPkZl6mK04PkLE45Fw1NxfBRSlh40Gu7MkxHUw++ociQ==", + "node_modules/pvutils": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/pvutils/-/pvutils-1.1.5.tgz", + "integrity": "sha512-KTqnxsgGiQ6ZAzZCVlJH5eOjSnvlyEgx1m8bkRJfOhmGRqfo5KLvmAlACQkrjEtOQ4B7wF9TdSLIs9O90MX9xA==", "dev": true, "license": "MIT", - "bin": { - "oxlint": "bin/oxlint" - }, "engines": { - "node": "^20.19.0 || >=22.12.0" - }, - "funding": { - "url": "https://github.com/sponsors/Boshen" - }, - "optionalDependencies": { - "@oxlint/binding-android-arm-eabi": "1.67.0", - "@oxlint/binding-android-arm64": "1.67.0", - "@oxlint/binding-darwin-arm64": "1.67.0", - "@oxlint/binding-darwin-x64": "1.67.0", - "@oxlint/binding-freebsd-x64": "1.67.0", - "@oxlint/binding-linux-arm-gnueabihf": "1.67.0", - "@oxlint/binding-linux-arm-musleabihf": "1.67.0", - "@oxlint/binding-linux-arm64-gnu": "1.67.0", - "@oxlint/binding-linux-arm64-musl": "1.67.0", - "@oxlint/binding-linux-ppc64-gnu": "1.67.0", - "@oxlint/binding-linux-riscv64-gnu": "1.67.0", - "@oxlint/binding-linux-riscv64-musl": "1.67.0", - "@oxlint/binding-linux-s390x-gnu": "1.67.0", - "@oxlint/binding-linux-x64-gnu": "1.67.0", - "@oxlint/binding-linux-x64-musl": "1.67.0", - "@oxlint/binding-openharmony-arm64": "1.67.0", - "@oxlint/binding-win32-arm64-msvc": "1.67.0", - "@oxlint/binding-win32-ia32-msvc": "1.67.0", - "@oxlint/binding-win32-x64-msvc": "1.67.0" + "node": ">=16.0.0" + } + }, + "node_modules/qrcode": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.4.tgz", + "integrity": "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==", + "license": "MIT", + "dependencies": { + "dijkstrajs": "^1.0.1", + "pngjs": "^5.0.0", + "yargs": "^15.3.1" }, - "peerDependencies": { - "oxlint-tsgolint": ">=0.22.1", - "vite-plus": "*" + "bin": { + "qrcode": "bin/qrcode" }, - "peerDependenciesMeta": { - "oxlint-tsgolint": { - "optional": true - }, - "vite-plus": { - "optional": true - } + "engines": { + "node": ">=10.13.0" } }, - "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, + "node_modules/qrcode/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==", "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, + "node_modules/qrcode/node_modules/cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" + } + }, + "node_modules/qrcode/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", "license": "MIT", "dependencies": { - "yocto-queue": "^0.1.0" + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" }, "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=8" } }, - "node_modules/p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "node_modules/qrcode/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", "license": "MIT", "dependencies": { - "p-limit": "^2.2.0" + "p-locate": "^4.1.0" }, "engines": { "node": ">=8" } }, - "node_modules/p-locate/node_modules/p-limit": { + "node_modules/qrcode/node_modules/p-limit": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", @@ -5849,420 +8095,594 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/p-map": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/p-map/-/p-map-7.0.4.tgz", - "integrity": "sha512-tkAQEw8ysMzmkhgw8k+1U/iPhWNhykKnSk4Rd5zLoPJCuJaGRPo6YposrZgaxHKzDHdDWWZvE/Sk7hsL2X/CpQ==", - "dev": true, + "node_modules/qrcode/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, "engines": { - "node": ">=18" + "node": ">=8" + } + }, + "node_modules/qrcode/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==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "engines": { + "node": ">=8" } }, - "node_modules/p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "node_modules/qrcode/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==", "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, "engines": { - "node": ">=6" + "node": ">=8" } }, - "node_modules/package-json-from-dist": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", - "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", - "dev": true, - "license": "BlueOak-1.0.0" + "node_modules/qrcode/node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } }, - "node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "node_modules/qrcode/node_modules/y18n": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", + "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", + "license": "ISC" + }, + "node_modules/qrcode/node_modules/yargs": { + "version": "15.4.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", + "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", "license": "MIT", + "dependencies": { + "cliui": "^6.0.0", + "decamelize": "^1.2.0", + "find-up": "^4.1.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^4.2.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^18.1.2" + }, "engines": { "node": ">=8" } }, - "node_modules/path-is-absolute": { + "node_modules/qrcode/node_modules/yargs-parser": { + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "license": "ISC", + "dependencies": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/query-selector-shadow-dom": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "resolved": "https://registry.npmjs.org/query-selector-shadow-dom/-/query-selector-shadow-dom-1.0.1.tgz", + "integrity": "sha512-lT5yCqEBgfoMYpf3F2xQRK7zEr1rhIIZuceDK6+xRkJQ4NMbHTwXqk4NkwDwQMNqXgG9r9fyHnzwNVs6zV5KRw==", + "license": "MIT" + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "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/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": ">=0.10.0" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "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, + "node_modules/react": { + "version": "19.2.7", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.7.tgz", + "integrity": "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==", "license": "MIT", "engines": { - "node": ">=8" + "node": ">=0.10.0" } }, - "node_modules/path-scurry": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", - "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "node_modules/react-doctor": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/react-doctor/-/react-doctor-0.8.1.tgz", + "integrity": "sha512-lP/MaS7dDMeVFpWBjh8qYp6NYcb/1TmsAwSixqkqOI50fea9kfh89fyn+UUAC3vb53FGIqwvDPZIjlJR6BgGAQ==", "dev": true, - "license": "BlueOak-1.0.0", + "license": "SEE LICENSE IN LICENSE", "dependencies": { - "lru-cache": "^10.2.0", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + "@babel/code-frame": "^7.29.0", + "@sentry/node": "^10.54.0", + "agent-install": "0.0.5", + "conf": "^15.1.0", + "confbox": "^0.2.4", + "deslop-js": "0.8.1", + "eslint-plugin-react-hooks": "^7.1.1", + "jiti": "^2.7.0", + "magicast": "^0.5.3", + "oxlint": ">=1.66.0 <1.67.0", + "oxlint-plugin-react-doctor": "0.8.1", + "prompts": "^2.4.2", + "typescript": ">=5.0.4 <6", + "vscode-languageserver": "^9.0.1", + "vscode-languageserver-textdocument": "^1.0.12", + "vscode-uri": "^3.1.0", + "yaml": "^2.9.0" }, - "engines": { - "node": ">=16 || 14 >=14.18" + "bin": { + "react-doctor": "bin/react-doctor.js" }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "engines": { + "node": "^20.19.0 || >=22.13.0" } }, - "node_modules/path-scurry/node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "dev": true, - "license": "ISC" - }, - "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==", + "node_modules/react-doctor/node_modules/@oxlint/binding-android-arm-eabi": { + "version": "1.66.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm-eabi/-/binding-android-arm-eabi-1.66.0.tgz", + "integrity": "sha512-f7kq8N51T4phpzqfBpA2qaVTI/KrkCmNwaj3t/97I/WLTDI+UhlP5GL9eER+zVxBhtlx5rKXWByJU1/zDAvyaw==", + "cpu": [ + "arm" + ], "dev": true, "license": "MIT", + "optional": true, + "os": [ + "android" + ], "engines": { - "node": ">=12", - "npm": ">=6" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/jet2jet" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/pend": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", - "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", - "dev": true, - "license": "MIT" - }, - "node_modules/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true, - "license": "ISC" - }, - "node_modules/picomatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", - "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "node_modules/react-doctor/node_modules/@oxlint/binding-android-arm64": { + "version": "1.66.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm64/-/binding-android-arm64-1.66.0.tgz", + "integrity": "sha512-xu6QO71tdDS9mjmLZ3AqhtaVHBvdmsOKkYnReNNDgh+XiwnsipeQOIxbiYOOO0iAXycJ+GK0wdMSZP/2j/AmSg==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", + "optional": true, + "os": [ + "android" + ], "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/plist": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/plist/-/plist-3.1.0.tgz", - "integrity": "sha512-uysumyrvkUX0rX/dEVqt8gC3sTBzd4zoWfLeS29nb53imdaXVvLINYXTI2GNqzaMuvacNx4uJQ8+b3zXR0pkgQ==", + "node_modules/react-doctor/node_modules/@oxlint/binding-darwin-arm64": { + "version": "1.66.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-arm64/-/binding-darwin-arm64-1.66.0.tgz", + "integrity": "sha512-HZ24VimSOC7mxuEA99e0H2FS0C1yO3+iW13jPRAk+e2njsUs3QeAXsafCDyaIrV/MirdOVez+etQNQsJE43zNQ==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@xmldom/xmldom": "^0.8.8", - "base64-js": "^1.5.1", - "xmlbuilder": "^15.1.1" - }, + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": ">=10.4.0" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/pngjs": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz", - "integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==", + "node_modules/react-doctor/node_modules/@oxlint/binding-darwin-x64": { + "version": "1.66.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-x64/-/binding-darwin-x64-1.66.0.tgz", + "integrity": "sha512-awhj8ZvJrrRSnXj7V++rpZvTmnl99L6mi0B7gg7Cp7BN6cKpzuI481bHNLvXGA9GB1/oEgA3ponuyoAc6Md12A==", + "cpu": [ + "x64" + ], + "dev": true, "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": ">=10.13.0" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/postcss": { - "version": "8.5.6", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", - "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } + "node_modules/react-doctor/node_modules/@oxlint/binding-freebsd-x64": { + "version": "1.66.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-freebsd-x64/-/binding-freebsd-x64-1.66.0.tgz", + "integrity": "sha512-KQF0oVV21/FjIqkRuL8Q1vh8ECsE5+ocdH5tcqTQ4ZnYuDVoYibQUNfqBjQaUsP6UIIda5Y75Wpm5p4RgQWiWw==", + "cpu": [ + "x64" ], + "dev": true, "license": "MIT", - "dependencies": { - "nanoid": "^3.3.11", - "picocolors": "^1.1.1", - "source-map-js": "^1.2.1" - }, + "optional": true, + "os": [ + "freebsd" + ], "engines": { - "node": "^10 || ^12 || >=14" + "node": "^20.19.0 || >=22.12.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==", + "node_modules/react-doctor/node_modules/@oxlint/binding-linux-arm-gnueabihf": { + "version": "1.66.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.66.0.tgz", + "integrity": "sha512-9u1rgwZSEXWb30vbFZzQ78HVXBo0WCKNwJ3a2InRUTNMRng+PUDIoSFmA+m4HdUfBaIqftShq8J8qHc+eE/Vig==", + "cpu": [ + "arm" + ], "dev": true, "license": "MIT", "optional": true, - "peer": true, - "dependencies": { - "commander": "^9.4.0" - }, - "bin": { - "postject": "dist/cli.js" - }, + "os": [ + "linux" + ], "engines": { - "node": ">=14.0.0" + "node": "^20.19.0 || >=22.12.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==", + "node_modules/react-doctor/node_modules/@oxlint/binding-linux-arm-musleabihf": { + "version": "1.66.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-1.66.0.tgz", + "integrity": "sha512-Ynot2HR1bHxUaNWoC280MVTDfZuaWuP3XfSMRDhyuZrVjhzoaBCVFlw8h8qeZjWKVUBhPWFIxB7AQTlK8Z2WWg==", + "cpu": [ + "arm" + ], "dev": true, "license": "MIT", "optional": true, - "peer": true, + "os": [ + "linux" + ], "engines": { - "node": "^12.20.0 || >=14" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/preact": { - "version": "10.29.1", - "resolved": "https://registry.npmjs.org/preact/-/preact-10.29.1.tgz", - "integrity": "sha512-gQCLc/vWroE8lIpleXtdJhTFDogTdZG9AjMUpVkDf2iTCNwYNWA+u16dL41TqUDJO4gm2IgrcMv3uTpjd4Pwmg==", + "node_modules/react-doctor/node_modules/@oxlint/binding-linux-arm64-gnu": { + "version": "1.66.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.66.0.tgz", + "integrity": "sha512-xCbgzciGgo+A4aQZEknsNrNiIwY7sU5SfRuMmRjPIvZAgdF34cIHiKvwOsS5XRLjlTVSFwitmq6YclTtHTfU+g==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/preact" - } - }, - "node_modules/proc-log": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-5.0.0.tgz", - "integrity": "sha512-Azwzvl90HaF0aCz1JrDdXQykFakSSNPaPoiZ9fm5qJIMHioDZEi7OAdRwSm6rSoPtY3Qutnm3L7ogmg3dc+wbQ==", - "dev": true, - "license": "ISC", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/progress": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", - "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "node_modules/react-doctor/node_modules/@oxlint/binding-linux-arm64-musl": { + "version": "1.66.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.66.0.tgz", + "integrity": "sha512-hmo+ZB/lHkR1HdDmnziNpzSLmulnUSu10VEqX2Yex7OwvoBAbjJQLvy4gIBRV3AAwWnCvAxKp5Nv1GE6LU1QMg==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=0.4.0" + "node": "^20.19.0 || >=22.12.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==", + "node_modules/react-doctor/node_modules/@oxlint/binding-linux-ppc64-gnu": { + "version": "1.66.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.66.0.tgz", + "integrity": "sha512-2Invd4Uyy81mVooQC5FBtfxSNrvcX1OxbMlVQ6M2erRrNI2awFYF26YNW2yFxdVFZ4ffNOWKghtMjhnUPsXsVA==", + "cpu": [ + "ppc64" + ], "dev": true, "license": "MIT", - "dependencies": { - "err-code": "^2.0.2", - "retry": "^0.12.0" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=10" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/prompts": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", - "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "node_modules/react-doctor/node_modules/@oxlint/binding-linux-riscv64-gnu": { + "version": "1.66.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-1.66.0.tgz", + "integrity": "sha512-s0iXPDQVdgayE3RGa/N2DZF7tjgg0TwEtD1sGoDxqPDGrIXgo45H0yHknT0f9A0yteASsweYZtDyTuVlM4aSag==", + "cpu": [ + "riscv64" + ], "dev": true, "license": "MIT", - "dependencies": { - "kleur": "^3.0.3", - "sisteransi": "^1.0.5" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">= 6" + "node": "^20.19.0 || >=22.12.0" } }, - "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==", + "node_modules/react-doctor/node_modules/@oxlint/binding-linux-riscv64-musl": { + "version": "1.66.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-1.66.0.tgz", + "integrity": "sha512-OekL4XFiu7RPK0JIZi8VeHgtIXPREf42t8Cy/rKEsC+P3gcqDgNAAGiyuUOpdbG4wwbfue1q4CHcCO7spSve6w==", + "cpu": [ + "riscv64" + ], "dev": true, "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.4", - "retry": "^0.12.0", - "signal-exit": "^3.0.2" + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/pump": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz", - "integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==", + "node_modules/react-doctor/node_modules/@oxlint/binding-linux-s390x-gnu": { + "version": "1.66.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.66.0.tgz", + "integrity": "sha512-Ga1D0kj1SFslm34ThA/BdkUlyAYEnTsXyRC4pF0C5agZSwtGdHYWMTQWemUfBGp4RCG4QWXgdO+HmmmKqOtlBg==", + "cpu": [ + "s390x" + ], "dev": true, "license": "MIT", - "dependencies": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/punycode": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "node_modules/react-doctor/node_modules/@oxlint/binding-linux-x64-gnu": { + "version": "1.66.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.66.0.tgz", + "integrity": "sha512-p5jfP1wUZe/IC3qpQO84n9DRnf9g3lKRtLBlQq23ykyrDglHcVx7sWmVTlPuU6SBw8mNnPzyOn022G3XZHnlww==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=6" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/qrcode": { - "version": "1.5.4", - "resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.4.tgz", - "integrity": "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==", + "node_modules/react-doctor/node_modules/@oxlint/binding-linux-x64-musl": { + "version": "1.66.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-musl/-/binding-linux-x64-musl-1.66.0.tgz", + "integrity": "sha512-vUB/sYlYZorDL1ZD+o9mRv7zbsykrrFRtmgS6R8musZqLtrPRQn1gc1eGpuX+sfdccz42STl/AqldY6XRb2upQ==", + "cpu": [ + "x64" + ], + "dev": true, "license": "MIT", - "dependencies": { - "dijkstrajs": "^1.0.1", - "pngjs": "^5.0.0", - "yargs": "^15.3.1" - }, - "bin": { - "qrcode": "bin/qrcode" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=10.13.0" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/qrcode/node_modules/yargs": { - "version": "15.4.1", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", - "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", + "node_modules/react-doctor/node_modules/@oxlint/binding-openharmony-arm64": { + "version": "1.66.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-openharmony-arm64/-/binding-openharmony-arm64-1.66.0.tgz", + "integrity": "sha512-yde+6p/F59xRkGR9H1HfngWRif1QRJjynZK349l+UI0H6w9hL3G8/AVaTHFyTtLVQ56qtNbX2/5Dc77n1ovnOg==", + "cpu": [ + "arm64" + ], + "dev": true, "license": "MIT", - "dependencies": { - "cliui": "^6.0.0", - "decamelize": "^1.2.0", - "find-up": "^4.1.0", - "get-caller-file": "^2.0.1", - "require-directory": "^2.1.1", - "require-main-filename": "^2.0.0", - "set-blocking": "^2.0.0", - "string-width": "^4.2.0", - "which-module": "^2.0.0", - "y18n": "^4.0.0", - "yargs-parser": "^18.1.2" - }, + "optional": true, + "os": [ + "openharmony" + ], "engines": { - "node": ">=8" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/qrcode/node_modules/yargs-parser": { - "version": "18.1.3", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", - "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", - "license": "ISC", - "dependencies": { - "camelcase": "^5.0.0", - "decamelize": "^1.2.0" - }, + "node_modules/react-doctor/node_modules/@oxlint/binding-win32-arm64-msvc": { + "version": "1.66.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.66.0.tgz", + "integrity": "sha512-O9GLucgoTdmOrbBX+EjzNe7o/Ze5TFOvXcib6bzUOtBOmj6cV+zw18NgB+cGKAkDw1Pdqs8vGkfHbbsLuDtXWg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": ">=6" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/qrcode/node_modules/yargs/node_modules/cliui": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", - "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", - "license": "ISC", - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", - "wrap-ansi": "^6.2.0" + "node_modules/react-doctor/node_modules/@oxlint/binding-win32-ia32-msvc": { + "version": "1.66.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-1.66.0.tgz", + "integrity": "sha512-m3Pjwc2MfTcom4E4gOv7DyuGyt7OfGNCbmqDHd+N7EzXmP+ppHuudm2NjcA3AjV5TSeGxaguVF4SbTKHe1USYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/qrcode/node_modules/yargs/node_modules/cliui/node_modules/wrap-ansi": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", - "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "node_modules/react-doctor/node_modules/@oxlint/binding-win32-x64-msvc": { + "version": "1.66.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.66.0.tgz", + "integrity": "sha512-/DbBvw8UFBhja6PqudUjV4UtfsJr0Oa7jUjWVKB0g86lj/VwnPrkngn0sFql3c9RDA0O16dh7ozsXb6GjNAzBQ==", + "cpu": [ + "x64" + ], + "dev": true, "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": ">=8" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/qrcode/node_modules/yargs/node_modules/y18n": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", - "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", - "license": "ISC" - }, - "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==", + "node_modules/react-doctor/node_modules/oxlint": { + "version": "1.66.0", + "resolved": "https://registry.npmjs.org/oxlint/-/oxlint-1.66.0.tgz", + "integrity": "sha512-N4LLxYLd94KEBqXDMDM5f+2PUpItTjDLreXe2Gn5KhjhCK4Qp2YUXaBi8Yu325ryOgKwt22m45fpD7nPOn69Yw==", "dev": true, "license": "MIT", + "bin": { + "oxlint": "bin/oxlint" + }, "engines": { - "node": ">=10" + "node": "^20.19.0 || >=22.12.0" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/sponsors/Boshen" + }, + "optionalDependencies": { + "@oxlint/binding-android-arm-eabi": "1.66.0", + "@oxlint/binding-android-arm64": "1.66.0", + "@oxlint/binding-darwin-arm64": "1.66.0", + "@oxlint/binding-darwin-x64": "1.66.0", + "@oxlint/binding-freebsd-x64": "1.66.0", + "@oxlint/binding-linux-arm-gnueabihf": "1.66.0", + "@oxlint/binding-linux-arm-musleabihf": "1.66.0", + "@oxlint/binding-linux-arm64-gnu": "1.66.0", + "@oxlint/binding-linux-arm64-musl": "1.66.0", + "@oxlint/binding-linux-ppc64-gnu": "1.66.0", + "@oxlint/binding-linux-riscv64-gnu": "1.66.0", + "@oxlint/binding-linux-riscv64-musl": "1.66.0", + "@oxlint/binding-linux-s390x-gnu": "1.66.0", + "@oxlint/binding-linux-x64-gnu": "1.66.0", + "@oxlint/binding-linux-x64-musl": "1.66.0", + "@oxlint/binding-openharmony-arm64": "1.66.0", + "@oxlint/binding-win32-arm64-msvc": "1.66.0", + "@oxlint/binding-win32-ia32-msvc": "1.66.0", + "@oxlint/binding-win32-x64-msvc": "1.66.0" + }, + "peerDependencies": { + "oxlint-tsgolint": ">=0.22.1" + }, + "peerDependenciesMeta": { + "oxlint-tsgolint": { + "optional": true + } } }, - "node_modules/react": { - "version": "19.2.6", - "resolved": "https://registry.npmjs.org/react/-/react-19.2.6.tgz", - "integrity": "sha512-sfWGGfavi0xr8Pg0sVsyHMAOziVYKgPLNrS7ig+ivMNb3wbCBw3KxtflsGBAwD3gYQlE/AEZsTLgToRrSCjb0Q==", - "license": "MIT", + "node_modules/react-doctor/node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, "engines": { - "node": ">=0.10.0" + "node": ">=14.17" } }, "node_modules/react-dom": { - "version": "19.2.6", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.6.tgz", - "integrity": "sha512-0prMI+hvBbPjsWnxDLxlCGyM8PN6UuWjEUCYmZhO67xIV9Xasa/r/vDnq+Xyq4Lo27g8QSbO5YzARu0D1Sps3g==", + "version": "19.2.7", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.7.tgz", + "integrity": "sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==", "license": "MIT", "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { - "react": "^19.2.6" + "react": "^19.2.7" + } + }, + "node_modules/react-grab": { + "version": "0.1.48", + "resolved": "https://registry.npmjs.org/react-grab/-/react-grab-0.1.48.tgz", + "integrity": "sha512-p3WnmK9LLvXE/c4ITPLlXcP1fkXo2VFEQqK94tIfcHIWKNdqdhYYFyNVioO50HR+uyHIwT63Z4txZDqJlVcD/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@react-grab/cli": "0.1.48", + "bippy": "^0.5.43" + }, + "bin": { + "react-grab": "bin/cli.js" + }, + "peerDependencies": { + "react": ">=17.0.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + } } }, "node_modules/react-refresh": { @@ -6276,45 +8696,64 @@ } }, "node_modules/react-scan": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/react-scan/-/react-scan-0.5.3.tgz", - "integrity": "sha512-qde9PupmUf0L3MU1H6bjmoukZNbCXdMyTEwP4Gh8RQ4rZPd2GGNBgEKWszwLm96E8k+sGtMpc0B9P0KyFDP6Bw==", + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/react-scan/-/react-scan-0.5.7.tgz", + "integrity": "sha512-KRlq734yN6q/f2CZmZi9CWHuiqSzoLhPFLtcJOL6XM4lR54myyFcY81pG9QOwj+eBC1hIHm5n+Ntbtqiilu8Rg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/core": "^7.26.0", - "@babel/generator": "^7.26.2", - "@babel/types": "^7.26.0", - "@preact/signals": "^1.3.1", - "@rollup/pluginutils": "^5.1.3", - "@types/node": "^20.17.9", - "bippy": "^0.5.30", + "@babel/core": "^7.29.0", + "@babel/types": "^7.29.0", + "@preact/signals": "^2.9.0", + "@rollup/pluginutils": "^5.3.0", + "bippy": "^0.5.39", "commander": "^14.0.0", - "esbuild": "^0.25.0", - "estree-walker": "^3.0.3", "picocolors": "^1.1.1", - "preact": "^10.25.1", - "prompts": "^2.4.2" + "preact": "^10.29.1", + "prompts": "^2.4.2", + "react-doctor": "latest", + "react-grab": "latest" }, "bin": { "react-scan": "bin/cli.js" }, "optionalDependencies": { - "unplugin": "2.1.0" + "unplugin": "^3.0.0" }, "peerDependencies": { + "esbuild": ">=0.18.0", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "esbuild": { + "optional": true + } } }, - "node_modules/react-scan/node_modules/@types/node": { - "version": "20.19.39", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.39.tgz", - "integrity": "sha512-orrrD74MBUyK8jOAD/r0+lfa1I2MO6I+vAkmAWzMYbCcgrN4lCrmK52gRFQq/JRxfYPfonkr4b0jcY7Olqdqbw==", + "node_modules/react-scan/node_modules/commander": { + "version": "14.0.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz", + "integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==", "dev": true, "license": "MIT", - "dependencies": { - "undici-types": "~6.21.0" + "engines": { + "node": ">=20" + } + }, + "node_modules/react-use-measure": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/react-use-measure/-/react-use-measure-2.1.7.tgz", + "integrity": "sha512-KrvcAo13I/60HpwGO5jpW7E9DfusKyLPLvuHlUyP5zqnmAPhNc6qTRjUQrdTADl0lpPpDVU2/Gg51UlOGHXbdg==", + "license": "MIT", + "peerDependencies": { + "react": ">=16.13", + "react-dom": ">=16.13" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } } }, "node_modules/read-binary-file-arch": { @@ -6331,18 +8770,19 @@ } }, "node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "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": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" + "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/register-scheme": { @@ -6365,6 +8805,30 @@ "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/require-in-the-middle": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/require-in-the-middle/-/require-in-the-middle-8.0.1.tgz", + "integrity": "sha512-QT7FVMXfWOYFbeRBF6nu+I6tr2Tf3u0q8RIEjNob/heKY/nh7drD/k7eeMFmSQgnTtCzLDcCu/XEnpW2wk4xCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.5", + "module-details-from-path": "^1.0.3" + }, + "engines": { + "node": ">=9.3.0 || >=8.10.0 <9.0.0" + } + }, "node_modules/require-main-filename": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", @@ -6410,17 +8874,33 @@ } }, "node_modules/restore-cursor": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", - "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", + "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", "dev": true, "license": "MIT", "dependencies": { - "onetime": "^5.1.0", - "signal-exit": "^3.0.2" + "onetime": "^7.0.0", + "signal-exit": "^4.1.0" }, "engines": { - "node": ">=8" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/restore-cursor/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/retry": { @@ -6433,6 +8913,32 @@ "node": ">= 4" } }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "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", @@ -6453,13 +8959,13 @@ } }, "node_modules/rollup": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.57.1.tgz", - "integrity": "sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.2.tgz", + "integrity": "sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==", "dev": true, "license": "MIT", "dependencies": { - "@types/estree": "1.0.8" + "@types/estree": "1.0.9" }, "bin": { "rollup": "dist/bin/rollup" @@ -6469,45 +8975,69 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.57.1", - "@rollup/rollup-android-arm64": "4.57.1", - "@rollup/rollup-darwin-arm64": "4.57.1", - "@rollup/rollup-darwin-x64": "4.57.1", - "@rollup/rollup-freebsd-arm64": "4.57.1", - "@rollup/rollup-freebsd-x64": "4.57.1", - "@rollup/rollup-linux-arm-gnueabihf": "4.57.1", - "@rollup/rollup-linux-arm-musleabihf": "4.57.1", - "@rollup/rollup-linux-arm64-gnu": "4.57.1", - "@rollup/rollup-linux-arm64-musl": "4.57.1", - "@rollup/rollup-linux-loong64-gnu": "4.57.1", - "@rollup/rollup-linux-loong64-musl": "4.57.1", - "@rollup/rollup-linux-ppc64-gnu": "4.57.1", - "@rollup/rollup-linux-ppc64-musl": "4.57.1", - "@rollup/rollup-linux-riscv64-gnu": "4.57.1", - "@rollup/rollup-linux-riscv64-musl": "4.57.1", - "@rollup/rollup-linux-s390x-gnu": "4.57.1", - "@rollup/rollup-linux-x64-gnu": "4.57.1", - "@rollup/rollup-linux-x64-musl": "4.57.1", - "@rollup/rollup-openbsd-x64": "4.57.1", - "@rollup/rollup-openharmony-arm64": "4.57.1", - "@rollup/rollup-win32-arm64-msvc": "4.57.1", - "@rollup/rollup-win32-ia32-msvc": "4.57.1", - "@rollup/rollup-win32-x64-gnu": "4.57.1", - "@rollup/rollup-win32-x64-msvc": "4.57.1", + "@rollup/rollup-android-arm-eabi": "4.62.2", + "@rollup/rollup-android-arm64": "4.62.2", + "@rollup/rollup-darwin-arm64": "4.62.2", + "@rollup/rollup-darwin-x64": "4.62.2", + "@rollup/rollup-freebsd-arm64": "4.62.2", + "@rollup/rollup-freebsd-x64": "4.62.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.62.2", + "@rollup/rollup-linux-arm-musleabihf": "4.62.2", + "@rollup/rollup-linux-arm64-gnu": "4.62.2", + "@rollup/rollup-linux-arm64-musl": "4.62.2", + "@rollup/rollup-linux-loong64-gnu": "4.62.2", + "@rollup/rollup-linux-loong64-musl": "4.62.2", + "@rollup/rollup-linux-ppc64-gnu": "4.62.2", + "@rollup/rollup-linux-ppc64-musl": "4.62.2", + "@rollup/rollup-linux-riscv64-gnu": "4.62.2", + "@rollup/rollup-linux-riscv64-musl": "4.62.2", + "@rollup/rollup-linux-s390x-gnu": "4.62.2", + "@rollup/rollup-linux-x64-gnu": "4.62.2", + "@rollup/rollup-linux-x64-musl": "4.62.2", + "@rollup/rollup-openbsd-x64": "4.62.2", + "@rollup/rollup-openharmony-arm64": "4.62.2", + "@rollup/rollup-win32-arm64-msvc": "4.62.2", + "@rollup/rollup-win32-ia32-msvc": "4.62.2", + "@rollup/rollup-win32-x64-gnu": "4.62.2", + "@rollup/rollup-win32-x64-msvc": "4.62.2", "fsevents": "~2.3.2" } }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "devOptional": true, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "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", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "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.3", - "resolved": "https://registry.npmjs.org/sanitize-filename/-/sanitize-filename-1.6.3.tgz", - "integrity": "sha512-y/52Mcy7aw3gRm7IrcGDFx/bCk4AhRh2eI9luHOQM86nZsqwiRkkq2GekHXBBD+SmPidc8i2PqtYZl+pWJ8Oeg==", + "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": { @@ -6515,9 +9045,9 @@ } }, "node_modules/sax": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.4.tgz", - "integrity": "sha512-1n3r/tGXO6b6VXMdFT54SHzT9ytu9yr7TaELowdYpMqY/Ao7EnlQGmAQ1+RatX7Tkkdm6hONI2owqNx2aZj5Sw==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.6.0.tgz", + "integrity": "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==", "license": "BlueOak-1.0.0", "engines": { "node": ">=11.0.0" @@ -6529,16 +9059,21 @@ "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", "license": "MIT" }, + "node_modules/semifies": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/semifies/-/semifies-1.0.0.tgz", + "integrity": "sha512-xXR3KGeoxTNWPD4aBvL5NUpMTT7WMANr3EWnaS190QVkY52lqqcVRD7Q05UVbBhiWDGWMlJEUam9m7uFFGVScw==", + "dev": true, + "license": "Apache-2.0" + }, "node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "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" - }, - "engines": { - "node": ">=10" } }, "node_modules/semver-compare": { @@ -6566,6 +9101,20 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/serialize-error/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/set-blocking": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", @@ -6615,69 +9164,25 @@ "node": ">=10" } }, - "node_modules/sisteransi": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", - "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", - "dev": true, - "license": "MIT" - }, - "node_modules/slice-ansi": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-3.0.0.tgz", - "integrity": "sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "ansi-styles": "^4.0.0", - "astral-regex": "^2.0.0", - "is-fullwidth-code-point": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/smart-buffer": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", - "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 6.0.0", - "npm": ">= 3.0.0" - } - }, - "node_modules/socks": { - "version": "2.8.7", - "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz", - "integrity": "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==", + "node_modules/simple-update-notifier/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": "MIT", - "dependencies": { - "ip-address": "^10.0.1", - "smart-buffer": "^4.2.0" + "license": "ISC", + "bin": { + "semver": "bin/semver.js" }, "engines": { - "node": ">= 10.0.0", - "npm": ">= 3.0.0" + "node": ">=10" } }, - "node_modules/socks-proxy-agent": { - "version": "8.0.5", - "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz", - "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", "dev": true, - "license": "MIT", - "dependencies": { - "agent-base": "^7.1.2", - "debug": "^4.3.4", - "socks": "^2.8.3" - }, - "engines": { - "node": ">= 14" - } + "license": "MIT" }, "node_modules/source-map": { "version": "0.6.1", @@ -6718,19 +9223,6 @@ "license": "BSD-3-Clause", "optional": true }, - "node_modules/ssri": { - "version": "12.0.0", - "resolved": "https://registry.npmjs.org/ssri/-/ssri-12.0.0.tgz", - "integrity": "sha512-S7iGNosepx9RadX82oimUkvr0Ct7IjJbEbs4mJcTxst8um95J3sDYU1RBEOvdu6oL1Wek2ODI5i4MAw+dZ6cAQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "minipass": "^7.0.3" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, "node_modules/stat-mode": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/stat-mode/-/stat-mode-1.0.0.tgz", @@ -6741,93 +9233,79 @@ "node": ">= 6" } }, - "node_modules/string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "node_modules/stdin-discarder": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.3.2.tgz", + "integrity": "sha512-eCPu1qRxPVkl5605OTWF8Wz40b4Mf45NY5LQmVPQ599knfs5QhASUm9GbJ5BDMDOXgrnh0wyEdvzmL//YMlw0A==", "dev": true, "license": "MIT", - "dependencies": { - "safe-buffer": "~5.2.0" + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/string_decoder/node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "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, - "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/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==", "license": "MIT", "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" + "safe-buffer": "~5.1.0" } }, - "node_modules/string-width-cjs": { - "name": "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==", + "node_modules/string-width": { + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.2.2.tgz", + "integrity": "sha512-GaPUh5gfdrYzqeVNZvUfT23vYYxXzKYidUcnMtJg/3rxRV63EFZy3k6xfKlmfeJD0176lnUV/Usr3XcwSvFzpg==", "dev": true, "license": "MIT", "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" + "get-east-asian-width": "^1.5.0", + "strip-ansi": "^7.1.2" }, "engines": { - "node": ">=8" + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "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==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, "license": "MIT", "dependencies": { - "ansi-regex": "^5.0.1" + "ansi-regex": "^6.2.2" }, "engines": { - "node": ">=8" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, - "node_modules/strip-ansi-cjs": { - "name": "strip-ansi", - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "node_modules/stubborn-fs": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/stubborn-fs/-/stubborn-fs-2.0.0.tgz", + "integrity": "sha512-Y0AvSwDw8y+nlSNFXMm2g6L51rBGdAQT20J3YSOqxC53Lo3bjWRtr2BKcfYoAf352WYpsZSTURrA0tqhfgudPA==", "dev": true, "license": "MIT", "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" + "stubborn-utils": "^1.0.1" } }, + "node_modules/stubborn-utils": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/stubborn-utils/-/stubborn-utils-1.0.2.tgz", + "integrity": "sha512-zOh9jPYI+xrNOyisSelgym4tolKTJCQd5GBhK0+0xJvcYDcwlOoxF/rnFKQ2KRZknXSG9jWAp66fwP6AxN9STg==", + "dev": true, + "license": "MIT" + }, "node_modules/sumchecker": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/sumchecker/-/sumchecker-3.0.1.tgz", @@ -6854,10 +9332,32 @@ "node": ">=8" } }, + "node_modules/suspend-react": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/suspend-react/-/suspend-react-0.1.3.tgz", + "integrity": "sha512-aqldKgX9aZqpoDp3e8/BZ8Dm7x1pJl+qI3ZKxDN0i/IQTWUwBx/ManmlVJ3wowqbno6c2bmiIfs+Um6LbsjJyQ==", + "license": "MIT", + "peerDependencies": { + "react": ">=17.0" + } + }, + "node_modules/tagged-tag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/tagged-tag/-/tagged-tag-1.0.0.tgz", + "integrity": "sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/tar": { - "version": "7.5.13", - "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.13.tgz", - "integrity": "sha512-tOG/7GyXpFevhXVh8jOPJrmtRpOTsYqUIkVdVooZYJS/z8WhfQUX8RJILmeuJNinGAMSu1veBr4asSHFt5/hng==", + "version": "7.5.20", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.20.tgz", + "integrity": "sha512-9FcyK4PA6+WbzlTM9WhQm6vB5W7cP7dUiPsv1g7YDwEQnQ1CGpK3MGlKk/ITVWMk05kHZuBhmVhiv8LZoy/PFQ==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { @@ -6871,6 +9371,16 @@ "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", @@ -6897,19 +9407,11 @@ "fs-extra": "^10.0.0" } }, - "node_modules/temp/node_modules/rimraf": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", - "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", - "dev": true, - "license": "ISC", - "peer": true, - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - } + "node_modules/three": { + "version": "0.185.1", + "resolved": "https://registry.npmjs.org/three/-/three-0.185.1.tgz", + "integrity": "sha512-5aojFCXKwnjBRZvUnt3WFfEcvUJgkN5LlijRFN95hMy8WVkG4I0QNcJE+OuWvuJ0bOdStrbfXn0pkd6/QyiAlg==", + "license": "MIT" }, "node_modules/tiny-async-pool": { "version": "1.3.0", @@ -6937,10 +9439,20 @@ "integrity": "sha512-qVtvMxeXbVej0cQWKqVSSAHmKZEHAvxdF8HEUBFWts8h+xEo5m/lEiPakuyZ3BnCBjOD8i24kzNOiOLLgsSxhA==", "license": "MIT" }, + "node_modules/tinyexec": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.2.4.tgz", + "integrity": "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/tinyglobby": { - "version": "0.2.16", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", - "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", + "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": { @@ -6955,9 +9467,9 @@ } }, "node_modules/tmp": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz", - "integrity": "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==", + "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": { @@ -6974,6 +9486,19 @@ "tmp": "^0.2.0" } }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, "node_modules/tr46": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", @@ -6997,9 +9522,9 @@ "license": "0BSD" }, "node_modules/tsx": { - "version": "4.22.3", - "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.22.3.tgz", - "integrity": "sha512-mdoNxBC/cSQObGGVQ5Bpn5i+yv7j68gk3Nfm3wFjcJg3Z0Mix9jzAFfP12prmm5eVGmDKtp0yyArrs0Q+8gZHg==", + "version": "4.23.1", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.23.1.tgz", + "integrity": "sha512-GQHnkIfxyx1wYCOS/wonik5MVRZU9hi1TEZmzGZSCJB1y9YgoZ8H6itNE/u4suE+yLmOzuE4E5S4TZ/ZX2wcWQ==", "dev": true, "license": "MIT", "dependencies": { @@ -7016,9 +9541,9 @@ } }, "node_modules/tsx/node_modules/@esbuild/aix-ppc64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.0.tgz", - "integrity": "sha512-lhRUCeuOyJQURhTxl4WkpFTjIsbDayJHih5kZC1giwE+MhIzAb7mEsQMqMf18rHLsrb5qI1tafG20mLxEWcWlA==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz", + "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==", "cpu": [ "ppc64" ], @@ -7033,9 +9558,9 @@ } }, "node_modules/tsx/node_modules/@esbuild/android-arm": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.0.tgz", - "integrity": "sha512-wqh0ByljabXLKHeWXYLqoJ5jKC4XBaw6Hk08OfMrCRd2nP2ZQ5eleDZC41XHyCNgktBGYMbqnrJKq/K/lzPMSQ==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz", + "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==", "cpu": [ "arm" ], @@ -7050,9 +9575,9 @@ } }, "node_modules/tsx/node_modules/@esbuild/android-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.0.tgz", - "integrity": "sha512-+WzIXQOSaGs33tLEgYPYe/yQHf0WTU0X42Jca3y8NWMbUVhp7rUnw+vAsRC/QiDrdD31IszMrZy+qwPOPjd+rw==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz", + "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==", "cpu": [ "arm64" ], @@ -7067,9 +9592,9 @@ } }, "node_modules/tsx/node_modules/@esbuild/android-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.0.tgz", - "integrity": "sha512-+VJggoaKhk2VNNqVL7f6S189UzShHC/mR9EE8rDdSkdpN0KflSwWY/gWjDrNxxisg8Fp1ZCD9jLMo4m0OUfeUA==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz", + "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==", "cpu": [ "x64" ], @@ -7083,10 +9608,27 @@ "node": ">=18" } }, + "node_modules/tsx/node_modules/@esbuild/darwin-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz", + "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/tsx/node_modules/@esbuild/darwin-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.0.tgz", - "integrity": "sha512-fyzLm/DLDl/84OCfp2f/XQ4flmORsjU7VKt8HLjvIXChJoFFOIL6pLJPH4Yhd1n1gGFF9mPwtlN5Wf82DZs+LQ==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz", + "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==", "cpu": [ "x64" ], @@ -7101,9 +9643,9 @@ } }, "node_modules/tsx/node_modules/@esbuild/freebsd-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.0.tgz", - "integrity": "sha512-l9GeW5UZBT9k9brBYI+0WDffcRxgHQD8ShN2Ur4xWq/NFzUKm3k5lsH4PdaRgb2w7mI9u61nr2gI2mLI27Nh3Q==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz", + "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==", "cpu": [ "arm64" ], @@ -7118,9 +9660,9 @@ } }, "node_modules/tsx/node_modules/@esbuild/freebsd-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.0.tgz", - "integrity": "sha512-BXoQai/A0wPO6Es3yFJ7APCiKGc1tdAEOgeTNy3SsB491S3aHn4S4r3e976eUnPdU+NbdtmBuLncYir2tMU9Nw==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz", + "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==", "cpu": [ "x64" ], @@ -7135,9 +9677,9 @@ } }, "node_modules/tsx/node_modules/@esbuild/linux-arm": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.0.tgz", - "integrity": "sha512-CjaaREJagqJp7iTaNQjjidaNbCKYcd4IDkzbwwxtSvjI7NZm79qiHc8HqciMddQ6CKvJT6aBd8lO9kN/ZudLlw==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz", + "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==", "cpu": [ "arm" ], @@ -7152,9 +9694,9 @@ } }, "node_modules/tsx/node_modules/@esbuild/linux-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.0.tgz", - "integrity": "sha512-RVyzfb3FWsGA55n6WY0MEIEPURL1FcbhFE6BffZEMEekfCzCIMtB5yyDcFnVbTnwk+CLAgTujmV/Lgvih56W+A==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz", + "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==", "cpu": [ "arm64" ], @@ -7169,9 +9711,9 @@ } }, "node_modules/tsx/node_modules/@esbuild/linux-ia32": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.0.tgz", - "integrity": "sha512-KBnSTt1kxl9x70q+ydterVdl+Cn0H18ngRMRCEQfrbqdUuntQQ0LoMZv47uB97NljZFzY6HcfqEZ2SAyIUTQBQ==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz", + "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==", "cpu": [ "ia32" ], @@ -7186,9 +9728,9 @@ } }, "node_modules/tsx/node_modules/@esbuild/linux-loong64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.0.tgz", - "integrity": "sha512-zpSlUce1mnxzgBADvxKXX5sl8aYQHo2ezvMNI8I0lbblJtp8V4odlm3Yzlj7gPyt3T8ReksE6bK+pT3WD+aJRg==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz", + "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==", "cpu": [ "loong64" ], @@ -7203,9 +9745,9 @@ } }, "node_modules/tsx/node_modules/@esbuild/linux-mips64el": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.0.tgz", - "integrity": "sha512-2jIfP6mmjkdmeTlsX/9vmdmhBmKADrWqN7zcdtHIeNSCH1SqIoNI63cYsjQR8J+wGa4Y5izRcSHSm8K3QWmk3w==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz", + "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==", "cpu": [ "mips64el" ], @@ -7220,9 +9762,9 @@ } }, "node_modules/tsx/node_modules/@esbuild/linux-ppc64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.0.tgz", - "integrity": "sha512-bc0FE9wWeC0WBm49IQMPSPILRocGTQt3j5KPCA8os6VprfuJ7KD+5PzESSrJ6GmPIPJK965ZJHTUlSA6GNYEhg==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz", + "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==", "cpu": [ "ppc64" ], @@ -7237,9 +9779,9 @@ } }, "node_modules/tsx/node_modules/@esbuild/linux-riscv64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.0.tgz", - "integrity": "sha512-SQPZOwoTTT/HXFXQJG/vBX8sOFagGqvZyXcgLA3NhIqcBv1BJU1d46c0rGcrij2B56Z2rNiSLaZOYW5cUk7yLQ==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz", + "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==", "cpu": [ "riscv64" ], @@ -7254,9 +9796,9 @@ } }, "node_modules/tsx/node_modules/@esbuild/linux-s390x": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.0.tgz", - "integrity": "sha512-SCfR0HN8CEEjnYnySJTd2cw0k9OHB/YFzt5zgJEwa+wL/T/raGWYMBqwDNAC6dqFKmJYZoQBRfHjgwLHGSrn3Q==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz", + "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==", "cpu": [ "s390x" ], @@ -7271,9 +9813,9 @@ } }, "node_modules/tsx/node_modules/@esbuild/linux-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.0.tgz", - "integrity": "sha512-us0dSb9iFxIi8srnpl931Nvs65it/Jd2a2K3qs7fz2WfGPHqzfzZTfec7oxZJRNPXPnNYZtanmRc4AL/JwVzHQ==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz", + "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==", "cpu": [ "x64" ], @@ -7288,9 +9830,9 @@ } }, "node_modules/tsx/node_modules/@esbuild/netbsd-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.0.tgz", - "integrity": "sha512-CR/RYotgtCKwtftMwJlUU7xCVNg3lMYZ0RzTmAHSfLCXw3NtZtNpswLEj/Kkf6kEL3Gw+BpOekRX0BYCtklhUw==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz", + "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==", "cpu": [ "arm64" ], @@ -7305,9 +9847,9 @@ } }, "node_modules/tsx/node_modules/@esbuild/netbsd-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.0.tgz", - "integrity": "sha512-nU1yhmYutL+fQ71Kxnhg8uEOdC0pwEW9entHykTgEbna2pw2dkbFSMeqjjyHZoCmt8SBkOSvV+yNmm94aUrrqw==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz", + "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==", "cpu": [ "x64" ], @@ -7322,9 +9864,9 @@ } }, "node_modules/tsx/node_modules/@esbuild/openbsd-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.0.tgz", - "integrity": "sha512-cXb5vApOsRsxsEl4mcZ1XY3D4DzcoMxR/nnc4IyqYs0rTI8ZKmW6kyyg+11Z8yvgMfAEldKzP7AdP64HnSC/6g==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz", + "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==", "cpu": [ "arm64" ], @@ -7339,9 +9881,9 @@ } }, "node_modules/tsx/node_modules/@esbuild/openbsd-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.0.tgz", - "integrity": "sha512-8wZM2qqtv9UP3mzy7HiGYNH/zjTA355mpeuA+859TyR+e+Tc08IHYpLJuMsfpDJwoLo1ikIJI8jC3GFjnRClzA==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz", + "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==", "cpu": [ "x64" ], @@ -7356,9 +9898,9 @@ } }, "node_modules/tsx/node_modules/@esbuild/openharmony-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.0.tgz", - "integrity": "sha512-FLGfyizszcef5C3YtoyQDACyg95+dndv79i2EekILBofh5wpCa1KuBqOWKrEHZg3zrL3t5ouE5jgr94vA+Wb2w==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz", + "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==", "cpu": [ "arm64" ], @@ -7373,9 +9915,9 @@ } }, "node_modules/tsx/node_modules/@esbuild/sunos-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.0.tgz", - "integrity": "sha512-1ZgjUoEdHZZl/YlV76TSCz9Hqj9h9YmMGAgAPYd+q4SicWNX3G5GCyx9uhQWSLcbvPW8Ni7lj4gDa1T40akdlw==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz", + "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==", "cpu": [ "x64" ], @@ -7390,9 +9932,9 @@ } }, "node_modules/tsx/node_modules/@esbuild/win32-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.0.tgz", - "integrity": "sha512-Q9StnDmQ/enxnpxCCLSg0oo4+34B9TdXpuyPeTedN/6+iXBJ4J+zwfQI28u/Jl40nOYAxGoNi7mFP40RUtkmUA==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz", + "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==", "cpu": [ "arm64" ], @@ -7407,9 +9949,9 @@ } }, "node_modules/tsx/node_modules/@esbuild/win32-ia32": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.0.tgz", - "integrity": "sha512-zF3ag/gfiCe6U2iczcRzSYJKH1DCI+ByzSENHlM2FcDbEeo5Zd2C86Aq0tKUYAJJ1obRP84ymxIAksZUcdztHA==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz", + "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==", "cpu": [ "ia32" ], @@ -7424,9 +9966,9 @@ } }, "node_modules/tsx/node_modules/@esbuild/win32-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.0.tgz", - "integrity": "sha512-pEl1bO9mfAmIC+tW5btTmrKaujg3zGtUmWNdCw/xs70FBjwAL3o9OEKNHvNmnyylD6ubxUERiEhdsL0xBQ9efw==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz", + "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==", "cpu": [ "x64" ], @@ -7441,9 +9983,9 @@ } }, "node_modules/tsx/node_modules/esbuild": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.0.tgz", - "integrity": "sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz", + "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -7454,60 +9996,59 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.28.0", - "@esbuild/android-arm": "0.28.0", - "@esbuild/android-arm64": "0.28.0", - "@esbuild/android-x64": "0.28.0", - "@esbuild/darwin-arm64": "0.28.0", - "@esbuild/darwin-x64": "0.28.0", - "@esbuild/freebsd-arm64": "0.28.0", - "@esbuild/freebsd-x64": "0.28.0", - "@esbuild/linux-arm": "0.28.0", - "@esbuild/linux-arm64": "0.28.0", - "@esbuild/linux-ia32": "0.28.0", - "@esbuild/linux-loong64": "0.28.0", - "@esbuild/linux-mips64el": "0.28.0", - "@esbuild/linux-ppc64": "0.28.0", - "@esbuild/linux-riscv64": "0.28.0", - "@esbuild/linux-s390x": "0.28.0", - "@esbuild/linux-x64": "0.28.0", - "@esbuild/netbsd-arm64": "0.28.0", - "@esbuild/netbsd-x64": "0.28.0", - "@esbuild/openbsd-arm64": "0.28.0", - "@esbuild/openbsd-x64": "0.28.0", - "@esbuild/openharmony-arm64": "0.28.0", - "@esbuild/sunos-x64": "0.28.0", - "@esbuild/win32-arm64": "0.28.0", - "@esbuild/win32-ia32": "0.28.0", - "@esbuild/win32-x64": "0.28.0" - } - }, - "node_modules/tsx/node_modules/esbuild/node_modules/@esbuild/darwin-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.0.tgz", - "integrity": "sha512-0T+A9WZm+bZ84nZBtk1ckYsOvyA3x7e2Acj1KdVfV4/2tdG4fzUp91YHx+GArWLtwqp77pBXVCPn2We7Letr0Q==", - "cpu": [ - "arm64" - ], + "@esbuild/aix-ppc64": "0.28.1", + "@esbuild/android-arm": "0.28.1", + "@esbuild/android-arm64": "0.28.1", + "@esbuild/android-x64": "0.28.1", + "@esbuild/darwin-arm64": "0.28.1", + "@esbuild/darwin-x64": "0.28.1", + "@esbuild/freebsd-arm64": "0.28.1", + "@esbuild/freebsd-x64": "0.28.1", + "@esbuild/linux-arm": "0.28.1", + "@esbuild/linux-arm64": "0.28.1", + "@esbuild/linux-ia32": "0.28.1", + "@esbuild/linux-loong64": "0.28.1", + "@esbuild/linux-mips64el": "0.28.1", + "@esbuild/linux-ppc64": "0.28.1", + "@esbuild/linux-riscv64": "0.28.1", + "@esbuild/linux-s390x": "0.28.1", + "@esbuild/linux-x64": "0.28.1", + "@esbuild/netbsd-arm64": "0.28.1", + "@esbuild/netbsd-x64": "0.28.1", + "@esbuild/openbsd-arm64": "0.28.1", + "@esbuild/openbsd-x64": "0.28.1", + "@esbuild/openharmony-arm64": "0.28.1", + "@esbuild/sunos-x64": "0.28.1", + "@esbuild/win32-arm64": "0.28.1", + "@esbuild/win32-ia32": "0.28.1", + "@esbuild/win32-x64": "0.28.1" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], + "peer": true, + "dependencies": { + "prelude-ls": "^1.2.1" + }, "engines": { - "node": ">=18" + "node": ">= 0.8.0" } }, "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==", + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-5.8.0.tgz", + "integrity": "sha512-YGYEVz3Fm5iy/AybuA0oyNFq7H4CgQNfRp/qfe8nurE1kuCeNm3/vfm9X4Mtl+qLyaKJUh5xrFZwogr41SMjYA==", "dev": true, "license": "(MIT OR CC0-1.0)", - "optional": true, + "dependencies": { + "tagged-tag": "^1.0.0" + }, "engines": { - "node": ">=10" + "node": ">=20" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -7527,13 +10068,25 @@ "node": ">=14.17" } }, + "node_modules/uint8array-extras": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/uint8array-extras/-/uint8array-extras-1.5.0.tgz", + "integrity": "sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/undici": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/undici/-/undici-7.27.1.tgz", - "integrity": "sha512-UDdpiex+mzigiyrXrGbiUaF4HzTNhKbh2vRNFaTMzcqmLIPrZxaCtwo/1TMSuWoM1Xz3WiTo9KdgI3kRqYzJGg==", + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.28.0.tgz", + "integrity": "sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA==", "dev": true, "license": "MIT", - "optional": true, "engines": { "node": ">=20.18.1" } @@ -7545,32 +10098,6 @@ "dev": true, "license": "MIT" }, - "node_modules/unique-filename": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-4.0.0.tgz", - "integrity": "sha512-XSnEewXmQ+veP7xX2dS5Q4yZAvO40cBN2MWkJ7D/6sW4Dg6wYBNwM1Vrnz1FhH5AdeLIlUXRI9e28z1YZi71NQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "unique-slug": "^5.0.0" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/unique-slug": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-5.0.0.tgz", - "integrity": "sha512-9OdaqO5kwqR+1kVgHAhsp5vPNU0hnxRa26rBFNfNgM7M6pNtgzeBn3s/xbyCQL3dcjzOatcef6UUHpB/6MaETg==", - "dev": true, - "license": "ISC", - "dependencies": { - "imurmurhash": "^0.1.4" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, "node_modules/universalify": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", @@ -7581,18 +10108,88 @@ } }, "node_modules/unplugin": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-2.1.0.tgz", - "integrity": "sha512-us4j03/499KhbGP8BU7Hrzrgseo+KdfJYWcbcajCOqsAyb8Gk0Yn2kiUIcZISYCb1JFaZfIuG3b42HmguVOKCQ==", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-3.3.0.tgz", + "integrity": "sha512-qa66K+crbfyE6JK10GjvbJeRrOsuC/JpbnHctfyp/i4oBTxWOzJfRZyDiOk1PtErMFRu8JhsU/wPvOdBNWe5Rg==", "dev": true, "license": "MIT", "optional": true, "dependencies": { - "acorn": "^8.14.0", + "@jridgewell/remapping": "^2.3.5", + "picomatch": "^4.0.4", "webpack-virtual-modules": "^0.6.2" }, "engines": { - "node": ">=18.12.0" + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "@farmfe/core": "*", + "@rspack/core": "*", + "bun-types-no-globals": "*", + "esbuild": "*", + "rolldown": "*", + "rollup": "*", + "unloader": "*", + "vite": "*", + "webpack": "*" + }, + "peerDependenciesMeta": { + "@farmfe/core": { + "optional": true + }, + "@rspack/core": { + "optional": true + }, + "bun-types-no-globals": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "rolldown": { + "optional": true + }, + "rollup": { + "optional": true + }, + "unloader": { + "optional": true + }, + "vite": { + "optional": true + }, + "webpack": { + "optional": true + } + } + }, + "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/update-browserslist-db": { @@ -7632,10 +10229,20 @@ "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", "dev": true, "license": "BSD-2-Clause", + "peer": true, "dependencies": { "punycode": "^2.1.0" } }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/utf8-byte-length": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/utf8-byte-length/-/utf8-byte-length-1.0.5.tgz", @@ -7650,30 +10257,14 @@ "dev": true, "license": "MIT" }, - "node_modules/verror": { - "version": "1.10.1", - "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.1.tgz", - "integrity": "sha512-veufcmxri4e3XSrT0xwfUR7kguIkaxBeosDg00yDWhk49wdwkSUrvvsm7nc75e1PUyvIeZj6nS8VQRYz2/S4Xg==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "assert-plus": "^1.0.0", - "core-util-is": "1.0.2", - "extsprintf": "^1.2.0" - }, - "engines": { - "node": ">=0.6.0" - } - }, "node_modules/vite": { - "version": "7.3.5", - "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.5.tgz", - "integrity": "sha512-KuOaNhcnGFN2zIPGA7wRmzF+lJA1sea7rHq17aiJ++9lzY1WWG6Jpwqwe1KNbRVPIqHmr8GLYx7jbrQcN/7/ww==", + "version": "7.3.6", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.6.tgz", + "integrity": "sha512-4XP60spRGjSZFf1qYH+dJIkK2znL3zQfl9KkOV9MkkRR/3Dls0dxaBsQPTloEc5BLXWPL9vsOxopxyKoMmDueg==", "dev": true, "license": "MIT", "dependencies": { - "esbuild": "^0.27.0", + "esbuild": "^0.27.0 || ^0.28.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", @@ -7742,9 +10333,9 @@ } }, "node_modules/vite/node_modules/@esbuild/aix-ppc64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", - "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz", + "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==", "cpu": [ "ppc64" ], @@ -7759,9 +10350,9 @@ } }, "node_modules/vite/node_modules/@esbuild/android-arm": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz", - "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz", + "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==", "cpu": [ "arm" ], @@ -7776,9 +10367,9 @@ } }, "node_modules/vite/node_modules/@esbuild/android-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz", - "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz", + "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==", "cpu": [ "arm64" ], @@ -7793,9 +10384,9 @@ } }, "node_modules/vite/node_modules/@esbuild/android-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz", - "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz", + "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==", "cpu": [ "x64" ], @@ -7809,10 +10400,27 @@ "node": ">=18" } }, + "node_modules/vite/node_modules/@esbuild/darwin-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz", + "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/vite/node_modules/@esbuild/darwin-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz", - "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz", + "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==", "cpu": [ "x64" ], @@ -7827,9 +10435,9 @@ } }, "node_modules/vite/node_modules/@esbuild/freebsd-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz", - "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz", + "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==", "cpu": [ "arm64" ], @@ -7844,9 +10452,9 @@ } }, "node_modules/vite/node_modules/@esbuild/freebsd-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz", - "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz", + "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==", "cpu": [ "x64" ], @@ -7861,9 +10469,9 @@ } }, "node_modules/vite/node_modules/@esbuild/linux-arm": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz", - "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz", + "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==", "cpu": [ "arm" ], @@ -7878,9 +10486,9 @@ } }, "node_modules/vite/node_modules/@esbuild/linux-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz", - "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz", + "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==", "cpu": [ "arm64" ], @@ -7895,9 +10503,9 @@ } }, "node_modules/vite/node_modules/@esbuild/linux-ia32": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz", - "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz", + "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==", "cpu": [ "ia32" ], @@ -7912,9 +10520,9 @@ } }, "node_modules/vite/node_modules/@esbuild/linux-loong64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz", - "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz", + "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==", "cpu": [ "loong64" ], @@ -7929,9 +10537,9 @@ } }, "node_modules/vite/node_modules/@esbuild/linux-mips64el": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz", - "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz", + "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==", "cpu": [ "mips64el" ], @@ -7946,9 +10554,9 @@ } }, "node_modules/vite/node_modules/@esbuild/linux-ppc64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz", - "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz", + "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==", "cpu": [ "ppc64" ], @@ -7963,9 +10571,9 @@ } }, "node_modules/vite/node_modules/@esbuild/linux-riscv64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz", - "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz", + "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==", "cpu": [ "riscv64" ], @@ -7980,9 +10588,9 @@ } }, "node_modules/vite/node_modules/@esbuild/linux-s390x": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz", - "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz", + "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==", "cpu": [ "s390x" ], @@ -7997,9 +10605,9 @@ } }, "node_modules/vite/node_modules/@esbuild/linux-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz", - "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz", + "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==", "cpu": [ "x64" ], @@ -8014,9 +10622,9 @@ } }, "node_modules/vite/node_modules/@esbuild/netbsd-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz", - "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz", + "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==", "cpu": [ "arm64" ], @@ -8031,9 +10639,9 @@ } }, "node_modules/vite/node_modules/@esbuild/netbsd-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz", - "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz", + "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==", "cpu": [ "x64" ], @@ -8048,9 +10656,9 @@ } }, "node_modules/vite/node_modules/@esbuild/openbsd-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz", - "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz", + "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==", "cpu": [ "arm64" ], @@ -8065,9 +10673,9 @@ } }, "node_modules/vite/node_modules/@esbuild/openbsd-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz", - "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz", + "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==", "cpu": [ "x64" ], @@ -8082,9 +10690,9 @@ } }, "node_modules/vite/node_modules/@esbuild/openharmony-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz", - "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz", + "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==", "cpu": [ "arm64" ], @@ -8099,9 +10707,9 @@ } }, "node_modules/vite/node_modules/@esbuild/sunos-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz", - "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz", + "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==", "cpu": [ "x64" ], @@ -8116,9 +10724,9 @@ } }, "node_modules/vite/node_modules/@esbuild/win32-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz", - "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz", + "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==", "cpu": [ "arm64" ], @@ -8133,9 +10741,9 @@ } }, "node_modules/vite/node_modules/@esbuild/win32-ia32": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz", - "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz", + "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==", "cpu": [ "ia32" ], @@ -8150,9 +10758,9 @@ } }, "node_modules/vite/node_modules/@esbuild/win32-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz", - "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz", + "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==", "cpu": [ "x64" ], @@ -8167,9 +10775,9 @@ } }, "node_modules/vite/node_modules/esbuild": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", - "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz", + "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -8180,59 +10788,107 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.27.7", - "@esbuild/android-arm": "0.27.7", - "@esbuild/android-arm64": "0.27.7", - "@esbuild/android-x64": "0.27.7", - "@esbuild/darwin-arm64": "0.27.7", - "@esbuild/darwin-x64": "0.27.7", - "@esbuild/freebsd-arm64": "0.27.7", - "@esbuild/freebsd-x64": "0.27.7", - "@esbuild/linux-arm": "0.27.7", - "@esbuild/linux-arm64": "0.27.7", - "@esbuild/linux-ia32": "0.27.7", - "@esbuild/linux-loong64": "0.27.7", - "@esbuild/linux-mips64el": "0.27.7", - "@esbuild/linux-ppc64": "0.27.7", - "@esbuild/linux-riscv64": "0.27.7", - "@esbuild/linux-s390x": "0.27.7", - "@esbuild/linux-x64": "0.27.7", - "@esbuild/netbsd-arm64": "0.27.7", - "@esbuild/netbsd-x64": "0.27.7", - "@esbuild/openbsd-arm64": "0.27.7", - "@esbuild/openbsd-x64": "0.27.7", - "@esbuild/openharmony-arm64": "0.27.7", - "@esbuild/sunos-x64": "0.27.7", - "@esbuild/win32-arm64": "0.27.7", - "@esbuild/win32-ia32": "0.27.7", - "@esbuild/win32-x64": "0.27.7" - } - }, - "node_modules/vite/node_modules/esbuild/node_modules/@esbuild/darwin-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz", - "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==", - "cpu": [ - "arm64" - ], + "@esbuild/aix-ppc64": "0.28.1", + "@esbuild/android-arm": "0.28.1", + "@esbuild/android-arm64": "0.28.1", + "@esbuild/android-x64": "0.28.1", + "@esbuild/darwin-arm64": "0.28.1", + "@esbuild/darwin-x64": "0.28.1", + "@esbuild/freebsd-arm64": "0.28.1", + "@esbuild/freebsd-x64": "0.28.1", + "@esbuild/linux-arm": "0.28.1", + "@esbuild/linux-arm64": "0.28.1", + "@esbuild/linux-ia32": "0.28.1", + "@esbuild/linux-loong64": "0.28.1", + "@esbuild/linux-mips64el": "0.28.1", + "@esbuild/linux-ppc64": "0.28.1", + "@esbuild/linux-riscv64": "0.28.1", + "@esbuild/linux-s390x": "0.28.1", + "@esbuild/linux-x64": "0.28.1", + "@esbuild/netbsd-arm64": "0.28.1", + "@esbuild/netbsd-x64": "0.28.1", + "@esbuild/openbsd-arm64": "0.28.1", + "@esbuild/openbsd-x64": "0.28.1", + "@esbuild/openharmony-arm64": "0.28.1", + "@esbuild/sunos-x64": "0.28.1", + "@esbuild/win32-arm64": "0.28.1", + "@esbuild/win32-ia32": "0.28.1", + "@esbuild/win32-x64": "0.28.1" + } + }, + "node_modules/vscode-jsonrpc": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz", + "integrity": "sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], "engines": { - "node": ">=18" + "node": ">=14.0.0" } }, - "node_modules/wcwidth": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", - "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==", + "node_modules/vscode-languageserver": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/vscode-languageserver/-/vscode-languageserver-9.0.1.tgz", + "integrity": "sha512-woByF3PDpkHFUreUa7Hos7+pUWdeWMXRd26+ZX2A8cFx6v/JPTtd4/uN0/jB6XQHYaOlHbio03NTHCqrgG5n7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "vscode-languageserver-protocol": "3.17.5" + }, + "bin": { + "installServerIntoExtension": "bin/installServerIntoExtension" + } + }, + "node_modules/vscode-languageserver-protocol": { + "version": "3.17.5", + "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.5.tgz", + "integrity": "sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==", + "dev": true, + "license": "MIT", + "dependencies": { + "vscode-jsonrpc": "8.2.0", + "vscode-languageserver-types": "3.17.5" + } + }, + "node_modules/vscode-languageserver-textdocument": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.12.tgz", + "integrity": "sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA==", + "dev": true, + "license": "MIT" + }, + "node_modules/vscode-languageserver-types": { + "version": "3.17.5", + "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz", + "integrity": "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==", + "dev": true, + "license": "MIT" + }, + "node_modules/vscode-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.1.0.tgz", + "integrity": "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/web-vitals": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-5.3.0.tgz", + "integrity": "sha512-q6LWsLatGYZp5VGBIOvbTj6JBV2nOmC8KvWztXBmwJcfFAzhwKwbOxhUH306XY3CcaZDUlSmSuNPBsCn0bFu+g==", + "license": "Apache-2.0" + }, + "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": { - "defaults": "^1.0.3" + "@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/webidl-conversions": { @@ -8259,6 +10915,13 @@ "webidl-conversions": "^3.0.0" } }, + "node_modules/when-exit": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/when-exit/-/when-exit-2.1.5.tgz", + "integrity": "sha512-VGkKJ564kzt6Ms1dbgPP/yuIoQCrsFAnRbptpC5wOEsDaNsbCB2bnfnaA8i/vRs5tjUSEOtIuvl9/MyVsvQZCg==", + "dev": true, + "license": "MIT" + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -8281,6 +10944,17 @@ "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==", "license": "ISC" }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/wrap-ansi": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", @@ -8299,23 +10973,42 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, - "node_modules/wrap-ansi-cjs": { - "name": "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==", + "node_modules/wrap-ansi/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/wrap-ansi/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": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" }, "engines": { - "node": ">=10" + "node": ">=8" + } + }, + "node_modules/wrap-ansi/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" }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + "engines": { + "node": ">=8" } }, "node_modules/wrappy": { @@ -8326,9 +11019,9 @@ "license": "ISC" }, "node_modules/ws": { - "version": "8.21.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", - "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", + "version": "8.21.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.1.tgz", + "integrity": "sha512-+0NTnW77fFN/DjQi6k/Sq/Yvk4Sgajw7urW8V+asjXnRgDs9gyGkdb7EzgfhA4goXsRIZKE28fzIXBHEzhuiWw==", "license": "MIT", "engines": { "node": ">=10.0.0" @@ -8367,19 +11060,32 @@ } }, "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==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", "dev": true, - "license": "BlueOak-1.0.0", + "license": "ISC" + }, + "node_modules/yaml": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", + "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==", + "dev": true, + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, "engines": { - "node": ">=18" + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" } }, "node_modules/yargs": { - "version": "17.7.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", - "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "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": { @@ -8405,15 +11111,42 @@ "node": ">=12" } }, - "node_modules/yauzl": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", - "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "node_modules/yargs/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/yargs/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/yargs/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": { - "buffer-crc32": "~0.2.3", - "fd-slicer": "~1.1.0" + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" } }, "node_modules/yocto-queue": { @@ -8428,6 +11161,71 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/yoctocolors": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yoctocolors/-/yoctocolors-2.1.2.tgz", + "integrity": "sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", + "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-validation-error": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz", + "integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + } + }, + "node_modules/zustand": { + "version": "5.0.14", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.14.tgz", + "integrity": "sha512-/8tAspM5LMPr28b3fwLYrtdj77ECpfZviaP75CMTnwO8ISyaE4GDIG/9rDDYq/cH9D2Xw2A2RXglLInmVBQB/g==", + "license": "MIT", + "engines": { + "node": ">=12.20.0" + }, + "peerDependencies": { + "@types/react": ">=18.0.0", + "immer": ">=9.0.6", + "react": ">=18.0.0", + "use-sync-external-store": ">=1.2.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + }, + "use-sync-external-store": { + "optional": true + } + } } } } diff --git a/opennow-stable/package.json b/opennow-stable/package.json index 919e7cc99..4efa71df8 100644 --- a/opennow-stable/package.json +++ b/opennow-stable/package.json @@ -27,35 +27,61 @@ "lint": "oxlint src", "locales:check": "node scripts/check-translations.mjs", "typecheck": "tsc --noEmit -p tsconfig.node.json && tsc --noEmit -p tsconfig.json", - "test": "node scripts/run-tests.mjs && node --test scripts/after-sign-mac.test.mjs" + "test": "node scripts/run-tests.mjs && node --test scripts/after-sign-mac.test.mjs scripts/windows-pe-imports.test.mjs" }, "dependencies": { + "@formkit/auto-animate": "^0.10.0", + "@react-three/fiber": "^9.6.1", "discord-rpc": "^4.0.1", - "electron-updater": "^6.8.3", - "lucide-react": "^1.17.0", - "motion": "^12.42.0", + "electron-updater": "^6.8.9", + "lucide-react": "^1.25.0", + "motion": "^12.42.2", + "posthog-js": "^1.406.2", + "posthog-node": "^5.46.0", "qrcode": "^1.5.4", - "react": "^19.2.6", - "react-dom": "^19.2.6", - "ws": "^8.21.0" + "react": "^19.2.7", + "react-dom": "^19.2.7", + "three": "^0.185.1", + "ws": "^8.21.1" }, "devDependencies": { - "@types/discord-rpc": "^4.0.10", - "@types/node": "^22.19.17", + "@types/discord-rpc": "^4.0.11", + "@types/node": "^22.20.1", "@types/qrcode": "^1.5.6", - "@types/react": "^19.2.14", + "@types/react": "^19.2.17", "@types/react-dom": "^19.2.3", + "@types/three": "^0.185.1", "@types/ws": "^8.18.1", "@vitejs/plugin-react": "^5.2.0", "cross-env": "^10.1.0", - "electron": "^42.3.3", - "electron-builder": "^26.8.1", + "electron": "^43.1.1", + "electron-builder": "^26.15.3", "electron-vite": "^5.0.0", - "oxlint": "^1.67.0", - "react-scan": "^0.5.3", - "tsx": "^4.22.3", + "oxlint": "^1.74.0", + "react-scan": "^0.5.7", + "tsx": "^4.23.1", "typescript": "^6.0.3", - "vite": "^7.3.5" + "vite": "^7.3.6" + }, + "overrides": { + "lodash": "4.18.1", + "minimatch@3": "3.1.5", + "minimatch@5": "5.1.9", + "minimatch@9": "9.0.9", + "minimatch@10": "10.2.5", + "postcss": "8.5.20", + "rollup": "4.62.2", + "tar": "7.5.20", + "tmp": "0.2.7", + "undici": "7.28.0", + "js-yaml": "4.3.0", + "ajv@6": "6.15.0", + "brace-expansion@1": "1.1.13", + "brace-expansion@2": "2.0.3", + "brace-expansion@5": "5.0.7", + "ip-address": "10.2.0", + "@xmldom/xmldom": "0.8.13", + "ws": "$ws" }, "build": { "appId": "com.zortos.opennow.stable", @@ -129,6 +155,7 @@ "gstreamer1.0-plugins-good", "gstreamer1.0-plugins-bad", "gstreamer1.0-plugins-ugly", + "gstreamer1.0-nice", "gstreamer1.0-vaapi", "gstreamer1.0-gl", "gstreamer1.0-x", diff --git a/opennow-stable/scripts/after-sign-mac.mjs b/opennow-stable/scripts/after-sign-mac.mjs index 0eaf613f4..c9bd4df15 100644 --- a/opennow-stable/scripts/after-sign-mac.mjs +++ b/opennow-stable/scripts/after-sign-mac.mjs @@ -1,6 +1,105 @@ -import { execFileSync } from "node:child_process"; +import { execFileSync, spawnSync } from "node:child_process"; +import { open, readdir, stat } from "node:fs/promises"; import { join } from "node:path"; +const NESTED_CODE_BUNDLE_EXTENSIONS = [".app", ".appex", ".bundle", ".framework", ".xpc"]; +const MACH_O_MAGICS = new Set([ + 0xfeedface, + 0xfeedfacf, + 0xcafebabe, + 0xcafebabf, + 0xbebafeca, + 0xbfbafeca, + 0xcffaedfe, + 0xcefaedfe, +]); + +function isNestedCodeBundle(path) { + return NESTED_CODE_BUNDLE_EXTENSIONS.some((extension) => path.endsWith(extension)); +} + +async function isMachO(path) { + let file; + try { + file = await open(path, "r"); + const header = Buffer.allocUnsafe(4); + const { bytesRead } = await file.read(header, 0, header.length, 0); + return bytesRead === header.length && MACH_O_MAGICS.has(header.readUInt32BE(0)); + } catch (error) { + if (error?.code === "ENOENT") return false; + throw error; + } finally { + await file?.close(); + } +} + +async function collectNestedCodeObjects(root) { + const codeObjects = []; + + async function isFileEntry(path, entry) { + if (entry.isFile()) return true; + if (!entry.isSymbolicLink()) return false; + try { + return (await stat(path)).isFile(); + } catch (error) { + if (error?.code === "ENOENT") return false; + throw error; + } + } + + async function walk(directory) { + let entries; + try { + entries = await readdir(directory, { withFileTypes: true }); + } catch (error) { + if (error?.code === "ENOENT") return; + throw error; + } + + for (const entry of entries) { + const path = join(directory, entry.name); + if (entry.isDirectory()) { + await walk(path); + if (isNestedCodeBundle(path)) codeObjects.push(path); + } else if (await isFileEntry(path, entry) && await isMachO(path)) { + codeObjects.push(path); + } + } + } + + await walk(root); + return codeObjects.sort((left, right) => { + const leftDepth = left.split(/[\\/]+/).length; + const rightDepth = right.split(/[\\/]+/).length; + return rightDepth - leftDepth; + }); +} + +function hasValidCodeSignature(path) { + return spawnSync("codesign", ["--verify", "--strict", path], { + stdio: "ignore", + }).status === 0; +} + +function adHocSign(path, extraArgs = []) { + execFileSync("codesign", ["--force", "--sign", "-", ...extraArgs, path]); +} + +export async function signMacAppPreservingValidSignatures( + appPath, + bundleId, + { isSignatureValid = hasValidCodeSignature, sign = adHocSign } = {}, +) { + // Sign nested Mach-O binaries and bundles inside-out. Valid signatures are + // preserved; unsigned or invalidated code (for example after relocation) gets + // a fresh ad-hoc signature before its containing bundle is signed. + for (const codeObject of await collectNestedCodeObjects(join(appPath, "Contents"))) { + if (!isSignatureValid(codeObject)) sign(codeObject); + } + + sign(appPath, ["--requirements", `=designated => identifier "${bundleId}"`]); +} + export default async function afterSign({ appOutDir, packager }) { if (process.platform !== "darwin") return; if (process.env.CSC_IDENTITY_AUTO_DISCOVERY !== "false") return; @@ -8,10 +107,5 @@ export default async function afterSign({ appOutDir, packager }) { const appPath = join(appOutDir, `${packager.appInfo.productFilename}.app`); const bundleId = packager.appInfo.id; - execFileSync("codesign", [ - "--force", - "--sign", "-", - "--requirements", `=designated => identifier "${bundleId}"`, - appPath, - ]); + await signMacAppPreservingValidSignatures(appPath, bundleId); } diff --git a/opennow-stable/scripts/after-sign-mac.test.mjs b/opennow-stable/scripts/after-sign-mac.test.mjs index 21e3f05ed..f6a542d04 100644 --- a/opennow-stable/scripts/after-sign-mac.test.mjs +++ b/opennow-stable/scripts/after-sign-mac.test.mjs @@ -1,42 +1,58 @@ import assert from "node:assert/strict"; -import { chmod, mkdtemp, mkdir, readFile, rm, writeFile } from "node:fs/promises"; +import { mkdtemp, mkdir, rm, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; import test from "node:test"; -import afterSign from "./after-sign-mac.mjs"; +import { signMacAppPreservingValidSignatures } from "./after-sign-mac.mjs"; -test("re-signs only the outer macOS app bundle", { skip: process.platform !== "darwin" }, async () => { +test("signs nested Mach-O code inside-out without replacing valid signatures", async () => { const root = await mkdtemp(join(tmpdir(), "opennow-after-sign-")); - const binDir = join(root, "bin"); - const argsFile = join(root, "codesign-args"); const appOutDir = join(root, "output"); - const originalEnv = { ...process.env }; try { - await mkdir(binDir); - await mkdir(join(appOutDir, "OpenNOW.app"), { recursive: true }); - await writeFile(join(binDir, "codesign"), '#!/bin/sh\nprintf "%s\\n" "$@" >> "$CODESIGN_ARGS_FILE"\n'); - await chmod(join(binDir, "codesign"), 0o755); - process.env.PATH = `${binDir}:${process.env.PATH}`; - process.env.CODESIGN_ARGS_FILE = argsFile; - process.env.CSC_IDENTITY_AUTO_DISCOVERY = "false"; + const appPath = join(appOutDir, "OpenNOW.app"); + const frameworksDir = join(appPath, "Contents", "Frameworks"); + const signedHelper = join(frameworksDir, "OpenNOW Helper.app"); + const unsignedHelper = join(frameworksDir, "OpenNOW Helper (Plugin).app"); + const electronFramework = join(frameworksDir, "Electron Framework.framework"); + const frameworkBinary = join(electronFramework, "Versions", "A", "Electron Framework"); + const crashpadHandler = join( + electronFramework, + "Versions", + "A", + "Helpers", + "chrome_crashpad_handler", + ); + await mkdir(signedHelper, { recursive: true }); + await mkdir(unsignedHelper, { recursive: true }); + await mkdir(join(crashpadHandler, ".."), { recursive: true }); + await writeFile(frameworkBinary, Buffer.from([0xcf, 0xfa, 0xed, 0xfe])); + await writeFile(crashpadHandler, Buffer.from([0xcf, 0xfa, 0xed, 0xfe])); + const signCalls = []; - await afterSign({ - appOutDir, - packager: { - appInfo: { id: "com.zortos.opennow.stable", productFilename: "OpenNOW" }, - }, + await signMacAppPreservingValidSignatures(appPath, "com.zortos.opennow.stable", { + isSignatureValid: (path) => path === signedHelper, + sign: (path, extraArgs = []) => signCalls.push({ path, extraArgs }), }); - const args = (await readFile(argsFile, "utf8")).trim().split("\n"); - assert.equal(args.length, 6); - assert.equal(args.includes("--deep"), false); - assert.deepEqual(args.slice(0, 4), ["--force", "--sign", "-", "--requirements"]); - assert.equal(args[4], '=designated => identifier "com.zortos.opennow.stable"'); - assert.equal(args[5], join(appOutDir, "OpenNOW.app")); + const signedPaths = signCalls.map(({ path }) => path); + assert.equal(signedPaths.includes(signedHelper), false); + assert.equal(signedPaths.includes(unsignedHelper), true); + assert.equal(signedPaths.includes(crashpadHandler), true); + assert.equal(signedPaths.includes(frameworkBinary), true); + assert.equal(signedPaths.includes(electronFramework), true); + assert.ok(signedPaths.indexOf(crashpadHandler) < signedPaths.indexOf(frameworkBinary)); + assert.ok(signedPaths.indexOf(crashpadHandler) < signedPaths.indexOf(electronFramework)); + assert.ok(signedPaths.indexOf(frameworkBinary) < signedPaths.indexOf(electronFramework)); + assert.deepEqual(signCalls.at(-1), { + path: appPath, + extraArgs: [ + "--requirements", + '=designated => identifier "com.zortos.opennow.stable"', + ], + }); } finally { - process.env = originalEnv; await rm(root, { recursive: true, force: true }); } }); diff --git a/opennow-stable/scripts/build-native-streamer.mjs b/opennow-stable/scripts/build-native-streamer.mjs index 2ae8aa9f3..cf19c93ff 100644 --- a/opennow-stable/scripts/build-native-streamer.mjs +++ b/opennow-stable/scripts/build-native-streamer.mjs @@ -128,6 +128,28 @@ function formatCandidateSources(candidates) { return candidates.map((candidate) => candidate.source).join(", ") || "none"; } +function configureGstreamerPluginDiscovery(env, sdkRoot) { + const pluginDir = join(sdkRoot, "lib", "gstreamer-1.0"); + const scanner = join( + sdkRoot, + "libexec", + "gstreamer-1.0", + process.platform === "win32" ? "gst-plugin-scanner.exe" : "gst-plugin-scanner", + ); + + if (isExistingDirectory(pluginDir)) { + env.GST_PLUGIN_PATH = pluginDir; + env.GST_PLUGIN_PATH_1_0 = pluginDir; + env.GST_PLUGIN_SYSTEM_PATH = pluginDir; + env.GST_PLUGIN_SYSTEM_PATH_1_0 = pluginDir; + } + if (isExistingFile(scanner)) { + env.GST_PLUGIN_SCANNER = scanner; + env.GST_PLUGIN_SCANNER_1_0 = scanner; + } + env.GST_REGISTRY_REUSE_PLUGIN_SCANNER = "no"; +} + function configureGstreamerSdk(env) { if (process.platform === "win32") { const candidates = existingConfiguredCandidates([ @@ -158,6 +180,9 @@ function configureGstreamerSdk(env) { env.PKG_CONFIG = sdk.pkgConfigBinary; env.PKG_CONFIG_PATH = env.PKG_CONFIG_PATH ? `${pkgConfigDir}${delimiter}${env.PKG_CONFIG_PATH}` : pkgConfigDir; prependEnvPath(env, join(sdk.root, "bin")); + // The Windows MSI supports a custom INSTALLDIR, but a native executable outside + // the SDK cannot reliably infer that relocated plugin directory from its own path. + configureGstreamerPluginDiscovery(env, sdk.root); console.log(`Configured GStreamer SDK from ${sdk.source}.`); console.log("Configured pkg-config executable for GStreamer SDK."); return sdk.root; @@ -233,9 +258,29 @@ function bundleGstreamerRuntime(sdkRoot, nativeFeatures) { if (result.status !== 0) { process.exit(result.status ?? 1); } + + if (process.platform === "win32") { + injectWindowsVulkanPlugins(join(packagePlatformBinaryDir, "gstreamer")); + } + return true; } +function injectWindowsVulkanPlugins(runtimeRoot) { + const result = spawnSync( + process.execPath, + [join(__dirname, "inject-gstreamer-vulkan-windows.mjs"), "--dest", runtimeRoot], + { + cwd: packageRoot, + stdio: "inherit", + env: process.env, + }, + ); + if (result.status !== 0) { + process.exit(result.status ?? 1); + } +} + function isExistingFile(path) { try { return existsSync(path) && statSync(path).isFile(); @@ -390,6 +435,41 @@ function verifyGstreamerBinary(binaryPath, env) { console.log(`Verified native streamer GStreamer capabilities: ${availableVideoBackends.join(", ")}.`); } +function verifyBundledWindowsLoader(binaryPath, baseEnv) { + const result = spawnSync(binaryPath, { + cwd: dirname(binaryPath), + input: `${JSON.stringify({ id: verifyCommandId, type: "hello", protocolVersion: nativeStreamerProtocolVersion })}\n`, + encoding: "utf8", + env: { + SystemRoot: baseEnv.SystemRoot, + WINDIR: baseEnv.WINDIR, + PATH: dirname(binaryPath), + OPENNOW_NATIVE_STREAMER_BACKEND: "gstreamer", + }, + }); + if (result.status !== 0) { + console.error(result.stderr || result.stdout); + console.error("Bundled native streamer could not start using only DLLs next to its executable."); + process.exit(result.status ?? 1); + } + parseNativeStreamerResponse(result.stdout); + console.log("Verified native streamer Windows loader dependency closure."); +} + +function verifyBundledWindowsVulkanPlugin(binaryPath, env) { + const gstInspect = join(dirname(binaryPath), "gstreamer", "bin", "gst-inspect-1.0.exe"); + const result = spawnSync(gstInspect, ["vulkanupload"], { + encoding: "utf8", + env, + }); + if (result.status !== 0) { + console.error(result.stderr || result.stdout); + console.error("Bundled GStreamer Vulkan plugin failed to load."); + process.exit(result.status ?? 1); + } + console.log("Verified bundled GStreamer Vulkan plugin and loader."); +} + const cargoArgs = ["build", "--release", "--manifest-path", manifestPath]; if (nativeTarget) { cargoArgs.push("--target", nativeTarget); @@ -440,7 +520,12 @@ if (process.platform !== "win32") { if (hasFeature(nativeFeatures, "gstreamer")) { verifyGstreamerBinary(packageBinary, buildEnv); if (bundleGstreamerRuntime(gstreamerSdkRoot, nativeFeatures)) { - verifyGstreamerBinary(packagePlatformBinary, buildBundledGstreamerEnv(buildEnv, packagePlatformBinary)); + const bundledEnv = buildBundledGstreamerEnv(buildEnv, packagePlatformBinary); + if (process.platform === "win32") { + verifyBundledWindowsLoader(packagePlatformBinary, buildEnv); + verifyBundledWindowsVulkanPlugin(packagePlatformBinary, bundledEnv); + } + verifyGstreamerBinary(packagePlatformBinary, bundledEnv); } } diff --git a/opennow-stable/scripts/build-patched-gstreamer-d3d11.ps1 b/opennow-stable/scripts/build-patched-gstreamer-d3d11.ps1 new file mode 100644 index 000000000..abee94a9b --- /dev/null +++ b/opennow-stable/scripts/build-patched-gstreamer-d3d11.ps1 @@ -0,0 +1,91 @@ +param( + [Parameter(Mandatory = $true)] + [string] $GStreamerRoot, + [Parameter(Mandatory = $true)] + [string] $Version +) + +$ErrorActionPreference = "Stop" +$patch = Resolve-Path (Join-Path $PSScriptRoot "..\..\native\gstreamer-patches\0001-d3d11-enable-tearing-vrr.patch") +$workRoot = Join-Path ([System.IO.Path]::GetTempPath()) "opennow-gstreamer-$Version" +$archive = Join-Path $workRoot "gstreamer-$Version.tar.gz" +$sourceRoot = Join-Path $workRoot "gstreamer-$Version" +$buildRoot = Join-Path $workRoot "build" +$sourceUrl = "https://gitlab.freedesktop.org/gstreamer/gstreamer/-/archive/$Version/gstreamer-$Version.tar.gz" +$sourceMirrorUrl = "https://github.com/GStreamer/gstreamer/archive/refs/tags/$Version.tar.gz" + +Remove-Item -Recurse -Force $workRoot -ErrorAction SilentlyContinue +New-Item -ItemType Directory -Force -Path $workRoot | Out-Null + +& curl.exe --fail --location --retry 5 --retry-all-errors --output $archive $sourceUrl +if ($LASTEXITCODE -ne 0) { + & curl.exe --fail --location --retry 5 --retry-all-errors --output $archive $sourceMirrorUrl + if ($LASTEXITCODE -ne 0) { + throw "Failed to download GStreamer $Version source." + } +} + +& tar.exe -xzf $archive -C $workRoot +if ($LASTEXITCODE -ne 0) { + throw "Failed to extract GStreamer $Version source." +} + +& git.exe -C $sourceRoot apply --check $patch +if ($LASTEXITCODE -ne 0) { + throw "OpenNOW D3D11 tearing patch does not apply to GStreamer $Version." +} +& git.exe -C $sourceRoot apply $patch +if ($LASTEXITCODE -ne 0) { + throw "Failed to apply OpenNOW D3D11 tearing patch." +} + +python -m pip install --disable-pip-version-check --quiet meson ninja +if ($LASTEXITCODE -ne 0) { + throw "Failed to install Meson and Ninja." +} + +$env:PKG_CONFIG_PATH = "$(Join-Path $GStreamerRoot "lib\pkgconfig");$env:PKG_CONFIG_PATH" +$env:PATH = "$(Join-Path $GStreamerRoot "bin");$env:PATH" +$badPluginsSource = Join-Path $sourceRoot "subprojects\gst-plugins-bad" + +meson setup $buildRoot $badPluginsSource ` + --buildtype=release ` + --vsenv ` + -Dauto_features=disabled ` + -Dd3d11=enabled ` + -Dtests=disabled ` + -Dexamples=disabled ` + -Dtools=disabled ` + -Dintrospection=disabled ` + -Dgpl=disabled +if ($LASTEXITCODE -ne 0) { + throw "Failed to configure the patched GStreamer D3D11 plugin." +} + +meson compile -C $buildRoot gstd3d11 +if ($LASTEXITCODE -ne 0) { + throw "Failed to compile the patched GStreamer D3D11 plugin." +} + +$plugin = Get-ChildItem -Path $buildRoot -Filter "gstd3d11.dll" -Recurse | + Select-Object -First 1 +if (-not $plugin) { + throw "Patched gstd3d11.dll was not produced." +} + +$pluginDestination = Join-Path $GStreamerRoot "lib\gstreamer-1.0\gstd3d11.dll" +Copy-Item -Force $plugin.FullName $pluginDestination + +$d3d11Library = Get-ChildItem -Path $buildRoot -Filter "gstd3d11-1.0-0.dll" -Recurse | + Select-Object -First 1 +if ($d3d11Library) { + Copy-Item -Force $d3d11Library.FullName (Join-Path $GStreamerRoot "bin\gstd3d11-1.0-0.dll") +} + +$gstInspect = Join-Path $GStreamerRoot "bin\gst-inspect-1.0.exe" +& $gstInspect d3d11videosink +if ($LASTEXITCODE -ne 0) { + throw "Patched GStreamer D3D11 plugin failed its gst-inspect smoke check." +} + +Write-Host "Installed patched GStreamer D3D11 plugin: $pluginDestination" diff --git a/opennow-stable/scripts/bundle-gstreamer-runtime.mjs b/opennow-stable/scripts/bundle-gstreamer-runtime.mjs index 6f9ae4642..174cc4615 100644 --- a/opennow-stable/scripts/bundle-gstreamer-runtime.mjs +++ b/opennow-stable/scripts/bundle-gstreamer-runtime.mjs @@ -10,9 +10,10 @@ import { statSync, writeFileSync, } from "node:fs"; -import { delimiter, dirname, extname, join, relative, resolve } from "node:path"; +import { basename, delimiter, dirname, extname, join, relative, resolve } from "node:path"; import { fileURLToPath } from "node:url"; import { spawnSync } from "node:child_process"; +import { collectBundledPeDependencies } from "./windows-pe-imports.mjs"; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); @@ -149,21 +150,6 @@ function writeMetadata(destination, source, platform) { ); } -const WINDOWS_LOADER_DLLS = [ - "gstreamer-1.0-0.dll", - "glib-2.0-0.dll", - "gobject-2.0-0.dll", - "gio-2.0-0.dll", - "gmodule-2.0-0.dll", - "gthread-2.0-0.dll", - "intl-8.dll", - "ffi-8.dll", - "pcre2-8-0.dll", - "orc-0.4-0.dll", - "winpthread-1.dll", - "zlib1.dll", -]; - const WINDOWS_VC_RUNTIME_DLLS = [ "vcruntime140.dll", "vcruntime140_1.dll", @@ -191,9 +177,11 @@ function copyWindowsLoaderDlls({ sdkRoot, destination, binary }) { const copiedLoader = []; const copiedVc = []; - for (const name of WINDOWS_LOADER_DLLS) { - const source = join(sdkBin, name); - if (!isExistingFile(source)) continue; + if (!binary) { + throw new Error("A native streamer executable is required to collect its Windows DLL dependencies."); + } + for (const source of collectBundledPeDependencies(binary, sdkBin)) { + const name = basename(source); copyFileSync(source, join(executableDir, name)); copiedLoader.push(name); } diff --git a/opennow-stable/scripts/ensure-electron-installed.mjs b/opennow-stable/scripts/ensure-electron-installed.mjs index 2a4c83d77..1cfb33433 100644 --- a/opennow-stable/scripts/ensure-electron-installed.mjs +++ b/opennow-stable/scripts/ensure-electron-installed.mjs @@ -118,11 +118,27 @@ if (!hasElectronBinary()) { rmSync(resolvedDistRoot, { recursive: true, force: true }); mkdirSync(resolvedDistRoot, { recursive: true }); - const electronRequire = createRequire(electronPackageJson); - const { extract } = await import(pathToFileURL(electronRequire.resolve("@electron-internal/extract-zip")).href); - try { - await extract(zipPath, { dir: resolvedDistRoot }); + if (platform === "win32") { + // Windows ships bsdtar, which extracts Electron's zip reliably even on + // newer Node releases where extract-zip@2 can leave its promise pending. + const extractResult = spawnSync("tar.exe", ["-xf", zipPath, "-C", resolvedDistRoot], { + stdio: "inherit", + }); + if (extractResult.status !== 0) { + throw new Error(`Windows archive extraction failed with exit code ${extractResult.status ?? "unknown"}.`); + } + } else { + // Electron 42 publishes extract-zip as a regular dependency. Resolve it + // from Electron's package boundary so npm hoisting does not affect us. + const electronRequire = createRequire(electronPackageJson); + const extractZipModule = await import(pathToFileURL(electronRequire.resolve("extract-zip")).href); + const extract = extractZipModule.default ?? extractZipModule.extract; + if (typeof extract !== "function") { + throw new TypeError("Electron archive extractor did not expose a callable function."); + } + await extract(zipPath, { dir: resolvedDistRoot }); + } } catch (error) { console.error("Failed to extract Electron archive:", error); process.exit(1); diff --git a/opennow-stable/scripts/inject-gstreamer-vulkan-windows.mjs b/opennow-stable/scripts/inject-gstreamer-vulkan-windows.mjs new file mode 100644 index 000000000..94146d122 --- /dev/null +++ b/opennow-stable/scripts/inject-gstreamer-vulkan-windows.mjs @@ -0,0 +1,148 @@ +import { copyFileSync, existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from "node:fs"; +import { dirname, join, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; +import { spawnSync } from "node:child_process"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); +const packageRoot = resolve(__dirname, ".."); +const repoRoot = resolve(packageRoot, ".."); +const vendorRoot = join(repoRoot, "native", "opennow-streamer", "vendor", "gstreamer-vulkan-windows"); + +function parseArgs(argv) { + const parsed = new Map(); + for (let index = 0; index < argv.length; index += 1) { + const value = argv[index]; + if (!value.startsWith("--")) continue; + const key = value.slice(2); + const next = argv[index + 1]; + if (!next || next.startsWith("--")) { + parsed.set(key, "true"); + continue; + } + parsed.set(key, next); + index += 1; + } + return parsed; +} + +function isExistingFile(path) { + try { + return existsSync(path) && statSync(path).isFile(); + } catch { + return false; + } +} + +function isExistingDirectory(path) { + try { + return existsSync(path) && statSync(path).isDirectory(); + } catch { + return false; + } +} + +function readRuntimeVersion(runtimeRoot) { + const metadataPath = join(runtimeRoot, "OPENNOW-GSTREAMER-RUNTIME.txt"); + if (!isExistingFile(metadataPath)) return null; + const text = readFileSync(metadataPath, "utf8"); + const sourceLine = text.split(/\r?\n/).find((line) => line.startsWith("Source:")); + // Prefer probing the bundled gst-inspect version when available. + const inspect = join(runtimeRoot, "bin", "gst-inspect-1.0.exe"); + if (isExistingFile(inspect)) { + const result = spawnSync(inspect, ["--version"], { encoding: "utf8" }); + const match = `${result.stdout ?? ""}\n${result.stderr ?? ""}`.match(/GStreamer\s+(\d+\.\d+\.\d+)/i); + if (match) return match[1]; + } + return sourceLine ? sourceLine.slice("Source:".length).trim() : null; +} + +function listVendorVersions() { + if (!isExistingDirectory(vendorRoot)) return []; + return readdirSync(vendorRoot) + .filter((name) => /^\d+\.\d+\.\d+$/.test(name) && isExistingDirectory(join(vendorRoot, name))) + .sort((a, b) => b.localeCompare(a, undefined, { numeric: true })); +} + +function resolveVendorDir(runtimeVersion) { + const versions = listVendorVersions(); + if (versions.length === 0) return null; + if (runtimeVersion && versions.includes(runtimeVersion)) { + return join(vendorRoot, runtimeVersion); + } + // Fall back to newest vendored build; plugin ABI is usually compatible within a minor series. + if (runtimeVersion) { + const [major, minor] = runtimeVersion.split("."); + const sameSeries = versions.find((version) => version.startsWith(`${major}.${minor}.`)); + if (sameSeries) return join(vendorRoot, sameSeries); + return null; + } + return join(vendorRoot, versions[0]); +} + +function injectVulkanPlugins(runtimeRoot, vendorDir) { + const pluginSource = join(vendorDir, "lib", "gstreamer-1.0", "gstvulkan.dll"); + const librarySource = join(vendorDir, "bin", "gstvulkan-1.0-0.dll"); + const loaderSource = join(packageRoot, "node_modules", "electron", "dist", "vulkan-1.dll"); + if (!isExistingFile(pluginSource) || !isExistingFile(librarySource)) { + throw new Error(`Vendored Vulkan artifacts are incomplete under ${vendorDir}`); + } + if (!isExistingFile(loaderSource)) { + throw new Error(`Electron Vulkan loader was not found: ${loaderSource}`); + } + + const pluginDestDir = join(runtimeRoot, "lib", "gstreamer-1.0"); + const binDestDir = join(runtimeRoot, "bin"); + mkdirSync(pluginDestDir, { recursive: true }); + mkdirSync(binDestDir, { recursive: true }); + copyFileSync(pluginSource, join(pluginDestDir, "gstvulkan.dll")); + copyFileSync(librarySource, join(binDestDir, "gstvulkan-1.0-0.dll")); + copyFileSync(loaderSource, join(binDestDir, "vulkan-1.dll")); + + const metadataPath = join(runtimeRoot, "OPENNOW-GSTREAMER-RUNTIME.txt"); + if (isExistingFile(metadataPath)) { + const text = readFileSync(metadataPath, "utf8"); + if (!text.includes("Vulkan plugin: injected")) { + writeFileSync( + metadataPath, + `${text.trimEnd()}\nVulkan plugin: injected from ${vendorDir}\n`, + "utf8", + ); + } + } + + console.log(`Injected Windows GStreamer Vulkan plugins and loader into ${runtimeRoot}.`); +} + +const args = parseArgs(process.argv.slice(2)); +const destination = args.get("dest"); +if (!destination) { + console.error("Usage: node scripts/inject-gstreamer-vulkan-windows.mjs --dest "); + process.exit(1); +} + +if (process.platform !== "win32") { + console.log("Skipping Windows Vulkan plugin inject on non-Windows host."); + process.exit(0); +} + +try { + const runtimeRoot = resolve(packageRoot, destination); + if (!isExistingDirectory(runtimeRoot)) { + throw new Error(`GStreamer runtime directory was not found: ${runtimeRoot}`); + } + + const runtimeVersion = readRuntimeVersion(runtimeRoot); + const vendorDir = resolveVendorDir(runtimeVersion); + if (!vendorDir) { + throw new Error( + `No compatible vendored Windows GStreamer Vulkan plugins were found for runtime ${runtimeVersion ?? "unknown"}. ` + + `Expected artifacts under ${vendorRoot}//.`, + ); + } + + injectVulkanPlugins(runtimeRoot, vendorDir); +} catch (error) { + console.error(error instanceof Error ? error.message : String(error)); + process.exit(1); +} diff --git a/opennow-stable/scripts/windows-pe-imports.mjs b/opennow-stable/scripts/windows-pe-imports.mjs new file mode 100644 index 000000000..f00377934 --- /dev/null +++ b/opennow-stable/scripts/windows-pe-imports.mjs @@ -0,0 +1,123 @@ +import { readFileSync, readdirSync } from "node:fs"; +import { basename, join } from "node:path"; + +function checkedRead(buffer, offset, size, label) { + if (!Number.isInteger(offset) || offset < 0 || offset + size > buffer.length) { + throw new Error(`Invalid PE ${label} offset: ${offset}`); + } +} + +function readUInt16(buffer, offset, label) { + checkedRead(buffer, offset, 2, label); + return buffer.readUInt16LE(offset); +} + +function readUInt32(buffer, offset, label) { + checkedRead(buffer, offset, 4, label); + return buffer.readUInt32LE(offset); +} + +function readAsciiString(buffer, offset) { + checkedRead(buffer, offset, 1, "string"); + const end = buffer.indexOf(0, offset); + if (end < 0) { + throw new Error(`Unterminated PE import name at offset ${offset}`); + } + return buffer.toString("ascii", offset, end); +} + +export function readPeImportNames(buffer) { + if (buffer.length < 64 || buffer.toString("ascii", 0, 2) !== "MZ") { + throw new Error("File is not a PE executable"); + } + + const peOffset = readUInt32(buffer, 0x3c, "header"); + checkedRead(buffer, peOffset, 24, "signature"); + if (buffer.toString("ascii", peOffset, peOffset + 4) !== "PE\u0000\u0000") { + throw new Error("Invalid PE signature"); + } + + const sectionCount = readUInt16(buffer, peOffset + 6, "section count"); + const optionalHeaderSize = readUInt16(buffer, peOffset + 20, "optional header size"); + const optionalHeaderOffset = peOffset + 24; + const optionalHeaderMagic = readUInt16(buffer, optionalHeaderOffset, "optional header"); + const dataDirectoryOffset = optionalHeaderOffset + ( + optionalHeaderMagic === 0x10b ? 96 : optionalHeaderMagic === 0x20b ? 112 : 0 + ); + if (dataDirectoryOffset === optionalHeaderOffset) { + throw new Error(`Unsupported PE optional header: 0x${optionalHeaderMagic.toString(16)}`); + } + + checkedRead(buffer, optionalHeaderOffset, optionalHeaderSize, "optional header"); + const importTableRva = readUInt32(buffer, dataDirectoryOffset + 8, "import table"); + if (importTableRva === 0) { + return []; + } + + const sectionTableOffset = optionalHeaderOffset + optionalHeaderSize; + const sections = []; + for (let index = 0; index < sectionCount; index += 1) { + const offset = sectionTableOffset + index * 40; + checkedRead(buffer, offset, 40, "section"); + sections.push({ + virtualSize: readUInt32(buffer, offset + 8, "section virtual size"), + virtualAddress: readUInt32(buffer, offset + 12, "section virtual address"), + rawSize: readUInt32(buffer, offset + 16, "section raw size"), + rawOffset: readUInt32(buffer, offset + 20, "section raw offset"), + }); + } + + const rvaToOffset = (rva) => { + const section = sections.find(({ virtualAddress, virtualSize, rawSize }) => + rva >= virtualAddress && rva < virtualAddress + Math.max(virtualSize, rawSize), + ); + if (!section) { + throw new Error(`PE import RVA 0x${rva.toString(16)} is outside every section`); + } + const offset = section.rawOffset + rva - section.virtualAddress; + checkedRead(buffer, offset, 1, "import"); + return offset; + }; + + const imports = []; + let descriptorOffset = rvaToOffset(importTableRva); + for (;;) { + checkedRead(buffer, descriptorOffset, 20, "import descriptor"); + const fields = Array.from({ length: 5 }, (_, index) => + readUInt32(buffer, descriptorOffset + index * 4, "import descriptor"), + ); + if (fields.every((value) => value === 0)) { + break; + } + imports.push(readAsciiString(buffer, rvaToOffset(fields[3]))); + descriptorOffset += 20; + } + return imports; +} + +export function collectBundledPeDependencies(binary, dependencyDirectory) { + const available = new Map( + readdirSync(dependencyDirectory) + .filter((name) => name.toLowerCase().endsWith(".dll")) + .map((name) => [name.toLowerCase(), join(dependencyDirectory, name)]), + ); + const dependencies = new Map(); + const pending = [binary]; + + while (pending.length > 0) { + const current = pending.pop(); + for (const importedName of readPeImportNames(readFileSync(current))) { + const normalizedName = importedName.toLowerCase(); + const dependency = available.get(normalizedName); + if (!dependency || dependencies.has(normalizedName)) { + continue; + } + dependencies.set(normalizedName, dependency); + pending.push(dependency); + } + } + + return [...dependencies.values()].sort((left, right) => + basename(left).localeCompare(basename(right), "en", { sensitivity: "base" }), + ); +} diff --git a/opennow-stable/scripts/windows-pe-imports.test.mjs b/opennow-stable/scripts/windows-pe-imports.test.mjs new file mode 100644 index 000000000..c5e1a7ff0 --- /dev/null +++ b/opennow-stable/scripts/windows-pe-imports.test.mjs @@ -0,0 +1,71 @@ +import assert from "node:assert/strict"; +import { mkdtemp, mkdir, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import test from "node:test"; + +import { collectBundledPeDependencies, readPeImportNames } from "./windows-pe-imports.mjs"; + +function peFixture(importNames) { + const buffer = Buffer.alloc(0x800); + const peOffset = 0x80; + const optionalHeaderOffset = peOffset + 24; + const sectionTableOffset = optionalHeaderOffset + 0xf0; + const sectionRva = 0x1000; + const sectionOffset = 0x200; + const importTableOffset = sectionOffset; + + buffer.write("MZ", 0, "ascii"); + buffer.writeUInt32LE(peOffset, 0x3c); + buffer.write("PE\u0000\u0000", peOffset, "ascii"); + buffer.writeUInt16LE(0x8664, peOffset + 4); + buffer.writeUInt16LE(1, peOffset + 6); + buffer.writeUInt16LE(0xf0, peOffset + 20); + buffer.writeUInt16LE(0x20b, optionalHeaderOffset); + buffer.writeUInt32LE(sectionRva, optionalHeaderOffset + 112 + 8); + buffer.write(".rdata\u0000\u0000", sectionTableOffset, "ascii"); + buffer.writeUInt32LE(0x600, sectionTableOffset + 8); + buffer.writeUInt32LE(sectionRva, sectionTableOffset + 12); + buffer.writeUInt32LE(0x600, sectionTableOffset + 16); + buffer.writeUInt32LE(sectionOffset, sectionTableOffset + 20); + + let nameOffset = importTableOffset + (importNames.length + 1) * 20; + importNames.forEach((name, index) => { + buffer.writeUInt32LE(sectionRva + nameOffset - sectionOffset, importTableOffset + index * 20 + 12); + buffer.write(`${name}\u0000`, nameOffset, "ascii"); + nameOffset += Buffer.byteLength(name) + 1; + }); + return buffer; +} + +test("reads imported DLL names from a 64-bit PE image", () => { + assert.deepEqual( + readPeImportNames(peFixture(["gstvideo-1.0-0.dll", "KERNEL32.dll"])), + ["gstvideo-1.0-0.dll", "KERNEL32.dll"], + ); +}); + +test("collects only the recursive DLL closure available in the runtime", async () => { + const root = await mkdtemp(join(tmpdir(), "opennow-pe-imports-")); + const runtime = join(root, "runtime"); + const binary = join(root, "opennow-streamer.exe"); + + try { + await mkdir(runtime); + await writeFile(binary, peFixture(["gstvideo-1.0-0.dll", "KERNEL32.dll"])); + await writeFile( + join(runtime, "gstvideo-1.0-0.dll"), + peFixture(["gstreamer-1.0-0.dll", "ffi-7.dll"]), + ); + await writeFile(join(runtime, "gstreamer-1.0-0.dll"), peFixture(["glib-2.0-0.dll"])); + await writeFile(join(runtime, "glib-2.0-0.dll"), peFixture([])); + await writeFile(join(runtime, "unused.dll"), peFixture([])); + + assert.deepEqual( + collectBundledPeDependencies(binary, runtime).map((path) => path.slice(runtime.length + 1)), + ["glib-2.0-0.dll", "gstreamer-1.0-0.dll", "gstvideo-1.0-0.dll"], + ); + } finally { + await rm(root, { recursive: true, force: true }); + } +}); diff --git a/opennow-stable/src/main/community/provisionSessionProxy.ts b/opennow-stable/src/main/community/provisionSessionProxy.ts index 1d4489a6a..0b15a4088 100644 --- a/opennow-stable/src/main/community/provisionSessionProxy.ts +++ b/opennow-stable/src/main/community/provisionSessionProxy.ts @@ -5,8 +5,8 @@ import { type CommunityProxyProvisionResult, ZORTOS_COMMUNITY_PROXY_PROVISION_URL, } from "@shared/communityProxy"; -import { getStableDeviceId } from "../gfn/deviceId"; -import { normalizeSessionProxyUrl } from "../gfn/proxyUrl"; +import { getStableDeviceId } from "../platforms/gfn/deviceId"; +import { normalizeSessionProxyUrl } from "../platforms/gfn/proxyUrl"; import { fetchWithTimeout } from "../services/requestTimeout"; const PROVISION_TIMEOUT_MS = 15_000; diff --git a/opennow-stable/src/main/discordPresence.test.ts b/opennow-stable/src/main/discordPresence.test.ts index a78ed29fa..2937d40d3 100644 --- a/opennow-stable/src/main/discordPresence.test.ts +++ b/opennow-stable/src/main/discordPresence.test.ts @@ -28,10 +28,11 @@ test("discordActivityFromSession includes queue position and only timestamps str appId: 1001, status: 1, queuePosition: 12, - }, "Test Game"); + }, "Test Game", "https://example.com/test-game.jpg"); assert.deepEqual(queued, { gameName: "Test Game", + gameImageUrl: "https://example.com/test-game.jpg", kind: "queued", appId: "1001", queuePosition: 12, @@ -94,6 +95,7 @@ test("discordMonitorActivityDecision does not downgrade active streaming presenc test("discordMonitorActivityDecision preserves current game title on queue updates", () => { const decision = discordMonitorActivityDecision({ gameName: "Human Game Title", + gameImageUrl: "https://example.com/game.jpg", kind: "queued", appId: "1001", queuePosition: 12, @@ -107,10 +109,24 @@ test("discordMonitorActivityDecision preserves current game title on queue updat assert.equal(decision.action, "set"); if (decision.action === "set") { assert.equal(decision.activity.gameName, "Human Game Title"); + assert.equal(decision.activity.gameImageUrl, "https://example.com/game.jpg"); assert.equal(decision.activity.queuePosition, 11); } }); +test("isSameDiscordActivity detects newly available game artwork", () => { + assert.equal(isSameDiscordActivity({ + gameName: "Game", + kind: "streaming", + appId: "1001", + }, { + gameName: "Game", + gameImageUrl: "https://example.com/game.jpg", + kind: "streaming", + appId: "1001", + }), false); +}); + test("discordMonitorActivityDecision does not reset active streaming timer", () => { const startedAt = new Date("2026-01-01T00:00:00.000Z"); const decision = discordMonitorActivityDecision({ diff --git a/opennow-stable/src/main/discordPresence.ts b/opennow-stable/src/main/discordPresence.ts index 60584db9e..df2d4b50c 100644 --- a/opennow-stable/src/main/discordPresence.ts +++ b/opennow-stable/src/main/discordPresence.ts @@ -31,6 +31,7 @@ export function discordActivityKindForSession(session: DiscordPresenceSessionSta export function discordActivityFromSession( session: (SessionInfo | ActiveSessionInfo) & DiscordPresenceSessionState, gameName: string, + gameImageUrl?: string, ): DiscordActivityUpdate | null { const kind = discordActivityKindForSession(session); if (!kind) { @@ -40,6 +41,7 @@ export function discordActivityFromSession( const appId = typeof session.appId === "number" ? session.appId.toString() : session.appId; return { gameName, + gameImageUrl, kind, appId, queuePosition: session.queuePosition, @@ -54,6 +56,9 @@ export function isSameDiscordActivity( if (!current || current.kind !== next.kind || current.queuePosition !== next.queuePosition) { return false; } + if (current.gameImageUrl !== next.gameImageUrl) { + return false; + } if (current.appId || next.appId) { return current.appId === next.appId; } @@ -74,7 +79,7 @@ export function discordMonitorActivityDecision( } const gameName = current?.appId === sessionAppId ? current.gameName : sessionAppId; - const nextActivity = discordActivityFromSession(activeSession, gameName); + const nextActivity = discordActivityFromSession(activeSession, gameName, current?.gameImageUrl); if (!nextActivity) { return current?.appId === sessionAppId ? { action: "clear" } : { action: "none" }; } diff --git a/opennow-stable/src/main/discordRpc.test.ts b/opennow-stable/src/main/discordRpc.test.ts new file mode 100644 index 000000000..c96556158 --- /dev/null +++ b/opennow-stable/src/main/discordRpc.test.ts @@ -0,0 +1,36 @@ +/// + +import test from "node:test"; +import assert from "node:assert/strict"; + +import { discordRpcActivityPayload, discordRpcImageUrl } from "./discordRpc"; + +test("discordRpcImageUrl accepts public HTTPS artwork", () => { + assert.equal( + discordRpcImageUrl("https://img.nvidiagrid.net/game.jpg;f=webp;w=1200"), + "https://img.nvidiagrid.net/game.jpg;f=webp;w=1200", + ); +}); + +test("discordRpcImageUrl rejects non-HTTPS and malformed artwork URLs", () => { + assert.equal(discordRpcImageUrl("http://example.com/game.jpg"), undefined); + assert.equal(discordRpcImageUrl("not a URL"), undefined); +}); + +test("discordRpcActivityPayload sends game artwork as the large presence image", () => { + const startedAt = new Date("2026-01-01T00:00:00.000Z"); + assert.deepEqual(discordRpcActivityPayload({ + gameName: "7 Days to Die", + gameImageUrl: "https://example.com/7-days-to-die.jpg", + kind: "streaming", + appId: "1001", + startTimestamp: startedAt, + }), { + details: "7 Days to Die", + state: "Streaming via OpenNow", + startTimestamp: startedAt, + largeImageKey: "https://example.com/7-days-to-die.jpg", + largeImageText: "7 Days to Die", + instance: false, + }); +}); diff --git a/opennow-stable/src/main/discordRpc.ts b/opennow-stable/src/main/discordRpc.ts index c9d2c0d00..2ebfc36e7 100644 --- a/opennow-stable/src/main/discordRpc.ts +++ b/opennow-stable/src/main/discordRpc.ts @@ -10,7 +10,7 @@ const DISCORD_CLIENT_ID = "1479944467112001669"; let rpcClient: Client | null = null; let connected = false; -type DiscordRpcActivity = Omit & { +export type DiscordRpcActivity = Omit & { startTimestamp?: Date; }; @@ -85,6 +85,32 @@ function activityState(activity: DiscordRpcActivity): string { } } +export function discordRpcImageUrl(imageUrl?: string): string | undefined { + const candidate = imageUrl?.trim(); + if (!candidate) { + return undefined; + } + try { + return new URL(candidate).protocol === "https:" ? candidate : undefined; + } catch { + return undefined; + } +} + +export function discordRpcActivityPayload(activity: DiscordRpcActivity) { + const gameImageUrl = discordRpcImageUrl(activity.gameImageUrl); + return { + details: activity.gameName, + state: activityState(activity), + ...(activity.startTimestamp ? { startTimestamp: activity.startTimestamp } : {}), + ...(gameImageUrl ? { + largeImageKey: gameImageUrl, + largeImageText: activity.gameName, + } : {}), + instance: false, + }; +} + export async function setActivity(activity: DiscordRpcActivity): Promise { pendingActivity = activity; @@ -93,15 +119,7 @@ export async function setActivity(activity: DiscordRpcActivity): Promise { } try { - const rpcActivity = { - details: activity.gameName, - state: activityState(activity), - ...(activity.startTimestamp ? { startTimestamp: activity.startTimestamp } : {}), - instance: false, - }; - await rpcClient.setActivity({ - ...rpcActivity, - }); + await rpcClient.setActivity(discordRpcActivityPayload(activity)); lastActivity = pendingActivity; pendingActivity = null; } catch (err) { diff --git a/opennow-stable/src/main/escapeFullscreenGuard.test.ts b/opennow-stable/src/main/escapeFullscreenGuard.test.ts index 2320a3e5f..945275742 100644 --- a/opennow-stable/src/main/escapeFullscreenGuard.test.ts +++ b/opennow-stable/src/main/escapeFullscreenGuard.test.ts @@ -2,9 +2,12 @@ import test from "node:test"; import assert from "node:assert/strict"; import { + ESCAPE_HOLD_TO_EXIT_FULLSCREEN_MS, isEscapeKeyDownInput, + markEscapeHoldFired, nextPointerLockEscapeCaptureUntilMs, POINTER_LOCK_ESCAPE_FULLSCREEN_GRACE_MS, + resolveEscapeHoldCaptureAction, shouldCaptureEscapeFullscreenInput, } from "./escapeFullscreenGuard"; @@ -22,7 +25,9 @@ test("shouldCaptureEscapeFullscreenInput captures Escape while pointer locked", { type: "keyDown", key: "Escape" }, { allowEscapeToExitFullscreen: false, + streamInputActive: true, pointerLockActive: true, + rendererControlledFullscreen: false, windowFullscreen: false, pointerLockEscapeCaptureUntilMs: 0, nowMs: 100, @@ -35,7 +40,9 @@ test("shouldCaptureEscapeFullscreenInput captures rapid Escape presses during fu { type: "keyDown", key: "Escape" }, { allowEscapeToExitFullscreen: false, + streamInputActive: false, pointerLockActive: false, + rendererControlledFullscreen: false, windowFullscreen: true, pointerLockEscapeCaptureUntilMs: 1500, nowMs: 1000, @@ -43,25 +50,61 @@ test("shouldCaptureEscapeFullscreenInput captures rapid Escape presses during fu ), true); }); +test("shouldCaptureEscapeFullscreenInput captures Escape throughout renderer-controlled fullscreen", () => { + assert.equal(shouldCaptureEscapeFullscreenInput( + { type: "keyDown", key: "Escape" }, + { + allowEscapeToExitFullscreen: false, + streamInputActive: true, + pointerLockActive: false, + rendererControlledFullscreen: true, + windowFullscreen: false, + pointerLockEscapeCaptureUntilMs: 0, + nowMs: 10_000, + }, + ), true); +}); + +test("shouldCaptureEscapeFullscreenInput captures Escape for an active stream in native fullscreen", () => { + assert.equal(shouldCaptureEscapeFullscreenInput( + { type: "keyDown", key: "Escape" }, + { + allowEscapeToExitFullscreen: false, + streamInputActive: true, + pointerLockActive: false, + rendererControlledFullscreen: false, + windowFullscreen: true, + pointerLockEscapeCaptureUntilMs: 0, + nowMs: 10_000, + }, + ), true); +}); + test("shouldCaptureEscapeFullscreenInput allows Escape outside protected stream states", () => { const input = { type: "keyDown", key: "Escape" }; assert.equal(shouldCaptureEscapeFullscreenInput(input, { allowEscapeToExitFullscreen: true, + streamInputActive: true, pointerLockActive: true, + rendererControlledFullscreen: true, windowFullscreen: true, pointerLockEscapeCaptureUntilMs: 1500, nowMs: 1000, }), false); assert.equal(shouldCaptureEscapeFullscreenInput(input, { allowEscapeToExitFullscreen: false, + streamInputActive: false, pointerLockActive: false, + rendererControlledFullscreen: false, windowFullscreen: true, pointerLockEscapeCaptureUntilMs: 999, nowMs: 1000, }), false); assert.equal(shouldCaptureEscapeFullscreenInput(input, { allowEscapeToExitFullscreen: false, + streamInputActive: false, pointerLockActive: false, + rendererControlledFullscreen: false, windowFullscreen: false, pointerLockEscapeCaptureUntilMs: 1500, nowMs: 1000, @@ -76,3 +119,56 @@ test("nextPointerLockEscapeCaptureUntilMs only arms grace for unsuppressed point 1000 + POINTER_LOCK_ESCAPE_FULLSCREEN_GRACE_MS, ); }); + +test("resolveEscapeHoldCaptureAction arms hold then taps on early keyup", () => { + const guard = { + allowEscapeToExitFullscreen: false, + streamInputActive: true, + pointerLockActive: true, + rendererControlledFullscreen: true, + windowFullscreen: true, + pointerLockEscapeCaptureUntilMs: 0, + nowMs: 1000, + }; + const armed = resolveEscapeHoldCaptureAction( + { type: "keyDown", key: "Escape" }, + guard, + { keyDownCaptured: false, holdFired: false }, + ); + assert.equal(armed.action, "arm-hold"); + assert.equal(ESCAPE_HOLD_TO_EXIT_FULLSCREEN_MS, 1500); + + const tap = resolveEscapeHoldCaptureAction( + { type: "keyUp", key: "Escape" }, + guard, + armed.nextHoldState, + ); + assert.equal(tap.action, "tap"); + assert.deepEqual(tap.nextHoldState, { keyDownCaptured: false, holdFired: false }); +}); + +test("resolveEscapeHoldCaptureAction suppresses tap after hold fires", () => { + const guard = { + allowEscapeToExitFullscreen: false, + streamInputActive: true, + pointerLockActive: true, + rendererControlledFullscreen: true, + windowFullscreen: true, + pointerLockEscapeCaptureUntilMs: 0, + nowMs: 1000, + }; + const armed = resolveEscapeHoldCaptureAction( + { type: "keyDown", key: "Escape" }, + guard, + { keyDownCaptured: false, holdFired: false }, + ); + const held = markEscapeHoldFired(armed.nextHoldState); + assert.equal(held.holdFired, true); + + const keyup = resolveEscapeHoldCaptureAction( + { type: "keyUp", key: "Escape" }, + guard, + held, + ); + assert.equal(keyup.action, "hold-consumed-keyup"); +}); diff --git a/opennow-stable/src/main/escapeFullscreenGuard.ts b/opennow-stable/src/main/escapeFullscreenGuard.ts index cbba3e5dc..ae704b29b 100644 --- a/opennow-stable/src/main/escapeFullscreenGuard.ts +++ b/opennow-stable/src/main/escapeFullscreenGuard.ts @@ -1,22 +1,39 @@ export const POINTER_LOCK_ESCAPE_FULLSCREEN_GRACE_MS = 1000; +/** Match native Internal Escape-hold timing (gstreamer_platform.rs). */ +export const ESCAPE_HOLD_TO_EXIT_FULLSCREEN_MS = 1500; export interface EscapeKeyInput { type?: string; key?: string; code?: string; keyCode?: number; + isAutoRepeat?: boolean; } export interface EscapeFullscreenGuardState { allowEscapeToExitFullscreen: boolean; + streamInputActive: boolean; pointerLockActive: boolean; + rendererControlledFullscreen: boolean; windowFullscreen: boolean; pointerLockEscapeCaptureUntilMs: number; nowMs: number; } -export function isEscapeKeyDownInput(input: EscapeKeyInput): boolean { - return input.type === "keyDown" && ( +export type EscapeHoldCaptureAction = + | "ignore" + | "arm-hold" + | "hold-repeat" + | "tap" + | "hold-consumed-keyup"; + +export interface EscapeHoldCaptureState { + keyDownCaptured: boolean; + holdFired: boolean; +} + +export function isEscapeKeyInput(input: EscapeKeyInput): boolean { + return ( input.key === "Escape" || input.key === "Esc" || input.code === "Escape" || @@ -24,6 +41,14 @@ export function isEscapeKeyDownInput(input: EscapeKeyInput): boolean { ); } +export function isEscapeKeyDownInput(input: EscapeKeyInput): boolean { + return input.type === "keyDown" && isEscapeKeyInput(input); +} + +export function isEscapeKeyUpInput(input: EscapeKeyInput): boolean { + return input.type === "keyUp" && isEscapeKeyInput(input); +} + export function shouldCaptureEscapeFullscreenInput( input: EscapeKeyInput, state: EscapeFullscreenGuardState, @@ -36,9 +61,77 @@ export function shouldCaptureEscapeFullscreenInput( return true; } + // Electron's fullscreen state can lag the renderer IPC request. Protect both + // signals, but only while a stream input route is actually active. + if ( + state.streamInputActive + && (state.windowFullscreen || state.rendererControlledFullscreen) + ) { + return true; + } + return state.windowFullscreen && state.nowMs <= state.pointerLockEscapeCaptureUntilMs; } +/** + * Electron Internal/web path: Escape tap → game, hold → exit fullscreen. + * Mirrors native Internal RawInput hold timing without sending a tap after hold. + */ +export function resolveEscapeHoldCaptureAction( + input: EscapeKeyInput, + guardState: EscapeFullscreenGuardState, + holdState: EscapeHoldCaptureState, +): { action: EscapeHoldCaptureAction; nextHoldState: EscapeHoldCaptureState } { + if (guardState.allowEscapeToExitFullscreen || !isEscapeKeyInput(input)) { + return { + action: "ignore", + nextHoldState: { keyDownCaptured: false, holdFired: false }, + }; + } + + if (isEscapeKeyDownInput(input)) { + if (!shouldCaptureEscapeFullscreenInput(input, guardState) && !holdState.keyDownCaptured) { + return { action: "ignore", nextHoldState: holdState }; + } + + if (holdState.keyDownCaptured || input.isAutoRepeat) { + return { + action: "hold-repeat", + nextHoldState: { keyDownCaptured: true, holdFired: holdState.holdFired }, + }; + } + + return { + action: "arm-hold", + nextHoldState: { keyDownCaptured: true, holdFired: false }, + }; + } + + if (isEscapeKeyUpInput(input) && holdState.keyDownCaptured) { + if (holdState.holdFired) { + return { + action: "hold-consumed-keyup", + nextHoldState: { keyDownCaptured: false, holdFired: false }, + }; + } + return { + action: "tap", + nextHoldState: { keyDownCaptured: false, holdFired: false }, + }; + } + + return { action: "ignore", nextHoldState: holdState }; +} + +export function markEscapeHoldFired( + holdState: EscapeHoldCaptureState, +): EscapeHoldCaptureState { + if (!holdState.keyDownCaptured) { + return holdState; + } + return { keyDownCaptured: true, holdFired: true }; +} + export function nextPointerLockEscapeCaptureUntilMs( active: boolean, suppressEscapeFullscreenGrace: boolean, diff --git a/opennow-stable/src/main/gfn/auth.ts b/opennow-stable/src/main/gfn/auth.ts deleted file mode 100644 index 01eb132d0..000000000 --- a/opennow-stable/src/main/gfn/auth.ts +++ /dev/null @@ -1,1426 +0,0 @@ -import { createServer } from "node:http"; -import { createHash, randomBytes } from "node:crypto"; -import { access, mkdir, readFile, writeFile } from "node:fs/promises"; -import { dirname } from "node:path"; -import net from "node:net"; -import os from "node:os"; - -import { shell } from "electron"; - -import type { - AuthLoginRequest, - AuthDeviceLoginAttemptRequest, - AuthDeviceLoginChallenge, - AuthDeviceLoginPollRequest, - AuthDeviceLoginPollResult, - AuthDeviceLoginStartRequest, - AuthSession, - AuthSessionResult, - AuthTokens, - AuthUser, - LoginProvider, - SavedAccount, - StreamRegion, - SubscriptionInfo, -} from "@shared/gfn"; -import { - buildGfnLcarsHeaders, - buildNvidiaAuthHeaders, - GFN_USER_AGENT, -} from "./clientHeaders"; -import { fetchSubscription, fetchDynamicRegions } from "./subscription"; - -const SERVICE_URLS_ENDPOINT = "https://pcs.geforcenow.com/v1/serviceUrls"; -const TOKEN_ENDPOINT = "https://login.nvidia.com/token"; -const CLIENT_TOKEN_ENDPOINT = "https://login.nvidia.com/client_token"; -const USERINFO_ENDPOINT = "https://login.nvidia.com/userinfo"; -const AUTH_ENDPOINT = "https://login.nvidia.com/authorize"; -const DEVICE_AUTHORIZE_ENDPOINT = "https://login.nvidia.com/device/authorize"; - -const CLIENT_ID = "ZU7sPN-miLujMD95LfOQ453IB0AtjM8sMyvgJ9wCXEQ"; -const STEAM_DECK_CLIENT_ID = "q61ddeJrVt7O90Nl-P-N7I36yctih4Ml6FyXLrb6j-U"; -const SCOPES = "openid consent email tk_client age"; -const DEFAULT_IDP_ID = "PDiAhv2kJTFeQ7WOPqiQ2tRZ7lGhR2X11dXvM4TZSxg"; -const STEAM_DECK_USER_AGENT = - "Mozilla/5.0 (X11; Linux x86_64; Steam Deck) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36"; - -const REDIRECT_PORTS = [2259, 6460, 7119, 8870, 9096]; -const TOKEN_REFRESH_WINDOW_MS = 10 * 60 * 1000; -const CLIENT_TOKEN_REFRESH_WINDOW_MS = 5 * 60 * 1000; - -interface PersistedAuthState { - sessions: AuthSession[]; - activeUserId: string | null; - selectedProvider: LoginProvider | null; -} - -interface ServiceUrlsResponse { - requestStatus?: { - statusCode?: number; - }; - gfnServiceInfo?: { - gfnServiceEndpoints?: Array<{ - idpId: string; - loginProviderCode: string; - loginProviderDisplayName: string; - streamingServiceUrl: string; - loginProviderPriority?: number; - }>; - }; -} - -interface TokenResponse { - access_token: string; - refresh_token?: string; - id_token?: string; - client_token?: string; - expires_in?: number; -} - -interface ClientTokenResponse { - client_token: string; - expires_in?: number; -} - -interface DeviceAuthorizationResponse { - device_code?: string; - user_code?: string; - verification_uri?: string; - verification_uri_complete?: string; - expires_in?: number; - interval?: number; -} - -interface DeviceTokenErrorResponse { - error?: string; - error_description?: string; -} - -interface ServerInfoResponse { - requestStatus?: { - serverId?: string; - }; - metaData?: Array<{ - key: string; - value: string; - }>; -} - -interface DeviceLoginAttempt { - provider: LoginProvider; - deviceCode: string; - expiresAt: number; -} - -function defaultProvider(): LoginProvider { - return { - idpId: DEFAULT_IDP_ID, - code: "NVIDIA", - displayName: "NVIDIA", - streamingServiceUrl: "https://prod.cloudmatchbeta.nvidiagrid.net/", - priority: 0, - }; -} - -function normalizeProvider(provider: LoginProvider): LoginProvider { - return { - ...provider, - streamingServiceUrl: provider.streamingServiceUrl.endsWith("/") - ? provider.streamingServiceUrl - : `${provider.streamingServiceUrl}/`, - }; -} - -function decodeBase64Url(value: string): string { - const normalized = value.replace(/-/g, "+").replace(/_/g, "/"); - const padding = normalized.length % 4; - const padded = padding === 0 ? normalized : `${normalized}${"=".repeat(4 - padding)}`; - return Buffer.from(padded, "base64").toString("utf8"); -} - -function parseJwtPayload(token: string): T | null { - const parts = token.split("."); - if (parts.length !== 3) { - return null; - } - try { - const payload = decodeBase64Url(parts[1]); - return JSON.parse(payload) as T; - } catch { - return null; - } -} - -function toExpiresAt(expiresInSeconds: number | undefined, defaultSeconds = 86400): number { - return Date.now() + (expiresInSeconds ?? defaultSeconds) * 1000; -} - -function isExpired(expiresAt: number | undefined): boolean { - if (!expiresAt) { - return true; - } - return expiresAt <= Date.now(); -} - -function isNearExpiry(expiresAt: number | undefined, windowMs: number): boolean { - if (!expiresAt) { - return true; - } - return expiresAt - Date.now() < windowMs; -} - -function generateDeviceId(): string { - const host = os.hostname(); - const username = os.userInfo().username; - return createHash("sha256").update(`${host}:${username}:opennow-stable`).digest("hex"); -} - -function generatePkce(): { verifier: string; challenge: string } { - const verifier = randomBytes(64) - .toString("base64") - .replace(/\+/g, "-") - .replace(/\//g, "_") - .replace(/=+$/g, "") - .slice(0, 86); - - const challenge = createHash("sha256") - .update(verifier) - .digest("base64") - .replace(/\+/g, "-") - .replace(/\//g, "_") - .replace(/=+$/g, ""); - - return { verifier, challenge }; -} - -function buildAuthHeadersForClient( - authClientId = CLIENT_ID, - options: { - bearerToken?: string; - accept?: string; - contentType?: string; - includeReferer?: boolean; - } = {}, -): Record { - if (authClientId !== STEAM_DECK_CLIENT_ID) { - return buildNvidiaAuthHeaders(options); - } - - const headers: Record = { - Accept: options.accept ?? "application/json, text/plain, */*", - Origin: "https://play.geforcenow.com", - Referer: "https://play.geforcenow.com/", - "User-Agent": STEAM_DECK_USER_AGENT, - }; - - if (options.bearerToken !== undefined) { - headers.Authorization = `Bearer ${options.bearerToken}`; - } - if (options.contentType) { - headers["Content-Type"] = options.contentType; - } - - return headers; -} - -function buildAuthUrl(provider: LoginProvider, challenge: string, port: number): string { - const redirectUri = `http://localhost:${port}`; - const nonce = randomBytes(16).toString("hex"); - const params = new URLSearchParams({ - response_type: "code", - device_id: generateDeviceId(), - scope: SCOPES, - client_id: CLIENT_ID, - redirect_uri: redirectUri, - ui_locales: "en_US", - nonce, - prompt: "select_account", - code_challenge: challenge, - code_challenge_method: "S256", - idp_id: provider.idpId, - }); - return `${AUTH_ENDPOINT}?${params.toString()}`; -} - -async function isPortAvailable(port: number): Promise { - return new Promise((resolve) => { - const server = net.createServer(); - server.once("error", () => resolve(false)); - server.once("listening", () => { - server.close(() => resolve(true)); - }); - server.listen(port, "127.0.0.1"); - }); -} - -async function findAvailablePort(): Promise { - for (const port of REDIRECT_PORTS) { - if (await isPortAvailable(port)) { - return port; - } - } - - throw new Error("No available OAuth callback ports"); -} - -async function waitForAuthorizationCode(port: number, timeoutMs: number): Promise { - return new Promise((resolve, reject) => { - const server = createServer((request, response) => { - const url = new URL(request.url ?? "/", `http://localhost:${port}`); - const code = url.searchParams.get("code"); - const error = url.searchParams.get("error"); - - const html = `OpenNOW Login

OpenNOW Login

${ - code - ? "Login complete. You can close this window and return to OpenNOW Stable." - : "Login failed or was cancelled. You can close this window and return to OpenNOW Stable." - }

`; - - response.statusCode = 200; - response.setHeader("Content-Type", "text/html; charset=utf-8"); - response.end(html); - - server.close(() => { - if (code) { - resolve(code); - return; - } - reject(new Error(error ?? "Authorization failed")); - }); - }); - - server.listen(port, "127.0.0.1", () => { - const timer = setTimeout(() => { - server.close(() => reject(new Error("Timed out waiting for OAuth callback"))); - }, timeoutMs); - - server.once("close", () => clearTimeout(timer)); - }); - }); -} - -async function exchangeAuthorizationCode(code: string, verifier: string, port: number): Promise { - const body = new URLSearchParams({ - grant_type: "authorization_code", - code, - redirect_uri: `http://localhost:${port}`, - code_verifier: verifier, - }); - - const response = await fetch(TOKEN_ENDPOINT, { - method: "POST", - headers: buildAuthHeadersForClient(CLIENT_ID, { - contentType: "application/x-www-form-urlencoded; charset=UTF-8", - includeReferer: true, - }), - body, - }); - - if (!response.ok) { - const text = await response.text(); - throw new Error(`Token exchange failed (${response.status}): ${text.slice(0, 400)}`); - } - - const payload = (await response.json()) as TokenResponse; - return { - accessToken: payload.access_token, - refreshToken: payload.refresh_token, - idToken: payload.id_token, - expiresAt: toExpiresAt(payload.expires_in), - authClientId: CLIENT_ID, - }; -} - -async function requestDeviceAuthorization( - provider: LoginProvider, -): Promise> { - const deviceId = generateDeviceId(); - const body = new URLSearchParams({ - client_id: STEAM_DECK_CLIENT_ID, - scope: SCOPES, - device_id: deviceId, - display_name: "OpenNOW", - idp_id: provider.idpId, - }); - - const response = await fetch(DEVICE_AUTHORIZE_ENDPOINT, { - method: "POST", - headers: { - ...buildAuthHeadersForClient(STEAM_DECK_CLIENT_ID, { - contentType: "application/x-www-form-urlencoded; charset=UTF-8", - }), - "x-device-id": deviceId, - "nv-client-id": STEAM_DECK_CLIENT_ID, - "nv-client-streamer": "WEBRTC", - "nv-client-type": "BROWSER", - "nv-client-platform-name": "browser", - "nv-browser-type": "CHROME", - "nv-device-os": "STEAMOS", - "nv-device-type": "CONSOLE", - "nv-device-model": "STEAMDECK", - "nv-device-make": "VALVE", - }, - body, - }); - - if (!response.ok) { - const text = await response.text(); - throw new Error(`Device authorization failed (${response.status}): ${text.slice(0, 400)}`); - } - - const payload = (await response.json()) as DeviceAuthorizationResponse; - if ( - !payload.device_code || - !payload.user_code || - !payload.verification_uri || - !payload.verification_uri_complete - ) { - throw new Error("Device authorization response did not include QR login data"); - } - - return { - deviceCode: payload.device_code, - userCode: payload.user_code, - verificationUri: payload.verification_uri, - verificationUriComplete: payload.verification_uri_complete, - expiresAt: toExpiresAt(payload.expires_in, 600), - intervalSeconds: Math.max(1, payload.interval ?? 5), - }; -} - -async function exchangeDeviceCode(deviceCode: string): Promise { - const body = new URLSearchParams({ - grant_type: "urn:ietf:params:oauth:grant-type:device_code", - device_code: deviceCode, - client_id: STEAM_DECK_CLIENT_ID, - }); - - const response = await fetch(TOKEN_ENDPOINT, { - method: "POST", - headers: buildAuthHeadersForClient(STEAM_DECK_CLIENT_ID, { - contentType: "application/x-www-form-urlencoded; charset=UTF-8", - }), - body, - }); - - const payload = (await response.json().catch(() => null)) as TokenResponse | DeviceTokenErrorResponse | null; - if (!response.ok) { - return payload && typeof payload === "object" - ? payload as DeviceTokenErrorResponse - : { error: "device_token_exchange_failed", error_description: `Device token exchange failed (${response.status})` }; - } - - const tokenPayload = payload as TokenResponse | null; - if (!tokenPayload?.access_token) { - return { error: "invalid_token_response", error_description: "Device token response did not include access_token" }; - } - - return { - accessToken: tokenPayload.access_token, - refreshToken: tokenPayload.refresh_token, - idToken: tokenPayload.id_token, - expiresAt: toExpiresAt(tokenPayload.expires_in), - authClientId: STEAM_DECK_CLIENT_ID, - clientToken: tokenPayload.client_token, - }; -} - -async function refreshAuthTokens(refreshToken: string, authClientId = CLIENT_ID): Promise { - const body = new URLSearchParams({ - grant_type: "refresh_token", - refresh_token: refreshToken, - client_id: authClientId, - }); - - const response = await fetch(TOKEN_ENDPOINT, { - method: "POST", - headers: buildAuthHeadersForClient(authClientId, { - contentType: "application/x-www-form-urlencoded; charset=UTF-8", - }), - body, - }); - - if (!response.ok) { - const text = await response.text(); - throw new Error(`Token refresh failed (${response.status}): ${text.slice(0, 400)}`); - } - - const payload = (await response.json()) as TokenResponse; - return { - accessToken: payload.access_token, - refreshToken: payload.refresh_token ?? refreshToken, - idToken: payload.id_token, - expiresAt: toExpiresAt(payload.expires_in), - authClientId, - }; -} - -async function requestClientToken(accessToken: string, authClientId = CLIENT_ID): Promise<{ - token: string; - expiresAt: number; - lifetimeMs: number; -}> { - const response = await fetch(CLIENT_TOKEN_ENDPOINT, { - headers: buildAuthHeadersForClient(authClientId, { bearerToken: accessToken }), - }); - - if (!response.ok) { - const text = await response.text(); - throw new Error(`Client token request failed (${response.status}): ${text.slice(0, 400)}`); - } - - const payload = (await response.json()) as ClientTokenResponse; - const expiresAt = toExpiresAt(payload.expires_in); - return { - token: payload.client_token, - expiresAt, - lifetimeMs: Math.max(0, expiresAt - Date.now()), - }; -} - -async function refreshWithClientToken(clientToken: string, userId: string, authClientId = CLIENT_ID): Promise { - const body = new URLSearchParams({ - grant_type: "urn:ietf:params:oauth:grant-type:client_token", - client_token: clientToken, - client_id: authClientId, - sub: userId, - }); - - const response = await fetch(TOKEN_ENDPOINT, { - method: "POST", - headers: buildAuthHeadersForClient(authClientId, { - contentType: "application/x-www-form-urlencoded; charset=UTF-8", - }), - body, - }); - - if (!response.ok) { - const text = await response.text(); - throw new Error(`Client-token refresh failed (${response.status}): ${text.slice(0, 400)}`); - } - - return (await response.json()) as TokenResponse; -} - -function mergeTokenSnapshot(base: AuthTokens, refreshed: TokenResponse): AuthTokens { - return { - accessToken: refreshed.access_token, - refreshToken: refreshed.refresh_token ?? base.refreshToken, - idToken: refreshed.id_token, - expiresAt: toExpiresAt(refreshed.expires_in), - authClientId: base.authClientId ?? CLIENT_ID, - clientToken: refreshed.client_token ?? base.clientToken, - clientTokenExpiresAt: base.clientTokenExpiresAt, - clientTokenLifetimeMs: base.clientTokenLifetimeMs, - }; -} - -function gravatarUrl(email: string, size = 80): string { - const normalized = email.trim().toLowerCase(); - const hash = createHash("md5").update(normalized).digest("hex"); - return `https://www.gravatar.com/avatar/${hash}?s=${size}&d=identicon`; -} - -async function fetchUserInfo(tokens: AuthTokens): Promise { - const jwtToken = tokens.idToken ?? tokens.accessToken; - const parsed = parseJwtPayload<{ - sub?: string; - email?: string; - preferred_username?: string; - gfn_tier?: string; - picture?: string; - }>(jwtToken); - - if (parsed?.sub) { - const emailFromToken = parsed.email; - const pictureFromToken = parsed.picture; - if (emailFromToken || pictureFromToken) { - const avatar = pictureFromToken ?? (emailFromToken ? gravatarUrl(emailFromToken) : undefined); - return { - userId: parsed.sub, - displayName: parsed.preferred_username ?? emailFromToken?.split("@")[0] ?? "User", - email: emailFromToken, - avatarUrl: avatar, - membershipTier: parsed.gfn_tier ?? "FREE", - }; - } - } - - const response = await fetch(USERINFO_ENDPOINT, { - headers: buildAuthHeadersForClient(tokens.authClientId, { - bearerToken: tokens.accessToken, - accept: "application/json", - }), - }); - - if (!response.ok) { - throw new Error(`User info failed (${response.status})`); - } - - const payload = (await response.json()) as { - sub: string; - preferred_username?: string; - email?: string; - picture?: string; - }; - - const email = payload.email; - const avatar = payload.picture ?? (email ? gravatarUrl(email) : undefined); - - return { - userId: payload.sub, - displayName: payload.preferred_username ?? email?.split("@")[0] ?? "User", - email, - avatarUrl: avatar, - membershipTier: "FREE", - }; -} - -export class AuthService { - private providers: LoginProvider[] = []; - private sessions = new Map(); - private activeUserId: string | null = null; - private selectedProvider: LoginProvider = defaultProvider(); - private cachedSubscription: SubscriptionInfo | null = null; - private cachedVpcId: string | null = null; - private deviceLoginAttempts = new Map(); - private pendingDeviceLoginSessions = new Map(); - - constructor(private readonly statePath: string) {} - - async initialize(): Promise { - try { - await access(this.statePath); - } catch { - await mkdir(dirname(this.statePath), { recursive: true }); - await this.persist(); - return; - } - - try { - const raw = await readFile(this.statePath, "utf8"); - const parsed = JSON.parse(raw) as Partial & { - session?: AuthSession | null; - }; - if (parsed.selectedProvider) { - this.selectedProvider = normalizeProvider(parsed.selectedProvider); - } - - this.sessions.clear(); - if (Array.isArray(parsed.sessions)) { - for (const persistedSession of parsed.sessions) { - if (!persistedSession?.user?.userId) { - continue; - } - this.sessions.set(persistedSession.user.userId, { - ...persistedSession, - provider: normalizeProvider(persistedSession.provider), - }); - } - } else if (parsed.session?.user?.userId) { - this.sessions.set(parsed.session.user.userId, { - ...parsed.session, - provider: normalizeProvider(parsed.session.provider), - }); - } - - if (typeof parsed.activeUserId === "string" && this.sessions.has(parsed.activeUserId)) { - this.activeUserId = parsed.activeUserId; - } else { - this.activeUserId = this.sessions.keys().next().value ?? null; - } - - const restoredSession = this.getSession(); - if (restoredSession) { - this.selectedProvider = restoredSession.provider; - await this.enrichUserTier(); - await this.persist(); - } - } catch { - this.sessions.clear(); - this.activeUserId = null; - this.selectedProvider = defaultProvider(); - await this.persist(); - } - } - - private async persist(): Promise { - const payload: PersistedAuthState = { - sessions: Array.from(this.sessions.values()), - activeUserId: this.activeUserId, - selectedProvider: this.selectedProvider, - }; - - await mkdir(dirname(this.statePath), { recursive: true }); - await writeFile(this.statePath, JSON.stringify(payload, null, 2), "utf8"); - } - - private async ensureClientToken(tokens: AuthTokens, userId: string): Promise { - const hasUsableClientToken = - Boolean(tokens.clientToken) && - !isNearExpiry(tokens.clientTokenExpiresAt, CLIENT_TOKEN_REFRESH_WINDOW_MS); - if (hasUsableClientToken) { - return tokens; - } - - if (isExpired(tokens.expiresAt)) { - return tokens; - } - - const clientToken = await requestClientToken(tokens.accessToken, tokens.authClientId); - return { - ...tokens, - clientToken: clientToken.token, - clientTokenExpiresAt: clientToken.expiresAt, - clientTokenLifetimeMs: clientToken.lifetimeMs, - }; - } - - async getProviders(): Promise { - if (this.providers.length > 0) { - return this.providers; - } - - let response: Response; - try { - response = await fetch(SERVICE_URLS_ENDPOINT, { - headers: { - Accept: "application/json", - "User-Agent": GFN_USER_AGENT, - }, - }); - } catch (error) { - console.warn("Failed to fetch providers, using default:", error); - this.providers = [defaultProvider()]; - return this.providers; - } - - if (!response.ok) { - console.warn(`Providers fetch failed with status ${response.status}, using default`); - this.providers = [defaultProvider()]; - return this.providers; - } - - try { - const payload = (await response.json()) as ServiceUrlsResponse; - const endpoints = payload.gfnServiceInfo?.gfnServiceEndpoints ?? []; - - const providers = endpoints - .map((entry) => ({ - idpId: entry.idpId, - code: entry.loginProviderCode, - displayName: - entry.loginProviderCode === "BPC" ? "bro.game" : entry.loginProviderDisplayName, - streamingServiceUrl: entry.streamingServiceUrl, - priority: entry.loginProviderPriority ?? 0, - })) - .sort((a, b) => a.priority - b.priority) - .map(normalizeProvider); - - this.providers = providers.length > 0 ? providers : [defaultProvider()]; - console.log(`Loaded ${this.providers.length} providers`); - return this.providers; - } catch (error) { - console.warn("Failed to parse providers response, using default:", error); - this.providers = [defaultProvider()]; - return this.providers; - } - } - - setSession(session: AuthSession | null): void { - if (!session) { - this.sessions.clear(); - this.activeUserId = null; - this.selectedProvider = defaultProvider(); - this.clearSubscriptionCache(); - this.clearVpcCache(); - void this.persist(); - return; - } - - const normalized: AuthSession = { - ...session, - provider: normalizeProvider(session.provider), - }; - this.sessions.set(normalized.user.userId, normalized); - this.activeUserId = normalized.user.userId; - this.selectedProvider = normalized.provider; - this.clearSubscriptionCache(); - this.clearVpcCache(); - void this.persist(); - } - - getSession(): AuthSession | null { - if (!this.activeUserId) { - return null; - } - return this.sessions.get(this.activeUserId) ?? null; - } - - private setActiveAccount(userId: string | null): void { - this.activeUserId = userId && this.sessions.has(userId) ? userId : null; - this.selectedProvider = this.getSession()?.provider ?? defaultProvider(); - this.clearSubscriptionCache(); - this.clearVpcCache(); - } - - getSavedAccounts(): SavedAccount[] { - return Array.from(this.sessions.values()).map((session) => ({ - userId: session.user.userId, - displayName: session.user.displayName, - email: session.user.email, - avatarUrl: session.user.avatarUrl, - membershipTier: session.user.membershipTier, - providerCode: session.provider.code, - })); - } - - async switchAccount(userId: string): Promise { - const target = this.sessions.get(userId); - if (!target) { - throw new Error("Saved account not found"); - } - - const previousActiveUserId = this.activeUserId; - const previousSelectedProvider = this.selectedProvider; - - this.activeUserId = userId; - this.selectedProvider = target.provider; - this.clearSubscriptionCache(); - this.clearVpcCache(); - - const result = await this.ensureValidSessionWithStatus(true, userId); - const missingRefreshToken = result.refresh.outcome === "missing_refresh_token"; - const refreshFailed = result.refresh.outcome === "failed"; - const switchedUserMismatch = result.session?.user.userId !== userId; - if (!result.session || refreshFailed || missingRefreshToken || switchedUserMismatch) { - const fallbackMessage = "Failed to switch account due to an invalid or expired session."; - - if (missingRefreshToken) { - await this.removeAccount(userId); - this.setActiveAccount(previousActiveUserId); - await this.persist(); - throw new Error("Saved login for this account is incomplete. Please log in to this account again."); - } - - this.activeUserId = previousActiveUserId; - this.selectedProvider = previousActiveUserId && this.sessions.has(previousActiveUserId) - ? previousSelectedProvider - : this.getSession()?.provider ?? defaultProvider(); - this.clearSubscriptionCache(); - this.clearVpcCache(); - await this.persist(); - - if (switchedUserMismatch) { - throw new Error("Switched session did not match the selected account."); - } - throw new Error(result.refresh.message || fallbackMessage); - } - return result.session; - } - - async removeAccount(userId: string): Promise { - const removed = this.sessions.delete(userId); - if (!removed) { - return; - } - if (this.activeUserId === userId) { - this.setActiveAccount(this.sessions.keys().next().value ?? null); - } else { - this.clearSubscriptionCache(); - this.clearVpcCache(); - } - await this.persist(); - } - - async logoutAll(): Promise { - this.sessions.clear(); - this.activeUserId = null; - this.selectedProvider = defaultProvider(); - this.cachedSubscription = null; - this.clearVpcCache(); - await this.persist(); - } - - getSelectedProvider(): LoginProvider { - return this.getSession()?.provider ?? this.selectedProvider; - } - - private async selectLoginProvider(providerIdpId?: string): Promise { - const providers = await this.getProviders(); - const selected = - providers.find((provider) => provider.idpId === providerIdpId) ?? - this.selectedProvider ?? - providers[0] ?? - defaultProvider(); - this.selectedProvider = normalizeProvider(selected); - return this.selectedProvider; - } - - private async buildLoginSession(initialTokens: AuthTokens, provider: LoginProvider): Promise { - const user = await fetchUserInfo(initialTokens); - console.debug("auth: fetched user info during login", { userId: user.userId, email: user.email, avatarUrl: user.avatarUrl }); - let tokens = initialTokens; - try { - tokens = await this.ensureClientToken(initialTokens, user.userId); - } catch (error) { - console.warn("Unable to fetch client token after login. Falling back to OAuth token only:", error); - } - - return { - provider: normalizeProvider(provider), - tokens, - user, - }; - } - - private async saveLoginSession(session: AuthSession): Promise { - this.sessions.set(session.user.userId, session); - this.activeUserId = session.user.userId; - this.selectedProvider = session.provider; - this.clearSubscriptionCache(); - this.clearVpcCache(); - - // Fetch real membership tier from MES subscription API - // (JWT does not contain gfn_tier, so fetchUserInfo always falls back to "FREE") - await this.enrichUserTier(); - - await this.persist(); - return this.getSession() as AuthSession; - } - - private pruneExpiredDeviceLogins(now = Date.now(), skipAttemptId?: string): void { - for (const [attemptId, attempt] of this.deviceLoginAttempts) { - if (attemptId === skipAttemptId) { - continue; - } - if (attempt.expiresAt <= now) { - this.deviceLoginAttempts.delete(attemptId); - this.pendingDeviceLoginSessions.delete(attemptId); - } - } - } - - async getRegions(explicitToken?: string): Promise { - const provider = this.getSelectedProvider(); - const base = provider.streamingServiceUrl.endsWith("/") - ? provider.streamingServiceUrl - : `${provider.streamingServiceUrl}/`; - - let token = explicitToken; - if (!token) { - const session = await this.ensureValidSession(); - token = session ? session.tokens.idToken ?? session.tokens.accessToken : undefined; - } - - const headers = buildGfnLcarsHeaders({ - token, - clientType: "BROWSER", - clientStreamer: "WEBRTC", - includeUserAgent: true, - }); - - let response: Response; - try { - response = await fetch(`${base}v2/serverInfo`, { - headers, - }); - } catch { - return []; - } - - if (!response.ok) { - return []; - } - - const payload = (await response.json()) as ServerInfoResponse; - const regions = (payload.metaData ?? []) - .filter((entry) => entry.value.startsWith("https://")) - .filter((entry) => entry.key !== "gfn-regions" && !entry.key.startsWith("gfn-")) - .map((entry) => ({ - name: entry.key, - url: entry.value.endsWith("/") ? entry.value : `${entry.value}/`, - })) - .sort((a, b) => a.name.localeCompare(b.name)); - - return regions; - } - - async login(input: AuthLoginRequest): Promise { - const provider = await this.selectLoginProvider(input.providerIdpId); - - const { verifier, challenge } = generatePkce(); - const port = await findAvailablePort(); - const authUrl = buildAuthUrl(provider, challenge, port); - - const codePromise = waitForAuthorizationCode(port, 120000); - await shell.openExternal(authUrl); - const code = await codePromise; - - const initialTokens = await exchangeAuthorizationCode(code, verifier, port); - const session = await this.buildLoginSession(initialTokens, provider); - return this.saveLoginSession(session); - } - - async startDeviceLogin(input: AuthDeviceLoginStartRequest): Promise { - this.pruneExpiredDeviceLogins(); - const provider = await this.selectLoginProvider(input.providerIdpId); - const challenge = await requestDeviceAuthorization(provider); - const attemptId = randomBytes(16).toString("hex"); - this.deviceLoginAttempts.set(attemptId, { - provider, - deviceCode: challenge.deviceCode, - expiresAt: challenge.expiresAt, - }); - return { ...challenge, attemptId }; - } - - async pollDeviceLogin(input: AuthDeviceLoginPollRequest): Promise { - this.pruneExpiredDeviceLogins(); - if (!input.attemptId || !input.deviceCode) { - return { status: "error", error: "Missing device code" }; - } - - const attempt = this.deviceLoginAttempts.get(input.attemptId); - if (!attempt || attempt.deviceCode !== input.deviceCode) { - return { status: "expired", error: "QR login was cancelled or expired" }; - } - if (Date.now() >= attempt.expiresAt) { - this.cancelDeviceLogin(input); - return { status: "expired", error: "QR login expired" }; - } - - const result = await exchangeDeviceCode(input.deviceCode); - if (!this.deviceLoginAttempts.has(input.attemptId)) { - return { status: "expired", error: "QR login was cancelled" }; - } - - if ("accessToken" in result) { - const session = await this.buildLoginSession(result, attempt.provider); - if (!this.deviceLoginAttempts.has(input.attemptId)) { - return { status: "expired", error: "QR login was cancelled" }; - } - this.pendingDeviceLoginSessions.set(input.attemptId, session); - return { status: "authorized" }; - } - - switch (result.error) { - case "authorization_pending": - return { status: "pending", error: result.error_description }; - case "slow_down": - return { status: "slow_down", error: result.error_description }; - case "expired_token": - this.cancelDeviceLogin(input); - return { status: "expired", error: result.error_description ?? "QR login expired" }; - case "access_denied": - this.cancelDeviceLogin(input); - return { status: "access_denied", error: result.error_description ?? "QR login was denied" }; - default: - this.cancelDeviceLogin(input); - return { status: "error", error: result.error_description ?? result.error ?? "QR login failed" }; - } - } - - async completeDeviceLogin(input: AuthDeviceLoginAttemptRequest): Promise { - this.pruneExpiredDeviceLogins(Date.now(), input.attemptId); - const session = this.pendingDeviceLoginSessions.get(input.attemptId); - if (!session || !this.deviceLoginAttempts.has(input.attemptId)) { - throw new Error("QR login is no longer active"); - } - - this.cancelDeviceLogin(input); - return this.saveLoginSession(session); - } - - cancelDeviceLogin(input: AuthDeviceLoginAttemptRequest): void { - this.deviceLoginAttempts.delete(input.attemptId); - this.pendingDeviceLoginSessions.delete(input.attemptId); - } - - async logout(): Promise { - if (!this.activeUserId) { - return; - } - this.sessions.delete(this.activeUserId); - this.activeUserId = this.sessions.keys().next().value ?? null; - this.selectedProvider = this.getSession()?.provider ?? defaultProvider(); - this.cachedSubscription = null; - this.clearVpcCache(); - await this.persist(); - } - - /** - * Fetch subscription info for the current user. - * Uses caching - call clearSubscriptionCache() to force refresh. - */ - async getSubscription(): Promise { - // Return cached subscription if available - if (this.cachedSubscription) { - return this.cachedSubscription; - } - - const session = await this.ensureValidSession(); - if (!session) { - return null; - } - - const token = session.tokens.idToken ?? session.tokens.accessToken; - const userId = session.user.userId; - - // Fetch dynamic regions to get the VPC ID (handles Alliance partners correctly) - const { vpcId } = await fetchDynamicRegions(token, session.provider.streamingServiceUrl); - - const subscription = await fetchSubscription(token, userId, vpcId ?? undefined); - this.cachedSubscription = subscription; - return subscription; - } - - /** - * Clear the cached subscription info. - * Called automatically on logout. - */ - clearSubscriptionCache(): void { - this.cachedSubscription = null; - } - - /** - * Get the cached subscription without fetching. - * Returns null if not cached. - */ - getCachedSubscription(): SubscriptionInfo | null { - return this.cachedSubscription; - } - - /** - * Get the VPC ID for the current provider. - * Returns cached value if available, otherwise fetches from serverInfo endpoint. - * The VPC ID is used for Alliance partner support and routing to correct data center. - */ - async getVpcId(explicitToken?: string): Promise { - // Return cached VPC ID if available - if (this.cachedVpcId) { - return this.cachedVpcId; - } - - const provider = this.getSelectedProvider(); - const base = provider.streamingServiceUrl.endsWith("/") - ? provider.streamingServiceUrl - : `${provider.streamingServiceUrl}/`; - - let token = explicitToken; - if (!token) { - const session = await this.ensureValidSession(); - token = session ? session.tokens.idToken ?? session.tokens.accessToken : undefined; - } - - const headers = buildGfnLcarsHeaders({ - token, - clientType: "BROWSER", - clientStreamer: "WEBRTC", - includeUserAgent: true, - }); - - try { - const response = await fetch(`${base}v2/serverInfo`, { - headers, - }); - - if (!response.ok) { - return null; - } - - const payload = (await response.json()) as ServerInfoResponse; - const vpcId = payload.requestStatus?.serverId ?? null; - - // Cache the VPC ID - if (vpcId) { - this.cachedVpcId = vpcId; - } - - return vpcId; - } catch { - return null; - } - } - - /** - * Clear the cached VPC ID. - * Called automatically on logout. - */ - clearVpcCache(): void { - this.cachedVpcId = null; - } - - /** - * Get the cached VPC ID without fetching. - * Returns null if not cached. - */ - getCachedVpcId(): string | null { - return this.cachedVpcId; - } - - /** - * Enrich the current session's user with the real membership tier from MES API. - * Falls back silently to the existing tier if the fetch fails. - */ - private async enrichUserTier(): Promise { - const session = this.getSession(); - if (!session) return; - - try { - const subscription = await this.getSubscription(); - if (subscription && subscription.membershipTier) { - this.sessions.set(session.user.userId, { - ...session, - user: { - ...session.user, - membershipTier: subscription.membershipTier, - }, - }); - console.log(`Resolved membership tier: ${subscription.membershipTier}`); - } - } catch (error) { - console.warn("Failed to fetch subscription tier, keeping fallback:", error); - } - } - - private shouldRefresh(tokens: AuthTokens): boolean { - return isNearExpiry(tokens.expiresAt, TOKEN_REFRESH_WINDOW_MS); - } - - async ensureValidSessionWithStatus( - forceRefresh = false, - expectedUserId?: string, - ): Promise { - const currentSession = this.getSession(); - if (!currentSession) { - return { - session: null, - refresh: { - attempted: false, - forced: forceRefresh, - outcome: "not_attempted", - message: "No saved session found.", - }, - }; - } - - const userId = currentSession.user.userId; - let tokens = currentSession.tokens; - - // Official GFN client flow relies on client_token-based refresh. Bootstrap it - // for older sessions that were saved before we persisted client tokens. - if (!tokens.clientToken && !isExpired(tokens.expiresAt)) { - try { - const withClientToken = await this.ensureClientToken(tokens, userId); - if (withClientToken.clientToken && withClientToken.clientToken !== tokens.clientToken) { - this.sessions.set(userId, { - ...currentSession, - tokens: withClientToken, - }); - tokens = withClientToken; - await this.persist(); - } - } catch (error) { - console.warn("Unable to bootstrap client token from saved session:", error); - } - } - - const shouldRefreshNow = forceRefresh || this.shouldRefresh(tokens); - if (!shouldRefreshNow) { - return { - session: this.getSession(), - refresh: { - attempted: false, - forced: forceRefresh, - outcome: "not_attempted", - message: "Session token is still valid.", - }, - }; - } - - const applyRefreshedTokens = async ( - refreshedTokens: AuthTokens, - source: "client_token" | "refresh_token", - ): Promise => { - const latestSession = this.getSession() ?? currentSession; - const baseSession = latestSession.user.userId === userId ? latestSession : currentSession; - const expectedRefreshUserId = expectedUserId ?? userId; - let refreshedUser: AuthUser | null = null; - let userInfoError: string | undefined; - try { - refreshedUser = await fetchUserInfo(refreshedTokens); - console.debug("auth: fetched user info on token refresh", { - userId: refreshedUser.userId, - email: refreshedUser.email, - avatarUrl: refreshedUser.avatarUrl, - }); - } catch (error) { - console.warn("Token refresh succeeded but user info refresh failed. Keeping cached user:", error); - userInfoError = error instanceof Error ? error.message : "Unknown error while fetching user info"; - } - - const resolvedUser = refreshedUser ?? baseSession.user; - if (resolvedUser.userId !== expectedRefreshUserId) { - return { - session: baseSession, - refresh: { - attempted: true, - forced: forceRefresh, - outcome: "failed", - message: refreshedUser - ? "Token refresh returned a different account than expected." - : "Token refresh kept a cached account identity that did not match the expected account.", - error: refreshedUser - ? `expected_user_id:${expectedRefreshUserId} actual_user_id:${refreshedUser.userId}` - : userInfoError - ? `expected_user_id:${expectedRefreshUserId} cached_user_id:${resolvedUser.userId} user_info_error:${userInfoError}` - : `expected_user_id:${expectedRefreshUserId} cached_user_id:${resolvedUser.userId}`, - }, - }; - } - - const updatedSession: AuthSession = { - provider: baseSession.provider, - tokens: refreshedTokens, - user: resolvedUser, - }; - this.sessions.set(updatedSession.user.userId, updatedSession); - - // Re-fetch real tier after token refresh - this.clearSubscriptionCache(); - await this.enrichUserTier(); - await this.persist(); - - const sourceText = source === "client_token" ? "client token" : "refresh token"; - return { - session: this.getSession(), - refresh: { - attempted: true, - forced: forceRefresh, - outcome: "refreshed", - message: forceRefresh - ? `Saved session token refreshed via ${sourceText}.` - : `Session token refreshed via ${sourceText} because it was near expiry.`, - }, - }; - }; - - const refreshErrors: string[] = []; - - if (tokens.clientToken) { - try { - const refreshedFromClientToken = await refreshWithClientToken(tokens.clientToken, userId, tokens.authClientId); - let refreshedTokens = mergeTokenSnapshot(tokens, refreshedFromClientToken); - refreshedTokens = await this.ensureClientToken(refreshedTokens, userId); - return applyRefreshedTokens(refreshedTokens, "client_token"); - } catch (error) { - const message = - error instanceof Error ? error.message : "Unknown error while refreshing with client token"; - refreshErrors.push(`client_token: ${message}`); - } - } - - if (tokens.refreshToken) { - try { - const refreshedOAuth = await refreshAuthTokens(tokens.refreshToken, tokens.authClientId); - let refreshedTokens: AuthTokens = { - ...tokens, - ...refreshedOAuth, - // OAuth refresh does not always return a new client token. - clientToken: tokens.clientToken, - clientTokenExpiresAt: tokens.clientTokenExpiresAt, - clientTokenLifetimeMs: tokens.clientTokenLifetimeMs, - authClientId: refreshedOAuth.authClientId ?? tokens.authClientId, - }; - refreshedTokens = await this.ensureClientToken(refreshedTokens, userId); - return applyRefreshedTokens(refreshedTokens, "refresh_token"); - } catch (error) { - const message = - error instanceof Error ? error.message : "Unknown error while refreshing token"; - refreshErrors.push(`refresh_token: ${message}`); - } - } - - const errorText = refreshErrors.length > 0 ? refreshErrors.join(" | ") : undefined; - const expired = isExpired(tokens.expiresAt); - - if (!tokens.clientToken && !tokens.refreshToken) { - if (expired) { - await this.logout(); - return { - session: null, - refresh: { - attempted: true, - forced: forceRefresh, - outcome: "missing_refresh_token", - message: "Saved session expired and has no refresh mechanism. Please log in again.", - }, - }; - } - - return { - session: this.getSession(), - refresh: { - attempted: true, - forced: forceRefresh, - outcome: "missing_refresh_token", - message: "No refresh token available. Using saved session token.", - }, - }; - } - - if (expired) { - await this.logout(); - return { - session: null, - refresh: { - attempted: true, - forced: forceRefresh, - outcome: "failed", - message: "Token refresh failed and the saved session expired. Please log in again.", - error: errorText, - }, - }; - } - - return { - session: this.getSession(), - refresh: { - attempted: true, - forced: forceRefresh, - outcome: "failed", - message: "Token refresh failed. Using saved session token.", - error: errorText, - }, - }; - } - - async ensureValidSession(): Promise { - const result = await this.ensureValidSessionWithStatus(false); - return result.session; - } - - async resolveJwtToken(explicitToken?: string): Promise { - // Prefer the managed auth session whenever it exists so renderer-side cached - // tokens cannot bypass refresh logic. - if (this.getSession()) { - const session = await this.ensureValidSession(); - if (!session) { - throw new Error("No authenticated session available"); - } - return session.tokens.idToken ?? session.tokens.accessToken; - } - - if (explicitToken && explicitToken.trim()) { - return explicitToken.trim(); - } - - const session = await this.ensureValidSession(); - if (!session) { - throw new Error("No authenticated session available"); - } - - return session.tokens.idToken ?? session.tokens.accessToken; - } -} diff --git a/opennow-stable/src/main/gfn/cloudmatch.ts b/opennow-stable/src/main/gfn/cloudmatch.ts deleted file mode 100644 index 87ca228b8..000000000 --- a/opennow-stable/src/main/gfn/cloudmatch.ts +++ /dev/null @@ -1,1961 +0,0 @@ -import crypto from "node:crypto"; -import dns from "node:dns"; -import { createRequire } from "node:module"; -import { createHash } from "node:crypto"; - -import type { - ActiveSessionInfo, - AppLaunchMode, - ColorQuality, - NegotiatedStreamProfile, - IceServer, - MediaConnectionInfo, - StreamingFeatures, - SessionAdAction, - SessionAdInfo, - SessionAdReportRequest, - SessionAdState, - SessionClaimRequest, - SessionCreateRequest, - SessionInfo, - SessionPollRequest, - SessionStopRequest, - StreamSettings, -} from "@shared/gfn"; - -import { - DEFAULT_KEYBOARD_LAYOUT, - colorQualityBitDepth, - colorQualityChromaFormat, - resolveGfnKeyboardLayout, -} from "@shared/gfn"; -import { DEFAULT_MINIMUM_FPS_FOR_REFLEX_WITHOUT_VRR } from "@shared/cloudGsync"; - -import type { CloudMatchRequest, CloudMatchResponse, GetSessionsResponse } from "./types"; -import { SessionError } from "./errorCodes"; -import { - buildGfnCloudMatchClaimHeaders, - buildGfnCloudMatchHeaders, -} from "./clientHeaders"; -import { getStableDeviceId } from "./deviceId"; -import { fetchWithOptionalProxy } from "./proxyFetch"; -import { - readCloudMatchJson, - throwIfCloudMatchResponseError, -} from "./request"; - -const SESSION_MODIFY_ACTION_AD_UPDATE = 6; -const READY_SESSION_STATUSES = new Set([2, 3]); -const CLOUDMATCH_REQUEST_TIMEOUT_MS = 30_000; -const CLOUDMATCH_GET_RETRIES = 2; -const CLOUDMATCH_RETRY_DELAYS_MS = [250, 750]; -const CLOUDMATCH_RETRY_STATUSES = new Set([408, 425, 429, 500, 502, 503, 504]); -const NETWORK_TEST_SESSION_TIMEOUT_MS = 8_000; -const NETWORK_TEST_SESSION_CACHE_TTL_MS = 30 * 60 * 1000; - -const networkTestSessionCache = new Map(); -const require = createRequire(import.meta.url); - -interface CloudMatchServerInfoResponse { - metaData?: Array<{ - key: string; - value: string; - }>; -} - -interface NetworkTestSessionResponse { - requestStatus?: { - statusCode?: number; - statusDescription?: string; - serverId?: string; - }; - netTestSession?: { - sessionId?: string; - connectionInfo?: Array<{ - ip?: string; - port?: number; - appLevelProtocol?: number; - }>; - netTestThresholds?: { - recommendedBandwidthMBPS?: number; - requiredBandwidthMBPS?: number; - recommendedLatencyMS?: number; - requiredLatencyMS?: number; - recommendedPacketLossPct?: number; - requiredPacketLossPct?: number; - }; - serverId?: string; - }; -} - -interface CloudMatchFetchOptions { - proxyUrl?: string; - timeoutMs?: number; - retries?: number; -} - -function sleep(ms: number): Promise { - return new Promise((resolve) => setTimeout(resolve, ms)); -} - -async function fetchCloudMatch( - input: string, - init: RequestInit, - options: CloudMatchFetchOptions = {}, -): Promise { - const method = (init.method ?? "GET").toUpperCase(); - const retries = options.retries ?? (method === "GET" ? CLOUDMATCH_GET_RETRIES : 0); - const timeoutMs = options.timeoutMs ?? CLOUDMATCH_REQUEST_TIMEOUT_MS; - - let lastError: unknown; - for (let attempt = 0; attempt <= retries; attempt += 1) { - const controller = new AbortController(); - const timeout = setTimeout(() => controller.abort(), timeoutMs); - - try { - const response = await fetchWithOptionalProxy(input, { - ...init, - signal: controller.signal, - }, options.proxyUrl); - clearTimeout(timeout); - - if (attempt < retries && CLOUDMATCH_RETRY_STATUSES.has(response.status)) { - await sleep(CLOUDMATCH_RETRY_DELAYS_MS[Math.min(attempt, CLOUDMATCH_RETRY_DELAYS_MS.length - 1)] ?? 0); - continue; - } - - return response; - } catch (error) { - clearTimeout(timeout); - lastError = error; - if (attempt >= retries) { - throw error; - } - - const retryDelay = CLOUDMATCH_RETRY_DELAYS_MS[Math.min(attempt, CLOUDMATCH_RETRY_DELAYS_MS.length - 1)]; - await sleep(retryDelay ?? 0); - } - } - - throw lastError instanceof Error ? lastError : new Error(String(lastError)); -} - -function normalizeCloudMatchBaseUrl(url: string): string { - const trimmed = url.trim(); - const withProtocol = /^https?:\/\//i.test(trimmed) ? trimmed : `https://${trimmed}`; - return withProtocol.endsWith("/") ? withProtocol.slice(0, -1) : withProtocol; -} - -export function extractServerInfoRegionBases(payload: CloudMatchServerInfoResponse): string[] { - const metadata = payload.metaData ?? []; - const byKey = new Map(metadata.map((entry) => [entry.key, entry.value])); - const regionNames = byKey.get("gfn-regions") - ?.split(",") - .map((entry) => entry.trim()) - .filter(Boolean) ?? []; - const localRegionName = byKey.get("local-region")?.trim(); - const orderedRegionNames = [ - ...(localRegionName ? [localRegionName] : []), - ...regionNames, - ]; - const bases: string[] = []; - const seen = new Set(); - - for (const regionName of orderedRegionNames) { - const regionUrl = byKey.get(regionName); - if (!regionUrl?.startsWith("http")) { - continue; - } - const normalized = normalizeCloudMatchBaseUrl(regionUrl); - if (!seen.has(normalized)) { - seen.add(normalized); - bases.push(normalized); - } - } - - return bases; -} - -function isDefaultStreamingServiceBase(baseUrl: string): boolean { - try { - const hostname = new URL(baseUrl).hostname.toLowerCase(); - return hostname === "prod.cloudmatchbeta.nvidiagrid.net" || - (hostname.startsWith("prod.") && hostname.endsWith(".nvidiagrid.net")); - } catch { - return false; - } -} - -async function resolveCreateSessionBase( - base: string, - token: string, - clientId: string, - deviceId: string, - proxyUrl?: string, -): Promise { - if (!isDefaultStreamingServiceBase(base)) { - return base; - } - - try { - const response = await fetchCloudMatch(`${base}/v2/serverInfo`, { - method: "GET", - headers: buildGfnCloudMatchHeaders({ token, clientId, deviceId, includeOrigin: false }), - }, { proxyUrl }); - if (!response.ok) { - return base; - } - - const [localRegionBase] = extractServerInfoRegionBases( - (await response.json()) as CloudMatchServerInfoResponse, - ); - if (!localRegionBase || localRegionBase === base) { - return base; - } - - console.log(`[CloudMatch] createSession resolved ${base} to local region ${localRegionBase}`); - return localRegionBase; - } catch (error) { - console.warn(`[CloudMatch] createSession local-region discovery failed: ${formatErrorForLog(error)}`); - return base; - } -} - -const AD_ACTION_CODES: Record = { - start: 1, - pause: 2, - resume: 3, - finish: 4, - cancel: 5, -}; - -const GFN_AD_MEDIA_PROFILE_ORDER = new Map([ - ["mp4deinterlaced720p", 0], - ["webm", 1], - ["hlsadaptive", 2], -]); - -// Wire values used by cloudmatch session requests. Matches the official -// client's mapping: Default -> 1, GamepadFriendly -> 2, TouchFriendly -> 3. -const APP_LAUNCH_MODE_WIRE_VALUES: Record = { - default: 1, - gamepadFriendly: 2, - touchFriendly: 3, -}; - -export function appLaunchModeWireValue(mode: AppLaunchMode | undefined): number { - return APP_LAUNCH_MODE_WIRE_VALUES[mode ?? "default"]; -} - -/** Wire appLaunchMode the server echoes back for an existing session, if present. */ -function echoedSessionAppLaunchMode(payload: CloudMatchResponse): number | undefined { - const raw = payload.session?.sessionRequestData?.appLaunchMode; - return typeof raw === "number" && Number.isFinite(raw) ? raw : undefined; -} - -export function buildRequestedStreamingFeatures( - settings: StreamSettings, - bitDepth: number, - chromaFormat: number, - _hdrEnabled: boolean, -): CloudMatchRequest["sessionRequestData"]["requestedStreamingFeatures"] { - const cloudGsync = settings.enableCloudGsync; - - return { - reflex: shouldRequestReflex(settings), - bitDepth, - cloudGsync, - enabledL4S: settings.enableL4S, - supportedHidDevices: 0, - profile: 0, - fallbackToLogicalResolution: false, - chromaFormat, - prefilterMode: 0, - prefilterSharpness: 0, - prefilterNoiseReduction: 0, - hudStreamingMode: 0, - }; -} - -export function shouldRequestReflex(settings: StreamSettings): boolean { - if (typeof settings.cloudGsyncResolution?.reflexEnabled === "boolean") { - return settings.cloudGsyncResolution.reflexEnabled; - } - - const reflexMinimum = - settings.cloudGsyncResolution?.capabilities.minimumFpsForReflexWithoutVrr - ?? DEFAULT_MINIMUM_FPS_FOR_REFLEX_WITHOUT_VRR; - return settings.enableCloudGsync || settings.fps >= reflexMinimum; -} - -function isReadySessionStatus(status: number): boolean { - return READY_SESSION_STATUSES.has(status); -} - -async function resolveHostnameWithFallback(hostname: string): Promise { - // Try system resolver first, then fall back to Cloudflare (1.1.1.1) and Google (8.8.8.8) - try { - const r = await dns.promises.lookup(hostname); - if (r && (r as any).address) return (r as any).address; - } catch { - // ignore and try custom resolvers - } - - const fallbackServers = ["1.1.1.1", "8.8.8.8"]; - for (const server of fallbackServers) { - try { - const resolver = new dns.Resolver(); - resolver.setServers([server]); - const addrs: string[] = await new Promise((resolve, reject) => { - resolver.resolve4(hostname, (err, addresses) => { - if (err) reject(err); - else resolve(addresses); - }); - }); - if (addrs && addrs.length > 0) return addrs[0]; - } catch { - // try next fallback - } - } - - return null; -} - -async function normalizeIceServers(response: CloudMatchResponse): Promise { - const raw = response.session.iceServerConfiguration?.iceServers ?? []; - const servers = raw - .map((entry) => { - const urls = Array.isArray(entry.urls) ? entry.urls : [entry.urls]; - return { - urls, - username: entry.username, - credential: entry.credential, - }; - }) - .filter((entry) => entry.urls.length > 0); - - if (servers.length > 0) { - // Attempt to resolve any hostnames in STUN/TURN URLs to IPs to avoid relying on the - // renderer's DNS resolution. This makes it possible to try alternate DNS servers - // when the system resolver fails. - const resolvedServers: IceServer[] = []; - for (const s of servers) { - const resolvedUrls: string[] = []; - for (const u of s.urls) { - try { - const m = u.match(/^([a-zA-Z0-9+.-]+):([^/]+)/); - if (m) { - const scheme = m[1]; - const hostPort = m[2]; - const host = hostPort.split(":")[0]; - const portPart = hostPort.includes(":") ? ":" + hostPort.split(":").slice(1).join(":") : ""; - - // Helper to bracket IPv6 literals when necessary - const bracketIfIpv6 = (h: string) => { - if (h.startsWith("[") && h.endsWith("]")) return h; - // Heuristic: contains ':' and is not an IPv4 dotted-quad - if (h.includes(":") && !/^\d{1,3}(?:\.\d{1,3}){3}$/.test(h)) { - return `[${h}]`; - } - return h; - }; - - // If host already looks like an IPv4 or bracketed IPv6, keep original URL - if (/^\d{1,3}(?:\.\d{1,3}){3}$/.test(host) || /^\[[0-9a-fA-F:]+\]$/.test(host)) { - resolvedUrls.push(u); - } else { - const ip = await resolveHostnameWithFallback(host); - const finalHost = ip ?? host; - const maybeBracketted = bracketIfIpv6(finalHost); - resolvedUrls.push(`${scheme}:${maybeBracketted}${portPart}`); - } - } else { - resolvedUrls.push(u); - } - } catch { - resolvedUrls.push(u); - } - } - resolvedServers.push({ urls: resolvedUrls, username: s.username, credential: s.credential }); - } - - return resolvedServers; - } - - // Default fallbacks — try to resolve known STUN hostnames to IPs as well - const defaults = ["s1.stun.gamestream.nvidia.com:19308", "stun.l.google.com:19302", "stun1.l.google.com:19302"]; - const out: IceServer[] = []; - for (const d of defaults) { - const parts = d.split(":"); - const host = parts[0]; - const port = parts.length > 1 ? `:${parts.slice(1).join(":")}` : ""; - const ip = await resolveHostnameWithFallback(host); - const bracketIfIpv6 = (h: string) => (h.includes(":") && !h.startsWith("[") ? `[${h}]` : h); - if (ip) out.push({ urls: [`stun:${bracketIfIpv6(ip)}${port}`] }); - else out.push({ urls: [`stun:${bracketIfIpv6(host)}${port}`] }); - } - - return out; -} - -/** - * Extract the streaming server IP from the CloudMatch response, matching Rust's - * `streaming_server_ip()` priority chain: - * 1. connectionInfo[usage==14].ip (direct IP) - * 2. Host extracted from connectionInfo[usage==14].resourcePath (for rtsps:// URLs) - * 3. sessionControlInfo.ip (fallback) - */ -function streamingServerIp(response: CloudMatchResponse): string | null { - const connections = response.session.connectionInfo ?? []; - const sigConn = connections.find((conn) => conn.usage === 14); - - if (sigConn) { - // Priority 1: Direct IP field - const rawIp = sigConn.ip; - const directIp = Array.isArray(rawIp) ? rawIp[0] : rawIp; - if (directIp && directIp.length > 0) { - return directIp; - } - - // Priority 2: Extract host from resourcePath (Alliance format: rtsps://host:port) - if (sigConn.resourcePath) { - const host = extractHostFromUrl(sigConn.resourcePath); - if (host) return host; - } - } - - // Priority 3: sessionControlInfo.ip - const controlIp = response.session.sessionControlInfo?.ip; - if (controlIp && controlIp.length > 0) { - return Array.isArray(controlIp) ? controlIp[0] : controlIp; - } - - return null; -} - -/** - * Extract host from a URL string (handles rtsps://, rtsp://, wss://, https://). - * Matches Rust's extract_host_from_url(). - */ -function extractHostFromUrl(url: string): string | null { - const prefixes = ["rtsps://", "rtsp://", "wss://", "https://"]; - let afterProto: string | null = null; - for (const prefix of prefixes) { - if (url.startsWith(prefix)) { - afterProto = url.slice(prefix.length); - break; - } - } - if (!afterProto) return null; - - // Get host (before port or path) - const host = afterProto.split(":")[0]?.split("/")[0]; - if (!host || host.length === 0 || host.startsWith(".")) return null; - return host; -} - -/** - * Check if a given IP/hostname is a CloudMatch zone load balancer hostname - * (not a real game server IP). Zone hostnames look like: - * np-ams-06.cloudmatchbeta.nvidiagrid.net - */ -function isZoneHostname(ip: string): boolean { - return ip.includes("cloudmatchbeta.nvidiagrid.net") || ip.includes("cloudmatch.nvidiagrid.net"); -} - -function resolveSignaling(response: CloudMatchResponse): { - serverIp: string; - signalingServer: string; - signalingUrl: string; - mediaConnectionInfo?: MediaConnectionInfo; -} { - const connections = response.session.connectionInfo ?? []; - const signalingConnection = - connections.find((conn) => conn.usage === 14 && conn.ip) ?? connections.find((conn) => conn.ip); - - // Use the Rust-matching priority chain for server IP - const serverIp = streamingServerIp(response); - if (!serverIp) { - throw new Error("CloudMatch response did not include a signaling host"); - } - - const resourcePath = signalingConnection?.resourcePath ?? "/nvst/"; - - // Build signaling URL matching Rust's build_signaling_url() behavior: - // - rtsps://host:port -> extract host, convert to wss://host/nvst/ - // - wss://... -> use as-is - // - /path -> wss://serverIp:443/path - // - fallback -> wss://serverIp:443/nvst/ - const { signalingUrl, signalingHost } = buildSignalingUrl(resourcePath, serverIp); - - // Use the resolved signaling host (which may differ from serverIp if extracted from rtsps:// URL) - const effectiveHost = signalingHost ?? serverIp; - const signalingServer = effectiveHost.includes(":") - ? effectiveHost - : `${effectiveHost}:443`; - - return { - serverIp, - signalingServer, - signalingUrl, - mediaConnectionInfo: resolveMediaConnectionInfo(connections, serverIp, { - logMissing: isReadySessionStatus(response.session.status), - }), - }; -} - -/** - * Resolve the media connection endpoint (IP + port) from the session's connectionInfo array. - * Matches Rust's media_connection_info() priority chain: - * 1. usage=2 (Primary media path, UDP) - * 2. usage=17 (Alternative media path) - * 3. usage=14 with highest port (Alliance fallback — distinguishes media port from signaling port) - * 4. Fallback: use serverIp with the highest port from any usage=14 entry - * - * For each entry, IP is extracted from: - * a. The .ip field directly - * b. The hostname in .resourcePath (e.g. rtsps://80-250-97-40.server.net:48322) - * c. Fallback to serverIp (only for usage=14 Alliance fallback) - */ -function resolveMediaConnectionInfo( - connections: Array<{ ip?: string; port: number; usage: number; protocol?: number; resourcePath?: string }>, - serverIp: string, - options?: { logMissing?: boolean }, -): { ip: string; port: number; usage: number } | undefined { - // Helper: extract IP from a connection entry - const extractIp = (conn: { ip?: string; resourcePath?: string }): string | null => { - // Try direct IP field - const rawIp = conn.ip; - const directIp = Array.isArray(rawIp) ? rawIp[0] : rawIp; - if (directIp && directIp.length > 0) return directIp; - - // Try hostname from resourcePath - if (conn.resourcePath) { - const host = extractHostFromUrl(conn.resourcePath); - if (host) return host; - } - - return null; - }; - - // Helper: extract port from a connection entry (fallback to resourcePath URL port) - const extractPort = (conn: { port: number; resourcePath?: string }): number => { - if (conn.port > 0) return conn.port; - - // Try extracting port from resourcePath URL - if (conn.resourcePath) { - try { - const url = new URL(conn.resourcePath.replace("rtsps://", "https://").replace("rtsp://", "http://")); - const portStr = url.port; - if (portStr) return parseInt(portStr, 10); - } catch { - // Ignore - } - } - - return 0; - }; - - // Priority 1: usage=2 (Primary media path, UDP) - const primary = connections.find((c) => c.usage === 2); - if (primary) { - const ip = extractIp(primary); - const port = extractPort(primary); - console.log(`[CloudMatch] resolveMediaConnectionInfo: usage=2 candidate: ip=${ip}, port=${port}`); - if (ip && port > 0) return { ip, port, usage: primary.usage }; - } - - // Priority 2: usage=17 (Alternative media path) - const alt = connections.find((c) => c.usage === 17); - if (alt) { - const ip = extractIp(alt); - const port = extractPort(alt); - console.log(`[CloudMatch] resolveMediaConnectionInfo: usage=17 candidate: ip=${ip}, port=${port}`); - if (ip && port > 0) return { ip, port, usage: alt.usage }; - } - - // Priority 3: usage=14 with highest port (Alliance fallback) - const alliance = connections - .filter((c) => c.usage === 14) - .sort((a, b) => b.port - a.port); - - for (const conn of alliance) { - const ip = extractIp(conn) ?? serverIp; - const port = extractPort(conn); - console.log(`[CloudMatch] resolveMediaConnectionInfo: usage=14 candidate: ip=${ip}, port=${port} (serverIp fallback=${serverIp})`); - if (ip && port > 0) return { ip, port, usage: conn.usage }; - } - - if (options?.logMissing ?? true) { - console.log("[CloudMatch] resolveMediaConnectionInfo: NO valid media connection info found"); - } - return undefined; -} - -/** - * Build signaling WSS URL from the resourcePath, matching Rust implementation. - * Returns the URL and optionally the extracted host (if different from serverIp). - */ -function buildSignalingUrl( - raw: string, - serverIp: string, -): { signalingUrl: string; signalingHost: string | null } { - if (raw.startsWith("rtsps://") || raw.startsWith("rtsp://")) { - // Extract hostname from RTSP URL, convert to wss:// - const withoutScheme = raw.startsWith("rtsps://") - ? raw.slice("rtsps://".length) - : raw.slice("rtsp://".length); - const host = withoutScheme.split(":")[0]?.split("/")[0]; - if (host && host.length > 0 && !host.startsWith(".")) { - return { - signalingUrl: `wss://${host}/nvst/`, - signalingHost: host, - }; - } - return { - signalingUrl: `wss://${serverIp}:443/nvst/`, - signalingHost: null, - }; - } - - if (raw.startsWith("wss://")) { - // Already a full WSS URL, use as-is; extract host - const withoutScheme = raw.slice("wss://".length); - const host = withoutScheme.split("/")[0] ?? null; - return { signalingUrl: raw, signalingHost: host }; - } - - if (raw.startsWith("/")) { - // Relative path - return { - signalingUrl: `wss://${serverIp}:443${raw}`, - signalingHost: null, - }; - } - - // Fallback - return { - signalingUrl: `wss://${serverIp}:443/nvst/`, - signalingHost: null, - }; -} - -function parseResolution(input: string): { width: number; height: number } { - const [rawWidth, rawHeight] = input.split("x"); - const width = Number.parseInt(rawWidth ?? "", 10); - const height = Number.parseInt(rawHeight ?? "", 10); - - if (!Number.isFinite(width) || !Number.isFinite(height) || width <= 0 || height <= 0) { - return { width: 1920, height: 1080 }; - } - - return { width, height }; -} - -function networkTestSessionCacheKey(base: string, settings: StreamSettings, token: string, proxyUrl?: string): string { - const { width, height } = parseResolution(settings.resolution); - const identityHash = createHash("sha256") - .update(token) - .update("\0") - .update(proxyUrl ?? "") - .digest("hex") - .slice(0, 16); - return `${base}\0${width}x${height}@${settings.fps}\0${identityHash}`; -} - -function getCachedNetworkTestSessionId(base: string, settings: StreamSettings, token: string, proxyUrl?: string): string | null { - const cacheKey = networkTestSessionCacheKey(base, settings, token, proxyUrl); - const cached = networkTestSessionCache.get(cacheKey); - if (!cached) { - return null; - } - - if (cached.expiresAt <= Date.now()) { - networkTestSessionCache.delete(cacheKey); - return null; - } - - return cached.sessionId; -} - -function cacheNetworkTestSessionId( - base: string, - settings: StreamSettings, - token: string, - sessionId: string, - proxyUrl?: string, -): void { - networkTestSessionCache.set(networkTestSessionCacheKey(base, settings, token, proxyUrl), { - sessionId, - expiresAt: Date.now() + NETWORK_TEST_SESSION_CACHE_TTL_MS, - }); -} - -async function createNetworkTestSession(input: { - base: string; - token: string; - clientId: string; - deviceId: string; - settings: StreamSettings; - proxyUrl?: string; -}): Promise { - const cached = getCachedNetworkTestSessionId(input.base, input.settings, input.token, input.proxyUrl); - if (cached) { - return cached; - } - - const { width, height } = parseResolution(input.settings.resolution); - const body = { - netTestRequestData: { - clientPlatformName: "windows", - netTestProfile: { - widthInPixels: width, - heightInPixels: height, - framesPerSecond: input.settings.fps, - }, - }, - }; - - try { - const response = await fetchCloudMatch(`${input.base}/v2/nettestsession`, { - method: "POST", - headers: buildGfnCloudMatchHeaders({ - token: input.token, - clientId: input.clientId, - deviceId: input.deviceId, - includeOrigin: true, - }), - body: JSON.stringify(body), - }, { - proxyUrl: input.proxyUrl, - timeoutMs: NETWORK_TEST_SESSION_TIMEOUT_MS, - retries: 0, - }); - - if (!response.ok) { - console.warn(`[CloudMatch] nettestsession failed HTTP ${response.status}: ${(await response.text()).slice(0, 200)}`); - return null; - } - - const payload = (await response.json()) as NetworkTestSessionResponse; - if (payload.requestStatus?.statusCode !== 1) { - console.warn( - `[CloudMatch] nettestsession API error: ${payload.requestStatus?.statusCode ?? "unknown"} ` + - `${payload.requestStatus?.statusDescription ?? ""}`.trim(), - ); - return null; - } - - const sessionId = payload.netTestSession?.sessionId?.trim(); - if (!sessionId) { - console.warn("[CloudMatch] nettestsession response did not include a sessionId"); - return null; - } - - cacheNetworkTestSessionId(input.base, input.settings, input.token, sessionId, input.proxyUrl); - return sessionId; - } catch (error) { - console.warn(`[CloudMatch] nettestsession creation failed: ${formatErrorForLog(error)}`); - return null; - } -} - -function timezoneOffsetMs(): number { - return -new Date().getTimezoneOffset() * 60 * 1000; -} - -function webRtcSessionMetadata(width: number, height: number): Array<{ key: string; value: string }> { - return [ - { key: "SubSessionId", value: crypto.randomUUID() }, - { key: "wssignaling", value: "1" }, - { key: "GSStreamerType", value: "WebRTC" }, - { key: "networkType", value: "Unknown" }, - { key: "ClientImeSupport", value: "0" }, - { - key: "clientPhysicalResolution", - value: JSON.stringify({ horizontalPixels: width, verticalPixels: height }), - }, - { key: "surroundAudioInfo", value: "2" }, - ]; -} - -export function shouldEnableInGameSettingsPersistence( - input: Pick, -): boolean { - return ( - input.enablePersistingInGameSettings === true && - input.supportsInGameSettingsPersistence === true - ); -} - -function buildSessionRequestBody( - input: SessionCreateRequest, - deviceHashId: string, - networkTestSessionId: string | null = null, -): CloudMatchRequest { - const { width, height } = parseResolution(input.settings.resolution); - const cq = input.settings.colorQuality; - // IMPORTANT: hdrEnabled is a SEPARATE toggle from color quality. - // The Rust reference (cloudmatch.rs) uses settings.hdr_enabled independently. - // 10-bit color depth does NOT mean HDR — you can have 10-bit SDR. - // Conflating them caused the server to set up an HDR pipeline, which - // dynamically downscaled resolution to ~540p. - const hdrEnabled = false; // No HDR toggle implemented yet; hardcode off like claim body - const bitDepth = colorQualityBitDepth(cq); - const chromaFormat = colorQualityChromaFormat(cq); - const accountLinked = input.accountLinked ?? true; - - return { - sessionRequestData: { - appId: input.appId, - internalTitle: input.internalTitle || null, - availableSupportedControllers: [], - networkTestSessionId, - parentSessionId: null, - clientIdentification: "GFN-PC", - // Keep device identity stable across create -> reconnect/resume flows. - // The official client preserves this identity, and resume reliability depends on it. - deviceHashId, - clientVersion: "30.0", - sdkVersion: "1.0", - streamerVersion: 1, - clientPlatformName: "windows", - clientRequestMonitorSettings: [ - { - monitorId: 0, - positionX: 0, - positionY: 0, - widthInPixels: width, - heightInPixels: height, - framesPerSecond: input.settings.fps, - sdrHdrMode: hdrEnabled ? 1 : 0, - displayData: hdrEnabled - ? { - desiredContentMaxLuminance: 1000, - desiredContentMinLuminance: 0, - desiredContentMaxFrameAverageLuminance: 500, - } - : {}, - hdr10PlusGamingData: null, - dpi: 0, - }, - ], - useOps: true, - audioMode: 2, - metaData: webRtcSessionMetadata(width, height), - sdrHdrMode: hdrEnabled ? 1 : 0, - clientDisplayHdrCapabilities: hdrEnabled - ? { - version: 1, - hdrEdrSupportedFlagsInUint32: 1, - staticMetadataDescriptorId: 0, - } - : null, - surroundAudioInfo: 0, - remoteControllersBitmap: 0, - clientTimezoneOffset: timezoneOffsetMs(), - enhancedStreamMode: 1, - appLaunchMode: appLaunchModeWireValue(input.settings.appLaunchMode), - secureRTSPSupported: false, - partnerCustomData: "", - accountLinked, - enablePersistingInGameSettings: shouldEnableInGameSettingsPersistence(input), - userAge: 26, - requestedStreamingFeatures: buildRequestedStreamingFeatures( - input.settings, - bitDepth, - chromaFormat, - hdrEnabled, - ), - }, - }; -} - -function cloudmatchUrl(zone: string): string { - return `https://${zone}.cloudmatchbeta.nvidiagrid.net`; -} - -function resolveStreamingBaseUrl(zone: string, provided?: string): string { - if (provided && provided.trim()) { - const trimmed = provided.trim(); - return trimmed.endsWith("/") ? trimmed.slice(0, -1) : trimmed; - } - return cloudmatchUrl(zone); -} - -function shouldUseServerIp(baseUrl: string): boolean { - return baseUrl.includes("cloudmatchbeta.nvidiagrid.net"); -} - -function resolvePollStopBase(zone: string, provided?: string, serverIp?: string): string { - const base = resolveStreamingBaseUrl(zone, provided); - // Only use serverIp if it's a real server IP (not a zone hostname). - // The Rust version checks: if we're NOT an alliance partner AND we have a server_ip, use it. - // But if the "serverIp" is actually the zone hostname (from an early poll when connectionInfo - // was empty), using it is circular and doesn't help. - if (serverIp && shouldUseServerIp(base) && !isZoneHostname(serverIp)) { - return `https://${serverIp}`; - } - return base; -} - -function toPositiveInt(value: unknown): number | undefined { - if (typeof value === "number" && Number.isFinite(value)) { - const normalized = Math.trunc(value); - return normalized > 0 ? normalized : undefined; - } - if (typeof value === "string" && value.trim().length > 0) { - const parsed = Number.parseInt(value, 10); - return Number.isFinite(parsed) && parsed > 0 ? parsed : undefined; - } - return undefined; -} - -function toBoolean(value: unknown): boolean | undefined { - if (typeof value === "boolean") { - return value; - } - if (typeof value === "number" && Number.isFinite(value)) { - return value !== 0; - } - if (typeof value === "string") { - const normalized = value.trim().toLowerCase(); - if (normalized === "true" || normalized === "1") { - return true; - } - if (normalized === "false" || normalized === "0") { - return false; - } - } - return undefined; -} - -function toOptionalString(value: unknown): string | undefined { - if (typeof value !== "string") { - return undefined; - } - const trimmed = value.trim(); - return trimmed.length > 0 ? trimmed : undefined; -} - -function extractSessionQueuePosition(session: CloudMatchResponse["session"] | GetSessionsResponse["sessions"][number]): number | undefined { - const direct = toPositiveInt(session.queuePosition); - if (direct !== undefined) { - return direct; - } - - const seatSetup = session.seatSetupInfo; - if (seatSetup) { - const nested = toPositiveInt(seatSetup.queuePosition); - if (nested !== undefined) { - return nested; - } - } - - const nestedSessionProgress = session.sessionProgress; - if (nestedSessionProgress) { - const nested = toPositiveInt(nestedSessionProgress.queuePosition); - if (nested !== undefined) { - return nested; - } - } - - const nestedProgressInfo = session.progressInfo; - if (nestedProgressInfo) { - const nested = toPositiveInt(nestedProgressInfo.queuePosition); - if (nested !== undefined) { - return nested; - } - } - - return undefined; -} - -function extractQueuePosition(payload: CloudMatchResponse): number | undefined { - return extractSessionQueuePosition(payload.session); -} - -function extractSessionSeatSetupStep(session: CloudMatchResponse["session"] | GetSessionsResponse["sessions"][number]): number | undefined { - const raw = session.seatSetupInfo?.seatSetupStep; - if (typeof raw === "number" && Number.isFinite(raw)) { - return Math.trunc(raw); - } - return undefined; -} - -function extractSeatSetupStep(payload: CloudMatchResponse): number | undefined { - return extractSessionSeatSetupStep(payload.session); -} - -function normalizeSessionAdInfo(ad: NonNullable[number], index: number): SessionAdInfo | null { - const adId = toOptionalString(ad.adId); - const adMediaFiles = (ad.adMediaFiles ?? []) - .map((file) => ({ - mediaFileUrl: toOptionalString(file.mediaFileUrl), - encodingProfile: toOptionalString(file.encodingProfile), - })) - .filter((file) => file.mediaFileUrl || file.encodingProfile) - .sort((left, right) => { - const leftRank = left.encodingProfile ? GFN_AD_MEDIA_PROFILE_ORDER.get(left.encodingProfile) ?? Number.MAX_SAFE_INTEGER : Number.MAX_SAFE_INTEGER; - const rightRank = right.encodingProfile ? GFN_AD_MEDIA_PROFILE_ORDER.get(right.encodingProfile) ?? Number.MAX_SAFE_INTEGER : Number.MAX_SAFE_INTEGER; - return leftRank - rightRank; - }); - - // Match the official browser config preference order: MP4, WebM, then HLS. - const preferredMediaFile = adMediaFiles.find((file) => file.mediaFileUrl); - const mediaUrl = - preferredMediaFile?.mediaFileUrl ?? - toOptionalString(ad.adUrl) ?? - toOptionalString(ad.mediaUrl) ?? - toOptionalString(ad.videoUrl) ?? - toOptionalString(ad.url); - - const adUrl = toOptionalString(ad.adUrl); - const clickThroughUrl = toOptionalString(ad.clickThroughUrl); - const title = toOptionalString(ad.title); - const description = toOptionalString(ad.description); - const adLengthInSeconds = - typeof ad.adLengthInSeconds === "number" && Number.isFinite(ad.adLengthInSeconds) && ad.adLengthInSeconds > 0 - ? ad.adLengthInSeconds - : undefined; - - // adLengthInSeconds is the confirmed live field (value is in seconds, convert to ms). - // Fall back to legacy durationMs / durationInMs which are already in ms. - const durationMs = - (adLengthInSeconds !== undefined - ? Math.round(adLengthInSeconds * 1000) - : undefined) ?? - toPositiveInt(ad.durationMs) ?? - toPositiveInt(ad.durationInMs); - - const adState = typeof ad.adState === "number" && Number.isFinite(ad.adState) ? Math.trunc(ad.adState) : undefined; - - if (!adId && !mediaUrl && !adUrl && adMediaFiles.length === 0 && !title && !description) { - return null; - } - - return { - adId: adId ?? `ad-${index + 1}`, - state: adState, - adState, - adUrl, - mediaUrl, - adMediaFiles, - clickThroughUrl, - adLengthInSeconds, - durationMs, - title, - description, - }; -} - -function extractAdState(payload: CloudMatchResponse): SessionAdState | undefined { - const sessionAdsRequired = - toBoolean(payload.session.sessionAdsRequired) ?? - toBoolean(payload.session.isAdsRequired) ?? - toBoolean(payload.session.sessionProgress?.isAdsRequired) ?? - toBoolean(payload.session.progressInfo?.isAdsRequired); - - // Log raw sessionAds whenever the server signals ads are required so field names - // can be verified when creative URLs are expected but the ads[] array stays empty. - if (sessionAdsRequired) { - console.log( - `[CloudMatch] extractAdState: sessionAdsRequired=${payload.session.sessionAdsRequired}, ` + - `isAdsRequired=${payload.session.isAdsRequired}, ` + - `sessionAds=${JSON.stringify(payload.session.sessionAds ?? null)}, ` + - `opportunity=${JSON.stringify(payload.session.opportunity ?? null)}`, - ); - } - - const ads = (payload.session.sessionAds ?? []) - .map((ad, index) => normalizeSessionAdInfo(ad, index)) - .filter((ad): ad is SessionAdInfo => ad !== null); - - const opportunity = payload.session.opportunity; - const normalizedOpportunity = opportunity - ? { - state: toOptionalString(opportunity.state), - queuePaused: toBoolean(opportunity.queuePaused), - gracePeriodSeconds: toPositiveInt(opportunity.gracePeriodSeconds), - message: toOptionalString(opportunity.message), - title: toOptionalString(opportunity.title), - description: toOptionalString(opportunity.description), - } - : undefined; - const queuePaused = - normalizedOpportunity?.queuePaused ?? - (typeof normalizedOpportunity?.state === "string" ? normalizedOpportunity.state.toLowerCase() === "graceperiodstart" : undefined); - const gracePeriodSeconds = normalizedOpportunity?.gracePeriodSeconds; - const effectiveIsAdsRequired = sessionAdsRequired ?? ads.length > 0; - const message = - normalizedOpportunity?.message ?? - normalizedOpportunity?.description ?? - (queuePaused - ? "Resume ads to stay in queue." - : effectiveIsAdsRequired - ? "Finish ads to stay in queue." - : undefined); - - if (!effectiveIsAdsRequired && ads.length === 0 && !queuePaused && !message) { - return undefined; - } - - return { - isAdsRequired: effectiveIsAdsRequired, - sessionAdsRequired, - isQueuePaused: queuePaused, - gracePeriodSeconds, - message, - sessionAds: ads, - ads, - opportunity: normalizedOpportunity, - // Mark whether the server sent sessionAds=null (transient gap) so the - // renderer's mergeAdState can safely restore the previous ad list for the - // ad player, while NOT restoring it after an explicit client-side clear - // that follows a rejected finish action. - serverSentEmptyAds: payload.session.sessionAds == null, - }; -} - -function toColorQuality(bitDepth?: number, chromaFormat?: number): ColorQuality | undefined { - const normalizedBitDepth = bitDepth === 10 ? 1 : bitDepth; - const normalizedChromaFormat = chromaFormat === 2 ? 1 : chromaFormat; - - if (normalizedBitDepth !== 0 && normalizedBitDepth !== 1) { - return undefined; - } - if (normalizedChromaFormat !== 0 && normalizedChromaFormat !== 1) { - return undefined; - } - - if (normalizedBitDepth === 1) { - return normalizedChromaFormat === 1 ? "10bit_444" : "10bit_420"; - } - - return normalizedChromaFormat === 1 ? "8bit_444" : "8bit_420"; -} - -function normalizeStreamingFeatures( - features: - | NonNullable["requestedStreamingFeatures"] - | CloudMatchResponse["session"]["finalizedStreamingFeatures"] - | undefined, -): StreamingFeatures | undefined { - if (!features) { - return undefined; - } - - const normalized: StreamingFeatures = {}; - - if (typeof features.reflex === "boolean") { - normalized.reflex = features.reflex; - } - if (typeof features.bitDepth === "number" && Number.isFinite(features.bitDepth)) { - normalized.bitDepth = Math.trunc(features.bitDepth); - } - if (typeof features.cloudGsync === "boolean") { - normalized.cloudGsync = features.cloudGsync; - } - if (typeof features.chromaFormat === "number" && Number.isFinite(features.chromaFormat)) { - normalized.chromaFormat = Math.trunc(features.chromaFormat); - } - if (typeof features.enabledL4S === "boolean") { - normalized.enabledL4S = features.enabledL4S; - } - if ("trueHdr" in features && typeof features.trueHdr === "boolean") { - normalized.trueHdr = features.trueHdr; - } - - return Object.keys(normalized).length > 0 ? normalized : undefined; -} - -function extractNegotiatedStreamProfile(payload: CloudMatchResponse): NegotiatedStreamProfile | undefined { - const monitor = payload.session.sessionRequestData?.clientRequestMonitorSettings?.[0]; - const finalizedFeatures = payload.session.finalizedStreamingFeatures; - const requestedFeatures = payload.session.sessionRequestData?.requestedStreamingFeatures; - - const width = monitor?.widthInPixels; - const height = monitor?.heightInPixels; - const fps = monitor?.framesPerSecond; - const colorQuality = toColorQuality( - finalizedFeatures?.bitDepth ?? requestedFeatures?.bitDepth, - finalizedFeatures?.chromaFormat ?? requestedFeatures?.chromaFormat, - ); - const enabledL4S = finalizedFeatures?.enabledL4S ?? requestedFeatures?.enabledL4S; - const enabledCloudGsync = finalizedFeatures?.cloudGsync ?? requestedFeatures?.cloudGsync; - const enabledReflex = finalizedFeatures?.reflex ?? requestedFeatures?.reflex; - - const profile: NegotiatedStreamProfile = {}; - - if ( - typeof width === "number" && - Number.isFinite(width) && - width > 0 && - typeof height === "number" && - Number.isFinite(height) && - height > 0 - ) { - profile.resolution = `${Math.trunc(width)}x${Math.trunc(height)}`; - } - - if (typeof fps === "number" && Number.isFinite(fps) && fps > 0) { - profile.fps = Math.trunc(fps); - } - - if (colorQuality) { - profile.colorQuality = colorQuality; - } - - if (typeof enabledL4S === "boolean") { - profile.enableL4S = enabledL4S; - } - - if (typeof enabledCloudGsync === "boolean") { - profile.enableCloudGsync = enabledCloudGsync; - } - - if (typeof enabledReflex === "boolean") { - profile.enableReflex = enabledReflex; - } - - return Object.keys(profile).length > 0 ? profile : undefined; -} - -interface ToSessionInfoOptions { - zone: string; - streamingBaseUrl: string; - payload: CloudMatchResponse; - clientId?: string; - deviceId?: string; - fallbackAppId?: string; - /** Wire appLaunchMode sent with the request, used when the server does not echo it */ - fallbackAppLaunchMode?: number; -} - -async function toSessionInfo(options: ToSessionInfoOptions): Promise { - const { zone, streamingBaseUrl, payload, clientId, deviceId } = options; - if (payload.requestStatus.statusCode !== 1) { - // Use SessionError for parsing error responses - const errorJson = JSON.stringify(payload); - throw SessionError.fromResponse(200, errorJson); - } - - const signaling = resolveSignaling(payload); - const queuePosition = extractQueuePosition(payload); - const seatSetupStep = extractSeatSetupStep(payload); - const adState = extractAdState(payload); - const negotiatedStreamProfile = extractNegotiatedStreamProfile(payload); - const requestedStreamingFeatures = normalizeStreamingFeatures( - payload.session.sessionRequestData?.requestedStreamingFeatures, - ); - const finalizedStreamingFeatures = normalizeStreamingFeatures( - payload.session.finalizedStreamingFeatures, - ); - const enablePersistingInGameSettings = - typeof payload.session.sessionRequestData?.enablePersistingInGameSettings === "boolean" - ? payload.session.sessionRequestData.enablePersistingInGameSettings - : undefined; - - // Debug logging to trace signaling resolution - const connections = payload.session.connectionInfo ?? []; - const connectionSummary = connections - .map((conn) => { - const rawIp = Array.isArray(conn.ip) ? conn.ip[0] : conn.ip; - return `{usage=${conn.usage},ip=${rawIp ?? "null"},port=${conn.port},resourcePath=${conn.resourcePath ?? "null"}}`; - }) - .join(", "); - console.log( - `[CloudMatch] toSessionInfo: status=${payload.session.status}, ` + - `seatSetupStep=${seatSetupStep ?? "n/a"}, ` + - `queuePosition=${queuePosition ?? "n/a"}, ` + - `connectionInfo=${connections.length} entries, ` + - `serverIp=${signaling.serverIp}, ` + - `signalingServer=${signaling.signalingServer}, ` + - `signalingUrl=${signaling.signalingUrl}, ` + - `connections=[${connectionSummary}]`, - ); - console.log( - `[CloudMatch] negotiated streaming features: requested=${JSON.stringify(requestedStreamingFeatures ?? {})} finalized=${JSON.stringify(finalizedStreamingFeatures ?? {})} cloudGsync=${negotiatedStreamProfile?.enableCloudGsync ?? "n/a"}, reflex=${negotiatedStreamProfile?.enableReflex ?? "n/a"}, l4s=${negotiatedStreamProfile?.enableL4S ?? "n/a"}`, - ); - - return { - sessionId: payload.session.sessionId, - appId: payload.session.sessionRequestData?.appId ?? options.fallbackAppId, - status: payload.session.status, - seatSetupStep, - queuePosition, - adState, - zone, - streamingBaseUrl, - serverIp: signaling.serverIp, - signalingServer: signaling.signalingServer, - signalingUrl: signaling.signalingUrl, - gpuType: payload.session.gpuType, - appLaunchMode: echoedSessionAppLaunchMode(payload) ?? options.fallbackAppLaunchMode, - enablePersistingInGameSettings, - iceServers: await normalizeIceServers(payload), - mediaConnectionInfo: signaling.mediaConnectionInfo, - negotiatedStreamProfile, - requestedStreamingFeatures, - finalizedStreamingFeatures, - clientId, - deviceId, - }; -} - -export async function createSession(input: SessionCreateRequest): Promise { - if (!input.token) { - throw new Error("Missing token for session creation"); - } - - if (!/^\d+$/.test(input.appId)) { - throw new Error(`Invalid launch appId '${input.appId}' (must be numeric)`); - } - - // Generate client/device IDs once for the entire session lifecycle - const clientId = crypto.randomUUID(); - const deviceId = getStableDeviceId(); - - const requestedBase = resolveStreamingBaseUrl(input.zone, input.streamingBaseUrl); - const base = await resolveCreateSessionBase( - requestedBase, - input.token, - clientId, - deviceId, - input.proxyUrl, - ); - const networkTestSessionId = await createNetworkTestSession({ - base, - token: input.token, - clientId, - deviceId, - settings: input.settings, - proxyUrl: input.proxyUrl, - }); - const body = buildSessionRequestBody(input, deviceId, networkTestSessionId); - console.log( - `[CloudMatch] createSession in-game settings persistence: user=${input.enablePersistingInGameSettings === true}, ` + - `gameSupport=${input.supportsInGameSettingsPersistence === true}, ` + - `sent=${body.sessionRequestData.enablePersistingInGameSettings}, ` + - `networkTestSessionId=${networkTestSessionId ?? "none"}`, - ); - - const keyboardLayout = resolveGfnKeyboardLayout(input.settings.keyboardLayout ?? DEFAULT_KEYBOARD_LAYOUT, process.platform); - const languageCode = input.settings.gameLanguage ?? "en_US"; - const url = `${base}/v2/session?${new URLSearchParams({ keyboardLayout, languageCode }).toString()}`; - const response = await fetchCloudMatch(url, { - method: "POST", - headers: buildGfnCloudMatchHeaders({ token: input.token, clientId, deviceId, includeOrigin: true }), - body: JSON.stringify(body), - }, { proxyUrl: input.proxyUrl }); - - const { payload } = await readCloudMatchJson(response); - return await toSessionInfo({ - zone: input.zone, - streamingBaseUrl: base, - payload, - clientId, - deviceId, - fallbackAppId: input.appId, - fallbackAppLaunchMode: appLaunchModeWireValue(input.settings.appLaunchMode), - }); -} - -export async function pollSession(input: SessionPollRequest): Promise { - if (!input.token) { - throw new Error("Missing token for session polling"); - } - - // Use provided client/device IDs if available (should match session creation) - const clientId = input.clientId ?? crypto.randomUUID(); - const deviceId = input.deviceId ?? crypto.randomUUID(); - - const base = resolvePollStopBase(input.zone, input.streamingBaseUrl, input.serverIp); - const baseHost = new URL(base).hostname; - const pollProxyUrl = isZoneHostname(baseHost) ? input.proxyUrl : undefined; - const url = `${base}/v2/session/${input.sessionId}`; - // Polling should NOT include Origin/Referer headers (matches claimSession polling pattern) - const headers = buildGfnCloudMatchHeaders({ token: input.token, clientId, deviceId, includeOrigin: false }); - const response = await fetchCloudMatch(url, { - method: "GET", - headers, - }, { proxyUrl: pollProxyUrl }); - - const { payload } = await readCloudMatchJson(response); - - // Match Rust behavior: if the poll was routed through the zone load balancer - // and the response now contains a real server IP in connectionInfo, re-poll - // directly via the real server IP. This ensures the signaling data and - // connection info are correct (the zone LB may return different data than - // a direct server poll). - const realServerIp = streamingServerIp(payload); - const polledViaZone = isZoneHostname(baseHost); - const realIpDiffers = - realServerIp && - realServerIp.length > 0 && - !isZoneHostname(realServerIp) && - realServerIp !== input.serverIp; - - if (polledViaZone && realIpDiffers && isReadySessionStatus(payload.session.status)) { - // Session is ready and we now know the real server IP — re-poll directly - console.log( - `[CloudMatch] Session ready: re-polling via real server IP ${realServerIp} (was: ${baseHost})`, - ); - const directBase = `https://${realServerIp}`; - const directUrl = `${directBase}/v2/session/${input.sessionId}`; - try { - // The ready-session direct real-IP re-poll intentionally bypasses the session proxy. - const directResponse = await fetchCloudMatch(directUrl, { - method: "GET", - headers, - }); - if (directResponse.ok) { - const directText = await directResponse.text(); - const directPayload = JSON.parse(directText) as CloudMatchResponse; - if (directPayload.requestStatus.statusCode === 1) { - console.log("[CloudMatch] Direct re-poll succeeded, using direct response for signaling info"); - return await toSessionInfo({ zone: input.zone, streamingBaseUrl: directBase, payload: directPayload, clientId, deviceId }); - } - } - } catch (e) { - // Direct poll failed — fall through to use the original zone LB response - console.warn("[CloudMatch] Direct re-poll failed, using zone LB response:", e); - } - } - - return await toSessionInfo({ zone: input.zone, streamingBaseUrl: base, payload, clientId, deviceId }); -} - -export async function reportSessionAd(input: SessionAdReportRequest): Promise { - if (!input.token) { - throw new Error("Missing token for ad update"); - } - - const clientId = input.clientId ?? crypto.randomUUID(); - const deviceId = input.deviceId ?? crypto.randomUUID(); - const base = resolvePollStopBase(input.zone, input.streamingBaseUrl, input.serverIp); - const url = `${base}/v2/session/${input.sessionId}`; - const clientTimestamp = input.clientTimestamp ?? Math.floor(Date.now() / 1000); - const adUpdate = { - adId: input.adId, - adAction: AD_ACTION_CODES[input.action], - clientTimestamp, - ...(typeof input.watchedTimeInMs === "number" - ? { watchedTimeInMs: Math.max(0, Math.round(input.watchedTimeInMs)) } - : {}), - ...(typeof input.pausedTimeInMs === "number" - ? { pausedTimeInMs: Math.max(0, Math.round(input.pausedTimeInMs)) } - : {}), - ...(input.cancelReason ? { cancelReason: input.cancelReason } : {}), - }; - const requestBody = { - action: SESSION_MODIFY_ACTION_AD_UPDATE, - adUpdates: [adUpdate], - }; - - console.log( - `[CloudMatch] reportSessionAd: sending action=${input.action}(${requestBody.adUpdates[0].adAction}), adId=${input.adId}, ` + - `sessionId=${input.sessionId}, zone=${input.zone}, url=${url}, ` + - `cancelReason=${input.cancelReason ?? "n/a"}, errorInfo=${input.errorInfo ?? "n/a"}`, - ); - - const response = await fetchCloudMatch(url, { - method: "PUT", - // Official browser requests include Origin/Referer on cross-origin ad updates. - headers: buildGfnCloudMatchHeaders({ token: input.token, clientId, deviceId, includeOrigin: true }), - body: JSON.stringify(requestBody), - }); - - const { text, payload } = await readCloudMatchJson(response, { - onErrorText: (text) => { - console.warn( - `[CloudMatch] reportSessionAd: backend error status=${response.status}, sessionId=${input.sessionId}, ` + - `adId=${input.adId}, action=${input.action}, body=${text.slice(0, 500)}`, - ); - }, - }); - if (payload.requestStatus.statusCode !== 1) { - console.warn( - `[CloudMatch] reportSessionAd: API error requestStatus=${payload.requestStatus.statusCode}, ` + - `description=${payload.requestStatus.statusDescription ?? "unknown"}, sessionId=${input.sessionId}, ` + - `adId=${input.adId}, action=${input.action}`, - ); - throw SessionError.fromResponse(200, text); - } - - console.log( - `[CloudMatch] reportSessionAd: success sessionId=${input.sessionId}, adId=${input.adId}, action=${input.action}, ` + - `status=${payload.session.status}, queuePosition=${extractQueuePosition(payload) ?? "n/a"}, ` + - `adsRequired=${extractAdState(payload)?.isAdsRequired ?? false}`, - ); - - return await toSessionInfo({ zone: input.zone, streamingBaseUrl: base, payload, clientId, deviceId }); -} - -export async function stopSession(input: SessionStopRequest): Promise { - if (!input.token) { - throw new Error("Missing token for session stop"); - } - - // Use provided client/device IDs if available (should match session creation) - const clientId = input.clientId ?? crypto.randomUUID(); - const deviceId = input.deviceId ?? crypto.randomUUID(); - - const base = resolvePollStopBase(input.zone, input.streamingBaseUrl, input.serverIp); - const url = `${base}/v2/session/${input.sessionId}`; - const response = await fetchCloudMatch(url, { - method: "DELETE", - headers: buildGfnCloudMatchHeaders({ token: input.token, clientId, deviceId, includeOrigin: false }), - }); - - await throwIfCloudMatchResponseError(response); -} - -/** - * Get list of active sessions (status 2 or 3) - * Returns sessions that are Ready or Streaming - */ -export async function getActiveSessions( - token: string, - streamingBaseUrl: string, -): Promise { - if (!token) { - throw new Error("Missing token for getting active sessions"); - } - - const base = normalizeCloudMatchBaseUrl(streamingBaseUrl); - const headers = buildGfnCloudMatchHeaders({ - token, - deviceId: getStableDeviceId(), - includeOrigin: false, - }); - const primary = await fetchActiveSessionsFromBase(base, headers); - if (primary) { - return primary; - } - - for (const fallbackBase of await discoverActiveSessionFallbackBases(base, headers)) { - if (fallbackBase === base) { - continue; - } - const fallback = await fetchActiveSessionsFromBase(fallbackBase, headers); - if (fallback) { - return fallback; - } - } - - return []; -} - -async function discoverActiveSessionFallbackBases( - base: string, - headers: Record, -): Promise { - try { - const response = await fetchCloudMatch(`${base}/v2/serverInfo`, { - method: "GET", - headers, - }); - if (!response.ok) { - return []; - } - return extractServerInfoRegionBases((await response.json()) as CloudMatchServerInfoResponse); - } catch (error) { - console.warn(`[CloudMatch] getActiveSessions fallback discovery failed: ${formatErrorForLog(error)}`); - return []; - } -} - -async function fetchActiveSessionsFromBase( - base: string, - headers: Record, -): Promise { - const url = `${base}/v2/session`; - - let response: Response; - try { - response = await fetchCloudMatch(url, { - method: "GET", - headers, - }, { retries: 0 }); - } catch (error) { - console.warn(`[CloudMatch] getActiveSessions fetch failed for ${base}: ${formatErrorForLog(error)}`); - return null; - } - - const text = await response.text(); - - if (!response.ok) { - console.warn(`Get sessions failed: ${response.status} - ${text.slice(0, 200)}`); - return null; - } - - let sessionsResponse: GetSessionsResponse; - try { - sessionsResponse = JSON.parse(text) as GetSessionsResponse; - } catch { - return []; - } - - if (sessionsResponse.requestStatus.statusCode !== 1) { - console.warn(`Get sessions API error: ${sessionsResponse.requestStatus.statusDescription}`); - return []; - } - - // Filter active sessions: - // 1 = Setup/Queuing (counts against SESSION_LIMIT — must be included for resume logic) - // 2 = Ready - // 3 = Streaming - const activeSessions: ActiveSessionInfo[] = sessionsResponse.sessions - .filter((s) => s.status === 1 || s.status === 2 || s.status === 3) - .map((s) => { - // Extract appId from sessionRequestData - const appId = s.sessionRequestData?.appId ? Number(s.sessionRequestData.appId) : 0; - - // The server echoes the appLaunchMode the session was created with; keep it - // so claim/resume requests can stay session-stable. - const rawAppLaunchMode = s.sessionRequestData?.appLaunchMode; - const appLaunchMode = - typeof rawAppLaunchMode === "number" && Number.isFinite(rawAppLaunchMode) - ? rawAppLaunchMode - : undefined; - const enablePersistingInGameSettings = - typeof s.sessionRequestData?.enablePersistingInGameSettings === "boolean" - ? s.sessionRequestData.enablePersistingInGameSettings - : undefined; - - // Prefer the real server IP from connectionInfo[usage=14] — this is the actual game server, - // not the zone load balancer. sessionControlInfo.ip is the zone LB hostname and cannot - // accept claim (PUT) requests, which causes HTTP 400. - const connInfo = s.connectionInfo?.find((conn) => conn.usage === 14 && conn.ip); - const rawConnIp = connInfo?.ip as string | string[] | undefined; - const connIp = Array.isArray(rawConnIp) ? rawConnIp[0] : rawConnIp; - - const rawControlIp = s.sessionControlInfo?.ip as string | string[] | undefined; - const controlIp = Array.isArray(rawControlIp) ? rawControlIp[0] : rawControlIp; - - const serverIp = connIp ?? controlIp; - - const signalingUrl = connIp - ? `wss://${connIp}:443/nvst/` - : controlIp - ? `wss://${controlIp}:443/nvst/` - : undefined; - - // Extract resolution and fps from monitor settings - const monitorSettings = s.monitorSettings?.[0]; - const resolution = monitorSettings - ? `${monitorSettings.widthInPixels ?? 0}x${monitorSettings.heightInPixels ?? 0}` - : undefined; - const fps = monitorSettings?.framesPerSecond ?? undefined; - - return { - sessionId: s.sessionId, - appId, - appLaunchMode, - enablePersistingInGameSettings, - gpuType: s.gpuType, - status: s.status, - queuePosition: extractSessionQueuePosition(s), - seatSetupStep: extractSessionSeatSetupStep(s), - streamingBaseUrl: base, - serverIp, - signalingUrl, - resolution, - fps, - }; - }); - - return activeSessions; -} - -function formatErrorForLog(error: unknown): string { - if (error instanceof Error) { - const cause = error.cause instanceof Error ? `: ${error.cause.message}` : ""; - return `${error.message}${cause}`; - } - return String(error); -} - -/** - * Build claim/resume request payload - */ -function buildClaimRequestBody( - sessionId: string, - appId: string, - settings: StreamSettings, - sessionAppLaunchMode?: number, - enablePersistingInGameSettings = false, -): unknown { - // For RESUME claims, we must NOT attempt to renegotiate streaming parameters. - // The session is already configured on the server side. Sending different fps, resolution, - // codec, etc. causes HTTP 400 from the server because those parameters are immutable for - // an already-streaming session. Only send the action and minimal required fields. - const deviceId = getStableDeviceId(); - const subSessionId = crypto.randomUUID(); - const timezoneMs = timezoneOffsetMs(); - - return { - action: 2, - data: "RESUME", - sessionRequestData: { - // Minimal fields required for resume - NO streaming parameter renegotiation - audioMode: 2, - remoteControllersBitmap: 0, - sdrHdrMode: 0, - networkTestSessionId: null, - availableSupportedControllers: [], - clientVersion: "30.0", - deviceHashId: deviceId, - internalTitle: null, - clientPlatformName: "windows", - metaData: [ - { key: "SubSessionId", value: subSessionId }, - { key: "wssignaling", value: "1" }, - { key: "GSStreamerType", value: "WebRTC" }, - { key: "networkType", value: "Unknown" }, - { key: "ClientImeSupport", value: "0" }, - { key: "surroundAudioInfo", value: "2" }, - ], - surroundAudioInfo: 0, - clientTimezoneOffset: timezoneMs, - clientIdentification: "GFN-PC", - parentSessionId: null, - appId: parseInt(appId, 10), - streamerVersion: 1, - // Resume must not renegotiate session parameters: prefer the wire value the - // session was created with over whatever the UI toggles currently say. - appLaunchMode: sessionAppLaunchMode ?? appLaunchModeWireValue(settings.appLaunchMode), - sdkVersion: "1.0", - enhancedStreamMode: 1, - useOps: true, - clientDisplayHdrCapabilities: null, - accountLinked: true, - partnerCustomData: "", - enablePersistingInGameSettings, - secureRTSPSupported: false, - userAge: 26, - }, - metaData: [], - }; -} - -/** - * Claim/Resume an existing session - * Required before connecting to an existing session - */ -export async function claimSession(input: SessionClaimRequest): Promise { - if (!input.token) { - throw new Error("Missing token for session claim"); - } - - const deviceId = input.deviceId ?? getStableDeviceId(); - const clientId = input.clientId ?? crypto.randomUUID(); - - // Provide default values for optional parameters - const appId = input.appId ?? "0"; - const settings = input.settings ?? { - resolution: "1920x1080", - fps: 60, - maxBitrateMbps: 75, - codec: "H264", - colorQuality: "8bit_420", - keyboardLayout: DEFAULT_KEYBOARD_LAYOUT, - gameLanguage: "en_US", - enableL4S: false, - enableCloudGsync: false, - }; - const keyboardLayout = resolveGfnKeyboardLayout(settings.keyboardLayout ?? DEFAULT_KEYBOARD_LAYOUT, process.platform); - const languageCode = settings.gameLanguage ?? "en_US"; - - // The session list endpoint returns the zone LB hostname in sessionControlInfo.ip. - // A claim PUT sent to the zone LB returns HTTP 400 because it does not handle - // session-level mutations. The real game server IP is only reliably available from - // the individual session endpoint (GET /v2/session/{id}). Resolve it here before - // building the claim URL. - // IMPORTANT: We must query the SAME zone LB where the session is hosted (use serverIp), - // not the provider's generic streamingBaseUrl (which may route to a different zone LB). - let effectiveServerIp = input.serverIp; - console.log(`[CloudMatch] claimSession: input serverIp=${input.serverIp}, isZone=${isZoneHostname(input.serverIp)}`); - if (isZoneHostname(effectiveServerIp)) { - const zoneBase = `https://${effectiveServerIp}`; - const prefetchUrl = `${zoneBase}/v2/session/${input.sessionId}`; - console.log(`[CloudMatch] claimSession: pre-flight query ${prefetchUrl}`); - const prefetchHeaders = buildGfnCloudMatchHeaders({ token: input.token, clientId, deviceId, includeOrigin: false }); - try { - const prefetchResp = await fetchCloudMatch(prefetchUrl, { method: "GET", headers: prefetchHeaders }); - console.log(`[CloudMatch] claimSession: pre-flight response status=${prefetchResp.status}`); - if (prefetchResp.ok) { - const prefetchPayload = JSON.parse(await prefetchResp.text()) as CloudMatchResponse; - const realIp = streamingServerIp(prefetchPayload); - console.log(`[CloudMatch] claimSession: extracted realIp=${realIp}, isZone=${realIp ? isZoneHostname(realIp) : 'N/A'}`); - if (realIp) { - effectiveServerIp = realIp; - const ipType = isZoneHostname(realIp) ? 'zone LB' : 'direct IP'; - console.log(`[CloudMatch] claimSession: using extracted ${ipType}: ${realIp}`); - } - } else { - console.warn(`[CloudMatch] claimSession: pre-flight returned HTTP ${prefetchResp.status}, text=${await prefetchResp.text()}`); - } - } catch (e) { - console.warn("[CloudMatch] claimSession: pre-flight poll failed, proceeding with zone hostname:", e); - } - } - - const claimUrl = `https://${effectiveServerIp}/v2/session/${input.sessionId}?${new URLSearchParams({ keyboardLayout, languageCode }).toString()}`; - - // Pre-claim validation: check session status before deciding whether to send a RESUME claim. - // Status 1 (setup/launching/queuing) sessions cannot be RESUME'd — the server will reject - // with SESSION_NOT_PAUSED. For these sessions we skip the claim PUT and poll directly. - // Status 2/3 (ready/streaming) sessions are paused and can be RESUME'd normally. - let preClaimStatus: number | null = null; - let shouldSendResumeClaim = true; - try { - const validationUrl = `https://${effectiveServerIp}/v2/session/${input.sessionId}`; - const validationHeaders = buildGfnCloudMatchHeaders({ token: input.token, clientId, deviceId, includeOrigin: false }); - const validationResp = await fetchCloudMatch(validationUrl, { method: "GET", headers: validationHeaders }); - if (validationResp.ok) { - const validationText = await validationResp.text(); - const validationPayload = JSON.parse(validationText) as CloudMatchResponse; - preClaimStatus = validationPayload.session?.status ?? 0; - const errorCode = validationPayload.session?.errorCode ?? 0; - console.log(`[CloudMatch] claimSession: pre-claim validation status=${preClaimStatus}, errorCode=${errorCode}`); - console.log(`[CloudMatch] claimSession: validation response (first 1000 chars): ${validationText.slice(0, 1000)}`); - if (preClaimStatus === 1) { - console.log(`[CloudMatch] claimSession: session is still launching (status=1), skipping RESUME claim — polling directly to ready state`); - } else if ( - input.recoveryMode === true && - (preClaimStatus === 2 || preClaimStatus === 3) - ) { - // Recovery parity: if the session is already ready/streaming, avoid sending - // another RESUME mutation. Repeated RESUME PUTs can rotate signaling hosts - // and push the session back into transient setup/cleanup states. - shouldSendResumeClaim = false; - console.log( - `[CloudMatch] claimSession: recoveryMode and session already ready (status=${preClaimStatus}); skipping redundant RESUME claim`, - ); - } else if (preClaimStatus !== 2 && preClaimStatus !== 3) { - console.warn(`[CloudMatch] claimSession: session not in ready state (status=${preClaimStatus}), claim may fail`); - } - } else { - console.warn(`[CloudMatch] claimSession: pre-claim validation returned HTTP ${validationResp.status}`); - } - } catch (e) { - console.warn("[CloudMatch] claimSession: pre-claim validation failed:", e); - } - - // Only send the RESUME claim PUT if the session is in a paused state (status 2 or 3). - // For status=1 (still launching) we bypass the claim and fall through to the polling loop. - if (preClaimStatus !== 1 && shouldSendResumeClaim) { - const payload = buildClaimRequestBody( - input.sessionId, - appId, - settings, - input.appLaunchMode, - input.enablePersistingInGameSettings === true, - ); - - const headers = buildGfnCloudMatchClaimHeaders({ token: input.token, clientId, deviceId }); - - console.log(`[CloudMatch] claimSession PUT ${claimUrl}`); - console.log(`[CloudMatch] claimSession body: ${JSON.stringify(payload)}`); - const response = await fetchCloudMatch(claimUrl, { - method: "PUT", - headers, - body: JSON.stringify(payload), - }); - - const { text, payload: apiResponse } = await readCloudMatchJson(response, { - onText: (text) => { - console.log(`[CloudMatch] claimSession response: HTTP ${response.status}`); - console.log(`[CloudMatch] claimSession response body FULL: ${text}`); - }, - }); - - if (apiResponse.requestStatus.statusCode !== 1) { - throw SessionError.fromResponse(200, text); - } - } - - // Poll until session is ready (status 2 or 3) - const getUrl = `https://${effectiveServerIp}/v2/session/${input.sessionId}`; - const maxAttempts = 60; - - for (let attempt = 1; attempt <= maxAttempts; attempt++) { - if (attempt > 1) { - await new Promise((resolve) => setTimeout(resolve, 1000)); - } - - const pollHeaders = buildGfnCloudMatchHeaders({ token: input.token, clientId, deviceId, includeOrigin: false }); - - const pollResponse = await fetchCloudMatch(getUrl, { - method: "GET", - headers: pollHeaders, - }); - - if (!pollResponse.ok) { - continue; - } - - const pollText = await pollResponse.text(); - let pollApiResponse: CloudMatchResponse; - - try { - pollApiResponse = JSON.parse(pollText) as CloudMatchResponse; - } catch { - continue; - } - - const sessionData = pollApiResponse.session; - - if (sessionData.status === 2 || sessionData.status === 3) { - // Session is ready - const signaling = resolveSignaling(pollApiResponse); - const queuePosition = extractQueuePosition(pollApiResponse); - const negotiatedStreamProfile = extractNegotiatedStreamProfile(pollApiResponse); - const requestedStreamingFeatures = normalizeStreamingFeatures( - pollApiResponse.session.sessionRequestData?.requestedStreamingFeatures, - ); - const finalizedStreamingFeatures = normalizeStreamingFeatures( - pollApiResponse.session.finalizedStreamingFeatures, - ); - const enablePersistingInGameSettings = - typeof pollApiResponse.session.sessionRequestData?.enablePersistingInGameSettings === "boolean" - ? pollApiResponse.session.sessionRequestData.enablePersistingInGameSettings - : undefined; - console.log( - `[CloudMatch] claimed negotiated streaming features: requested=${JSON.stringify(requestedStreamingFeatures ?? {})} finalized=${JSON.stringify(finalizedStreamingFeatures ?? {})} cloudGsync=${negotiatedStreamProfile?.enableCloudGsync ?? "n/a"}, reflex=${negotiatedStreamProfile?.enableReflex ?? "n/a"}, l4s=${negotiatedStreamProfile?.enableL4S ?? "n/a"}`, - ); - - return { - sessionId: sessionData.sessionId, - appId: input.appId, - status: sessionData.status, - queuePosition, - zone: "", // Zone not applicable for claimed sessions - streamingBaseUrl: `https://${effectiveServerIp}`, - serverIp: signaling.serverIp, - signalingServer: signaling.signalingServer, - signalingUrl: signaling.signalingUrl, - gpuType: sessionData.gpuType, - appLaunchMode: echoedSessionAppLaunchMode(pollApiResponse) ?? input.appLaunchMode, - enablePersistingInGameSettings, - iceServers: await normalizeIceServers(pollApiResponse), - mediaConnectionInfo: signaling.mediaConnectionInfo, - negotiatedStreamProfile: negotiatedStreamProfile ?? extractNegotiatedStreamProfile(pollApiResponse), - requestedStreamingFeatures, - finalizedStreamingFeatures, - clientId, - deviceId, - }; - } - - // Status 1 (setup/launching), 6 (cleaning up), etc. — continue polling for ready state (2 or 3) - // Only break if we encounter a terminal error state (status 4, 5, etc.) - if (sessionData.status > 3 && sessionData.status !== 6) { - break; - } - } - - throw new Error("Session did not become ready after claiming"); -} diff --git a/opennow-stable/src/main/gfn/errorCodes.ts b/opennow-stable/src/main/gfn/errorCodes.ts deleted file mode 100644 index e7f8aa88d..000000000 --- a/opennow-stable/src/main/gfn/errorCodes.ts +++ /dev/null @@ -1,963 +0,0 @@ -import type { SessionErrorInfo } from "@shared/sessionError"; - -/** - * CloudMatch error codes. - * - * These mappings provide user-friendly messages for session failures. - */ - -/** Session error code constants. */ -export enum GfnErrorCode { - // Success codes - Success = 15859712, - - // Client-side errors (3237085xxx - 3237093xxx) - InvalidOperation = 3237085186, - NetworkError = 3237089282, - GetActiveSessionServerError = 3237089283, - AuthTokenNotUpdated = 3237093377, - SessionFinishedState = 3237093378, - ResponseParseFailure = 3237093379, - InvalidServerResponse = 3237093381, - PutOrPostInProgress = 3237093382, - GridServerNotInitialized = 3237093383, - DOMExceptionInSessionControl = 3237093384, - InvalidAdStateTransition = 3237093386, - AuthTokenUpdateTimeout = 3237093387, - - // Server error codes (base 3237093632 + statusCode) - SessionServerErrorBegin = 3237093632, - RequestForbidden = 3237093634, // statusCode 2 - ServerInternalTimeout = 3237093635, // statusCode 3 - ServerInternalError = 3237093636, // statusCode 4 - ServerInvalidRequest = 3237093637, // statusCode 5 - ServerInvalidRequestVersion = 3237093638, // statusCode 6 - SessionListLimitExceeded = 3237093639, // statusCode 7 - InvalidRequestDataMalformed = 3237093640, // statusCode 8 - InvalidRequestDataMissing = 3237093641, // statusCode 9 - RequestLimitExceeded = 3237093642, // statusCode 10 - SessionLimitExceeded = 3237093643, // statusCode 11 - InvalidRequestVersionOutOfDate = 3237093644, // statusCode 12 - SessionEntitledTimeExceeded = 3237093645, // statusCode 13 - AuthFailure = 3237093646, // statusCode 14 - InvalidAuthenticationMalformed = 3237093647, // statusCode 15 - InvalidAuthenticationExpired = 3237093648, // statusCode 16 - InvalidAuthenticationNotFound = 3237093649, // statusCode 17 - EntitlementFailure = 3237093650, // statusCode 18 - InvalidAppIdNotAvailable = 3237093651, // statusCode 19 - InvalidAppIdNotFound = 3237093652, // statusCode 20 - InvalidSessionIdMalformed = 3237093653, // statusCode 21 - InvalidSessionIdNotFound = 3237093654, // statusCode 22 - EulaUnAccepted = 3237093655, // statusCode 23 - MaintenanceStatus = 3237093656, // statusCode 24 - ServiceUnAvailable = 3237093657, // statusCode 25 - SteamGuardRequired = 3237093658, // statusCode 26 - SteamLoginRequired = 3237093659, // statusCode 27 - SteamGuardInvalid = 3237093660, // statusCode 28 - SteamProfilePrivate = 3237093661, // statusCode 29 - InvalidCountryCode = 3237093662, // statusCode 30 - InvalidLanguageCode = 3237093663, // statusCode 31 - MissingCountryCode = 3237093664, // statusCode 32 - MissingLanguageCode = 3237093665, // statusCode 33 - SessionNotPaused = 3237093666, // statusCode 34 - EmailNotVerified = 3237093667, // statusCode 35 - InvalidAuthenticationUnsupportedProtocol = 3237093668, // statusCode 36 - InvalidAuthenticationUnknownToken = 3237093669, // statusCode 37 - InvalidAuthenticationCredentials = 3237093670, // statusCode 38 - SessionNotPlaying = 3237093671, // statusCode 39 - InvalidServiceResponse = 3237093672, // statusCode 40 - AppPatching = 3237093673, // statusCode 41 - GameNotFound = 3237093674, // statusCode 42 - NotEnoughCredits = 3237093675, // statusCode 43 - InvitationOnlyRegistration = 3237093676, // statusCode 44 - RegionNotSupportedForRegistration = 3237093677, // statusCode 45 - SessionTerminatedByAnotherClient = 3237093678, // statusCode 46 - DeviceIdAlreadyUsed = 3237093679, // statusCode 47 - ServiceNotExist = 3237093680, // statusCode 48 - SessionExpired = 3237093681, // statusCode 49 - SessionLimitPerDeviceReached = 3237093682, // statusCode 50 - ForwardingZoneOutOfCapacity = 3237093683, // statusCode 51 - RegionNotSupportedIndefinitely = 3237093684, // statusCode 52 - RegionBanned = 3237093685, // statusCode 53 - RegionOnHoldForFree = 3237093686, // statusCode 54 - RegionOnHoldForPaid = 3237093687, // statusCode 55 - AppMaintenanceStatus = 3237093688, // statusCode 56 - ResourcePoolNotConfigured = 3237093689, // statusCode 57 - InsufficientVmCapacity = 3237093690, // statusCode 58 - InsufficientRouteCapacity = 3237093691, // statusCode 59 - InsufficientScratchSpaceCapacity = 3237093692, // statusCode 60 - RequiredSeatInstanceTypeNotSupported = 3237093693, // statusCode 61 - ServerSessionQueueLengthExceeded = 3237093694, // statusCode 62 - RegionNotSupportedForStreaming = 3237093695, // statusCode 63 - SessionForwardRequestAllocationTimeExpired = 3237093696, // statusCode 64 - SessionForwardGameBinariesNotAvailable = 3237093697, // statusCode 65 - GameBinariesNotAvailableInRegion = 3237093698, // statusCode 66 - UekRetrievalFailed = 3237093699, // statusCode 67 - EntitlementFailureForResource = 3237093700, // statusCode 68 - SessionInQueueAbandoned = 3237093701, // statusCode 69 - MemberTerminated = 3237093702, // statusCode 70 - SessionRemovedFromQueueMaintenance = 3237093703, // statusCode 71 - ZoneMaintenanceStatus = 3237093704, // statusCode 72 - GuestModeCampaignDisabled = 3237093705, // statusCode 73 - RegionNotSupportedAnonymousAccess = 3237093706, // statusCode 74 - InstanceTypeNotSupportedInSingleRegion = 3237093707, // statusCode 75 - InvalidZoneForQueuedSession = 3237093710, // statusCode 78 - SessionWaitingAdsTimeExpired = 3237093711, // statusCode 79 - UserCancelledWatchingAds = 3237093712, // statusCode 80 - StreamingNotAllowedInLimitedMode = 3237093713, // statusCode 81 - ForwardRequestJPMFailed = 3237093714, // statusCode 82 - MaxSessionNumberLimitExceeded = 3237093715, // statusCode 83 - GuestModePartnerCapacityDisabled = 3237093716, // statusCode 84 - SessionRejectedNoCapacity = 3237093717, // statusCode 85 - SessionInsufficientPlayabilityLevel = 3237093718, // statusCode 86 - ForwardRequestLOFNFailed = 3237093719, // statusCode 87 - InvalidTransportRequest = 3237093720, // statusCode 88 - UserStorageNotAvailable = 3237093721, // statusCode 89 - GfnStorageNotAvailable = 3237093722, // statusCode 90 - AppNotAllowedToStream = 3237093723, // statusCode 91 - SessionServerErrorEnd = 3237093887, - - // Session setup cancelled - SessionSetupCancelled = 15867905, - SessionSetupCancelledDuringQueuing = 15867906, - RequestCancelled = 15867907, - SystemSleepDuringSessionSetup = 15867909, - NoInternetDuringSessionSetup = 15868417, - - // Network errors (3237101xxx) - SocketError = 3237101580, - AddressResolveFailed = 3237101581, - ConnectFailed = 3237101582, - SslError = 3237101583, - ConnectionTimeout = 3237101584, - DataReceiveTimeout = 3237101585, - PeerNoResponse = 3237101586, - UnexpectedHttpRedirect = 3237101587, - DataSendFailure = 3237101588, - DataReceiveFailure = 3237101589, - CertificateRejected = 3237101590, - DataNotAllowed = 3237101591, - NetworkErrorUnknown = 3237101592, -} - -/** Error message entry with title and description */ -interface ErrorMessageEntry { - title: string; - description: string; -} - -/** User-friendly error messages map */ -export const ERROR_MESSAGES: Map = new Map([ - // Success - [15859712, { title: "Success", description: "Session started successfully." }], - - // Client errors - [ - 3237085186, - { - title: "Invalid Operation", - description: "The requested operation is not valid at this time.", - }, - ], - [ - 3237089282, - { - title: "Network Error", - description: "A network error occurred. Please check your internet connection.", - }, - ], - [ - 3237093377, - { - title: "Authentication Required", - description: "Your session has expired. Please log in again.", - }, - ], - [ - 3237093379, - { - title: "Server Response Error", - description: "Failed to parse server response. Please try again.", - }, - ], - [ - 3237093381, - { - title: "Invalid Server Response", - description: "The server returned an invalid response.", - }, - ], - [ - 3237093384, - { - title: "Session Error", - description: "An error occurred during session setup.", - }, - ], - [ - 3237093387, - { - title: "Authentication Timeout", - description: "Authentication token update timed out. Please log in again.", - }, - ], - - // Server errors - [ - 3237093634, - { - title: "Access Forbidden", - description: "Access to this service is forbidden.", - }, - ], - [ - 3237093635, - { - title: "Server Timeout", - description: "The server timed out. Please try again.", - }, - ], - [ - 3237093636, - { - title: "Server Error", - description: "An internal server error occurred. Please try again later.", - }, - ], - [ - 3237093637, - { - title: "Invalid Request", - description: "The request was invalid.", - }, - ], - [ - 3237093639, - { - title: "Too Many Sessions", - description: "You have too many active sessions. Please close some sessions and try again.", - }, - ], - [ - 3237093643, - { - title: "Session Limit Exceeded", - description: "You have reached your session limit. Another session may already be running on your account.", - }, - ], - [ - 3237093645, - { - title: "Session Time Exceeded", - description: "Your session time has been exceeded.", - }, - ], - [ - 3237093646, - { - title: "Authentication Failed", - description: "Authentication failed. Please log in again.", - }, - ], - [ - 3237093648, - { - title: "Session Expired", - description: "Your authentication has expired. Please log in again.", - }, - ], - [ - 3237093650, - { - title: "Entitlement Error", - description: "You don't have access to this game or service.", - }, - ], - [ - 3237093651, - { - title: "Game Not Available", - description: "This game is not currently available.", - }, - ], - [ - 3237093652, - { - title: "Game Not Found", - description: "This game was not found in the library.", - }, - ], - [ - 3237093655, - { - title: "EULA Required", - description: "You must accept the End User License Agreement to continue.", - }, - ], - [ - 3237093656, - { - title: "Under Maintenance", - description: "The service is currently under maintenance. Please try again later.", - }, - ], - [ - 3237093657, - { - title: "Service Unavailable", - description: "The service is temporarily unavailable. Please try again later.", - }, - ], - [ - 3237093658, - { - title: "Steam Guard Required", - description: "Steam Guard authentication is required. Please complete Steam Guard verification.", - }, - ], - [ - 3237093659, - { - title: "Steam Login Required", - description: "You need to link your Steam account to play this game.", - }, - ], - [ - 3237093660, - { - title: "Steam Guard Invalid", - description: "Steam Guard code is invalid. Please try again.", - }, - ], - [ - 3237093661, - { - title: "Steam Profile Private", - description: "Your Steam profile is private. Please make it public or friends-only.", - }, - ], - [ - 3237093667, - { - title: "Email Not Verified", - description: "Please verify your email address to continue.", - }, - ], - [ - 3237093673, - { - title: "Game Updating", - description: "This game is currently being updated. Please try again later.", - }, - ], - [ - 3237093674, - { - title: "Game Not Found", - description: "This game was not found.", - }, - ], - [ - 3237093675, - { - title: "Insufficient Credits", - description: "You don't have enough credits for this session.", - }, - ], - [ - 3237093678, - { - title: "Session Taken Over", - description: "Your session was taken over by another device.", - }, - ], - [ - 3237093681, - { - title: "Session Expired", - description: "Your session has expired.", - }, - ], - [ - 3237093682, - { - title: "Device Limit Reached", - description: "You have reached the session limit for this device.", - }, - ], - [ - 3237093683, - { - title: "Region At Capacity", - description: "Your region is currently at capacity. Please try again later.", - }, - ], - [ - 3237093684, - { - title: "Region Not Supported", - description: "The service is not available in your region.", - }, - ], - [ - 3237093685, - { - title: "Region Banned", - description: "The service is not available in your region.", - }, - ], - [ - 3237093686, - { - title: "Free Tier On Hold", - description: "Free tier is temporarily unavailable in your region.", - }, - ], - [ - 3237093687, - { - title: "Paid Tier On Hold", - description: "Paid tier is temporarily unavailable in your region.", - }, - ], - [ - 3237093688, - { - title: "Game Maintenance", - description: "This game is currently under maintenance.", - }, - ], - [ - 3237093690, - { - title: "No Capacity", - description: "No gaming rigs are available right now. Please try again later or join the queue.", - }, - ], - [ - 3237093694, - { - title: "Queue Full", - description: "The queue is currently full. Please try again later.", - }, - ], - [ - 3237093695, - { - title: "GeForce NOW Unavailable in Your Region", - description: - "GeForce NOW has restricted streaming in your region. This is not an OpenNOW issue — NVIDIA has blocked access from your location. You may need to use a VPN or check GeForce NOW's supported countries list.", - }, - ], - [ - 3237093698, - { - title: "Game Not Available", - description: "This game is not available in your region.", - }, - ], - [ - 3237093701, - { - title: "Queue Abandoned", - description: "Your session in queue was abandoned.", - }, - ], - [ - 3237093702, - { - title: "Account Terminated", - description: "Your account has been terminated.", - }, - ], - [ - 3237093703, - { - title: "Queue Maintenance", - description: "The queue was cleared due to maintenance.", - }, - ], - [ - 3237093704, - { - title: "Zone Maintenance", - description: "This server zone is under maintenance.", - }, - ], - [ - 3237093711, - { - title: "Ads Timeout", - description: "Session expired while waiting for ads. Free tier users must watch ads to play. Please start a new session.", - }, - ], - [ - 3237093712, - { - title: "Ads Cancelled", - description: "Session cancelled because ads were skipped. Free tier users must watch ads to play.", - }, - ], - [ - 3237093713, - { - title: "Limited Mode", - description: "Streaming is not allowed in limited mode.", - }, - ], - [ - 3237093715, - { - title: "Session Limit", - description: "Maximum number of sessions reached.", - }, - ], - [ - 3237093717, - { - title: "No Capacity", - description: "No gaming rigs are available. Please try again later.", - }, - ], - [ - 3237093718, - { - title: "Membership Upgrade Required", - description: "Your current GeForce NOW membership is not high enough to play this game. Upgrade to a higher tier and try again.", - }, - ], - [ - 3237093721, - { - title: "Storage Unavailable", - description: "User storage is not available.", - }, - ], - [ - 3237093722, - { - title: "Storage Error", - description: "Service storage is not available.", - }, - ], - [ - 3237093723, - { - title: "Streaming Not Allowed", - description: "This app is not allowed to stream on your current GeForce NOW account or region.", - }, - ], - - // Cancellation - [ - 15867905, - { - title: "Session Cancelled", - description: "Session setup was cancelled.", - }, - ], - [ - 15867906, - { - title: "Queue Cancelled", - description: "You left the queue.", - }, - ], - [ - 15867907, - { - title: "Request Cancelled", - description: "The request was cancelled.", - }, - ], - [ - 15867909, - { - title: "System Sleep", - description: "Session setup was interrupted by system sleep.", - }, - ], - [ - 15868417, - { - title: "No Internet", - description: "No internet connection during session setup.", - }, - ], - - // Network errors - [ - 3237101580, - { - title: "Socket Error", - description: "A socket error occurred. Please check your network.", - }, - ], - [ - 3237101581, - { - title: "DNS Error", - description: "Failed to resolve server address. Please check your network.", - }, - ], - [ - 3237101582, - { - title: "Connection Failed", - description: "Failed to connect to the server. Please check your network.", - }, - ], - [ - 3237101583, - { - title: "SSL Error", - description: "A secure connection error occurred.", - }, - ], - [ - 3237101584, - { - title: "Connection Timeout", - description: "Connection timed out. Please check your network.", - }, - ], - [ - 3237101585, - { - title: "Receive Timeout", - description: "Data receive timed out. Please check your network.", - }, - ], - [ - 3237101586, - { - title: "No Response", - description: "Server not responding. Please try again.", - }, - ], - [ - 3237101590, - { - title: "Certificate Error", - description: "Server certificate was rejected.", - }, - ], -]); - -/** CloudMatch error response structure */ -interface CloudMatchErrorResponse { - requestStatus?: { - statusCode?: number; - statusDescription?: string; - unifiedErrorCode?: number; - }; - session?: { - sessionId?: string; - errorCode?: number; - }; -} - -/** Session error class for parsing and handling CloudMatch errors */ -export class SessionError extends Error { - /** HTTP status code */ - public readonly httpStatus: number; - /** CloudMatch status code from requestStatus.statusCode */ - public readonly statusCode: number; - /** Status description from requestStatus.statusDescription */ - public readonly statusDescription?: string; - /** Unified error code from requestStatus.unifiedErrorCode */ - public readonly unifiedErrorCode?: number; - /** Session error code from session.errorCode */ - public readonly sessionErrorCode?: number; - /** Computed service error code */ - public readonly gfnErrorCode: number; - /** User-friendly title */ - public readonly title: string; - - constructor(info: SessionErrorInfo) { - super(info.description); - this.name = "SessionError"; - this.httpStatus = info.httpStatus; - this.statusCode = info.statusCode; - this.statusDescription = info.statusDescription; - this.unifiedErrorCode = info.unifiedErrorCode; - this.sessionErrorCode = info.sessionErrorCode; - this.gfnErrorCode = info.gfnErrorCode; - this.title = info.title; - } - - /** Get error type as a string (e.g., "SessionLimitExceeded") */ - get errorType(): string { - // Try to find the enum name from the error code - const entry = Object.entries(GfnErrorCode).find(([, value]) => value === this.gfnErrorCode); - if (entry) { - return entry[0]; - } - // Fallback to status code based naming - if (this.statusCode > 0) { - return `StatusCode${this.statusCode}`; - } - return "UnknownError"; - } - - /** Get user-friendly error message */ - get errorDescription(): string { - return this.message; - } - - /** - * Parse error from CloudMatch response JSON - */ - static fromResponse(httpStatus: number, responseBody: string): SessionError { - let json: CloudMatchErrorResponse = {}; - - try { - json = JSON.parse(responseBody) as CloudMatchErrorResponse; - } catch { - // Parsing failed, use empty object - } - - // Extract fields - const statusCode = json.requestStatus?.statusCode ?? 0; - const statusDescription = json.requestStatus?.statusDescription; - const unifiedErrorCode = json.requestStatus?.unifiedErrorCode; - const sessionErrorCode = json.session?.errorCode; - - // Compute normalized service error code - const gfnErrorCode = SessionError.computeErrorCode(statusCode, unifiedErrorCode); - - // Get user-friendly message - const { title, description } = SessionError.getErrorMessage( - gfnErrorCode, - statusDescription, - httpStatus, - ); - - return new SessionError({ - httpStatus, - statusCode, - statusDescription, - unifiedErrorCode, - sessionErrorCode, - gfnErrorCode, - title, - description, - }); - } - - /** - * Compute service error code from CloudMatch response - */ - private static computeErrorCode(statusCode: number, unifiedErrorCode?: number): number { - // Base error code - let errorCode: number = 3237093632; // SessionServerErrorBegin - - // Convert statusCode to error code - if (statusCode === 1) { - errorCode = 15859712; // Success - } else if (statusCode > 0 && statusCode < 255) { - errorCode = 3237093632 + statusCode; - } - - // Use unifiedErrorCode if available and error_code is generic - if (unifiedErrorCode !== undefined) { - switch (errorCode) { - case 3237093632: // SessionServerErrorBegin - case 3237093636: // ServerInternalError - case 3237093381: // InvalidServerResponse - errorCode = unifiedErrorCode; - break; - } - } - - return errorCode; - } - - /** - * Get user-friendly error message - */ - private static getErrorMessage( - errorCode: number, - statusDescription: string | undefined, - httpStatus: number, - ): { title: string; description: string } { - // Check for known error code - const knownError = ERROR_MESSAGES.get(errorCode); - if (knownError) { - return knownError; - } - - // Parse status description for known patterns - if (statusDescription) { - const descUpper = statusDescription.toUpperCase(); - - if (descUpper.includes("INSUFFICIENT_PLAYABILITY")) { - return { - title: "Membership Upgrade Required", - description: - "Your current GeForce NOW membership is not high enough to play this game. Upgrade to a higher tier and try again.", - }; - } - - if (descUpper.includes("SESSION_LIMIT")) { - return { - title: "Session Limit Exceeded", - description: "You have reached your maximum number of concurrent sessions.", - }; - } - - if (descUpper.includes("MAINTENANCE")) { - return { - title: "Under Maintenance", - description: "The service is currently under maintenance. Please try again later.", - }; - } - - if (descUpper.includes("CAPACITY") || descUpper.includes("QUEUE")) { - return { - title: "No Capacity Available", - description: "All gaming rigs are currently in use. Please try again later.", - }; - } - - if (descUpper.includes("AUTH") || descUpper.includes("TOKEN")) { - return { - title: "Authentication Error", - description: "Please log in again.", - }; - } - - if (descUpper.includes("ENTITLEMENT")) { - return { - title: "Access Denied", - description: "You don't have access to this game or service.", - }; - } - } - - // Fallback based on HTTP status - switch (httpStatus) { - case 401: - return { - title: "Unauthorized", - description: "Please log in again.", - }; - case 403: - return { - title: "Access Denied", - description: "Access to this resource was denied.", - }; - case 404: - return { - title: "Not Found", - description: "The requested resource was not found.", - }; - case 429: - return { - title: "Too Many Requests", - description: "Please wait a moment and try again.", - }; - } - - if (httpStatus >= 500 && httpStatus < 600) { - return { - title: "Server Error", - description: "A server error occurred. Please try again later.", - }; - } - - return { - title: "Error", - description: `An error occurred (HTTP ${httpStatus}).`, - }; - } - - /** - * Check if this error indicates another session is running - */ - isSessionConflict(): boolean { - const sessionConflictCodes = [ - GfnErrorCode.SessionLimitExceeded, // 3237093643 - GfnErrorCode.SessionLimitPerDeviceReached, // 3237093682 - GfnErrorCode.MaxSessionNumberLimitExceeded, // 3237093715 - ]; - - if (sessionConflictCodes.includes(this.gfnErrorCode)) { - return true; - } - - return false; - } - - /** - * Check if this is a temporary error that might resolve with retry - */ - isRetryable(): boolean { - const retryableCodes = [ - GfnErrorCode.NetworkError, // 3237089282 - GfnErrorCode.ServerInternalTimeout, // 3237093635 - GfnErrorCode.ServerInternalError, // 3237093636 - GfnErrorCode.ForwardingZoneOutOfCapacity, // 3237093683 - GfnErrorCode.InsufficientVmCapacity, // 3237093690 - GfnErrorCode.SessionRejectedNoCapacity, // 3237093717 - GfnErrorCode.ConnectionTimeout, // 3237101584 - GfnErrorCode.DataReceiveTimeout, // 3237101585 - GfnErrorCode.PeerNoResponse, // 3237101586 - ]; - - return retryableCodes.includes(this.gfnErrorCode); - } - - /** - * Check if user needs to log in again - */ - needsReauth(): boolean { - const reauthCodes = [ - GfnErrorCode.AuthTokenNotUpdated, // 3237093377 - GfnErrorCode.AuthTokenUpdateTimeout, // 3237093387 - GfnErrorCode.AuthFailure, // 3237093646 - GfnErrorCode.InvalidAuthenticationMalformed, // 3237093647 - GfnErrorCode.InvalidAuthenticationExpired, // 3237093648 - GfnErrorCode.InvalidAuthenticationNotFound, // 3237093649 - GfnErrorCode.InvalidAuthenticationUnsupportedProtocol, // 3237093668 - GfnErrorCode.InvalidAuthenticationUnknownToken, // 3237093669 - GfnErrorCode.InvalidAuthenticationCredentials, // 3237093670 - ]; - - if (reauthCodes.includes(this.gfnErrorCode)) { - return true; - } - - if (this.httpStatus === 401) { - return true; - } - - return false; - } - - /** - * Convert to a plain object for serialization - */ - toJSON(): SessionErrorInfo { - return { - httpStatus: this.httpStatus, - statusCode: this.statusCode, - statusDescription: this.statusDescription, - unifiedErrorCode: this.unifiedErrorCode, - sessionErrorCode: this.sessionErrorCode, - gfnErrorCode: this.gfnErrorCode, - title: this.title, - description: this.message, - }; - } -} - -/** Helper function to check if an error is a SessionError */ -export function isSessionError(error: unknown): error is SessionError { - return error instanceof SessionError; -} - -/** Helper function to parse error from CloudMatch response */ -export function parseCloudMatchError(httpStatus: number, responseBody: string): SessionError { - return SessionError.fromResponse(httpStatus, responseBody); -} diff --git a/opennow-stable/src/main/gfn/games.ts b/opennow-stable/src/main/gfn/games.ts deleted file mode 100644 index 2d7ea1305..000000000 --- a/opennow-stable/src/main/gfn/games.ts +++ /dev/null @@ -1,1513 +0,0 @@ -import type { - CatalogBrowseRequest, - CatalogBrowseResult, - CatalogFilterGroup, - CatalogSortOption, - GameCatalogSkuStrings, - GameInfo, - GamePanelResult, - GameVariant, - MarkGameOwnedResult, -} from "@shared/gfn"; -import { createHash } from "node:crypto"; -import { isOwnedLibraryStatus, normalizeGameStore } from "@shared/gfn"; -import { cacheManager } from "../services/cacheManager"; -import { appendPublicGameSearchMatches, fetchPublicGamesUncached, mergePublicGameVariants } from "./publicGames"; -import { - buildGfnGraphQlHeaders, - buildGfnLcarsHeaders, -} from "./clientHeaders"; -import { fetchAllAppsPages, type AppsPageResponse } from "./paginatedApps"; -import { fetchWithOptionalProxy } from "./proxyFetch"; -import { sessionProxyCacheKeyPart, sessionProxyHasCredentials } from "./proxyUrl"; -import { supportsInGameSettingsPersistence } from "./gameFeatures"; -import { fetchLcarsGraphQl, postLcarsMutation } from "./lcarsGraphql"; - -const GRAPHQL_URL = "https://games.geforce.com/graphql"; -const DEFAULT_LOCALE = "en_US"; -const DEFAULT_CATALOG_FETCH_COUNT = 120; -const MAX_CATALOG_PAGES = 3; -const LIBRARY_FETCH_COUNT = 200; -const MAX_LIBRARY_PAGES = 25; -const DEFAULT_SORT_ID = "relevance"; -const DEFAULT_LIBRARY_SORT = "variants.gfn.library.lastPlayedDate:DESC,computedValues.libraryAddedDate:DESC,sortName:ASC"; -const LIBRARY_GAMES_CACHE_SCOPE = "library:v2"; -const CATALOG_GAMES_CACHE_SCOPE = "catalog"; -const PUBLIC_GAMES_CACHE_KEY = "games:public:v2"; -const DEFAULT_CLOUDMATCH_BASE_URL = "https://prod.cloudmatchbeta.nvidiagrid.net/"; -const GFN_FEATURE_FIELDS = ` - __typename - ... on GfnSubscriptionFeatureValue { - key - value - } - ... on GfnSubscriptionFeatureValueList { - key - values - } -`; -const LIBRARY_APPS_FILTER = { - variants: { - gfn: { - library: { - status: { - notEquals: "NOT_OWNED", - }, - }, - }, - }, -} satisfies Record; - -function addProxyCacheScope(hash: ReturnType, proxyUrl?: string): void { - const proxyCachePart = sessionProxyCacheKeyPart(proxyUrl); - if (proxyCachePart) { - hash.update("\0").update(proxyCachePart); - } -} - -function publicGamesCacheKey(proxyUrl?: string): string { - const proxyCachePart = sessionProxyCacheKeyPart(proxyUrl); - return proxyCachePart ? `${PUBLIC_GAMES_CACHE_KEY}:${proxyCachePart}` : PUBLIC_GAMES_CACHE_KEY; -} - -function shouldBypassGamesCache(proxyUrl?: string): boolean { - return sessionProxyHasCredentials(proxyUrl); -} - -function accountScopedGamesCacheKey(scope: string, accountId: string, providerStreamingBaseUrl?: string, proxyUrl?: string): string { - const hash = createHash("sha256") - .update(accountId) - .update("\0") - .update(providerStreamingBaseUrl ?? ""); - addProxyCacheScope(hash, proxyUrl); - const digest = hash.digest("hex").slice(0, 16); - return `games:${scope}:${digest}`; -} - -function legacyTokenScopedGamesCacheKey(scope: string, token: string, providerStreamingBaseUrl?: string, proxyUrl?: string): string { - const hash = createHash("sha256") - .update(token) - .update("\0") - .update(providerStreamingBaseUrl ?? ""); - addProxyCacheScope(hash, proxyUrl); - const digest = hash.digest("hex").slice(0, 16); - return `games:${scope}:${digest}`; -} - -function resolveAccountCacheId(accountId: string | undefined, token: string): string { - return accountId?.trim() || token; -} - -async function loadAccountScopedFromCache( - scope: string, - accountId: string | undefined, - token: string, - providerStreamingBaseUrl?: string, - proxyUrl?: string, -): Promise>>> { - if (shouldBypassGamesCache(proxyUrl)) { - return null; - } - - const resolvedAccountId = resolveAccountCacheId(accountId, token); - const primaryKey = accountScopedGamesCacheKey(scope, resolvedAccountId, providerStreamingBaseUrl, proxyUrl); - const cached = await cacheManager.loadFromCache(primaryKey); - if (cached) { - return cached; - } - - if (resolvedAccountId !== token) { - const legacyKey = legacyTokenScopedGamesCacheKey(scope, token, providerStreamingBaseUrl, proxyUrl); - if (legacyKey !== primaryKey) { - const legacy = await cacheManager.loadFromCache(legacyKey); - if (legacy) { - void cacheManager.saveToCache(primaryKey, legacy.data); - void cacheManager.invalidateCache(legacyKey); - return legacy; - } - } - } - - return null; -} - -function catalogBrowseCacheKey(input: CatalogBrowseRequest, accountId: string): string { - const queryDigest = createHash("sha256") - .update(input.searchQuery?.trim() ?? "") - .update("\0") - .update(input.sortId ?? "") - .update("\0") - .update((input.filterIds ?? []).join(",")) - .update("\0") - .update(String(input.fetchCount ?? "")) - .digest("hex") - .slice(0, 12); - return `${getAccountCatalogGamesCachePrefix(accountId, input.providerStreamingBaseUrl, input.proxyUrl)}:${queryDigest}`; -} - -export function getAccountCatalogGamesCachePrefix( - accountId: string, - providerStreamingBaseUrl?: string, - proxyUrl?: string, -): string { - return accountScopedGamesCacheKey(CATALOG_GAMES_CACHE_SCOPE, accountId, providerStreamingBaseUrl, proxyUrl); -} - -export function getAccountGamesCacheKeys(accountId: string, providerStreamingBaseUrl?: string, proxyUrl?: string): { - main: string; - featured: string; - storePanels: string; - library: string; - catalogPrefix: string; - public: string; -} { - return { - main: accountScopedGamesCacheKey("main", accountId, providerStreamingBaseUrl, proxyUrl), - featured: accountScopedGamesCacheKey("featured", accountId, providerStreamingBaseUrl, proxyUrl), - storePanels: accountScopedGamesCacheKey("store-panels", accountId, providerStreamingBaseUrl, proxyUrl), - library: accountScopedGamesCacheKey(LIBRARY_GAMES_CACHE_SCOPE, accountId, providerStreamingBaseUrl, proxyUrl), - catalogPrefix: getAccountCatalogGamesCachePrefix(accountId, providerStreamingBaseUrl, proxyUrl), - public: publicGamesCacheKey(proxyUrl), - }; -} - -export function getLegacyTokenScopedAccountGamesCacheKeys(token: string, providerStreamingBaseUrl?: string, proxyUrl?: string): { - main: string; - featured: string; - storePanels: string; - library: string; - catalogPrefix: string; -} { - return { - main: legacyTokenScopedGamesCacheKey("main", token, providerStreamingBaseUrl, proxyUrl), - featured: legacyTokenScopedGamesCacheKey("featured", token, providerStreamingBaseUrl, proxyUrl), - storePanels: legacyTokenScopedGamesCacheKey("store-panels", token, providerStreamingBaseUrl, proxyUrl), - library: legacyTokenScopedGamesCacheKey(LIBRARY_GAMES_CACHE_SCOPE, token, providerStreamingBaseUrl, proxyUrl), - catalogPrefix: legacyTokenScopedGamesCacheKey(CATALOG_GAMES_CACHE_SCOPE, token, providerStreamingBaseUrl, proxyUrl), - }; -} - -export interface AccountGameCacheInvalidationInput { - userId: string; - providerStreamingBaseUrl?: string; - tokens?: Array; - proxyUrl?: string; - logPrefix?: string; -} - -export async function invalidateAccountGameCaches(input: AccountGameCacheInvalidationInput): Promise { - const cacheKeySets: Array<{ main: string; featured: string; storePanels: string; library: string; catalogPrefix: string }> = [ - getAccountGamesCacheKeys(input.userId, input.providerStreamingBaseUrl), - ]; - const legacyTokens = [...new Set((input.tokens ?? []).filter((token): token is string => Boolean(token)))]; - cacheKeySets.push( - ...legacyTokens.map((token) => getLegacyTokenScopedAccountGamesCacheKeys(token, input.providerStreamingBaseUrl)), - ); - - if (input.proxyUrl?.trim()) { - try { - cacheKeySets.push(getAccountGamesCacheKeys(input.userId, input.providerStreamingBaseUrl, input.proxyUrl)); - cacheKeySets.push( - ...legacyTokens.map((token) => getLegacyTokenScopedAccountGamesCacheKeys(token, input.providerStreamingBaseUrl, input.proxyUrl)), - ); - } catch (error) { - console.warn(`${input.logPrefix ?? "[Games]"} Skipping proxy-scoped game cache invalidation:`, error); - } - } - - const invalidations = new Map>(); - for (const keys of cacheKeySets) { - invalidations.set(keys.main, cacheManager.invalidateCache(keys.main)); - invalidations.set(keys.featured, cacheManager.invalidateCache(keys.featured)); - invalidations.set(keys.storePanels, cacheManager.invalidateCache(keys.storePanels)); - invalidations.set(keys.library, cacheManager.invalidateCache(keys.library)); - invalidations.set(keys.catalogPrefix, cacheManager.invalidateCachesByPrefix(keys.catalogPrefix)); - } - await Promise.allSettled(invalidations.values()); -} - -export interface MarkGameOwnedInput { - token: string; - userId: string; - variantId: string; - providerStreamingBaseUrl?: string; - proxyUrl?: string; - tokens?: Array; -} - -interface GraphQlResponse { - data?: { - panels: Array<{ - id?: string; - name: string; - sections: Array<{ - id?: string; - title?: string; - items: Array<{ - __typename: string; - app?: AppData; - }>; - }>; - }>; - }; - errors?: Array<{ message: string }>; -} - -interface AppMetaDataResponse { - data?: { - apps: { - items: AppData[]; - }; - }; - errors?: Array<{ message: string }>; -} - -interface FilterSortDefinitionsResponse { - data?: { - filterGroupDefinitions?: GraphQlFilterGroup[]; - sortOrderDefinitions?: Array<{ - id: string; - label: string; - orderBy: string; - }>; - }; - errors?: Array<{ message: string }>; -} - -interface AppsSearchResponse { - data?: { - apps?: { - numberReturned?: number; - numberSupported?: number; - pageInfo?: { - hasNextPage?: boolean; - endCursor?: string; - totalCount?: number; - }; - items?: AppData[]; - }; - }; - errors?: Array<{ message: string }>; -} - -interface AddOwnedVariantResponse { - data?: { - addOwnedVariant?: { - app?: { - id?: string; - }; - }; - }; - errors?: Array<{ message: string }>; -} - -type AppsPage = AppsPageResponse; - -interface GraphQlFilterGroup { - id: string; - label: string; - filters?: Array<{ - id: string; - label: string; - filters?: string[]; - }>; -} - -interface AppData { - id: string; - title: string; - shortName?: string; - description?: string; - longDescription?: string; - developerName?: string; - features?: unknown[]; - gameFeatures?: unknown[]; - appFeatures?: unknown[]; - genres?: unknown[]; - tags?: unknown[]; - supportedControls?: unknown[]; - nvidiaTech?: unknown[]; - maxLocalPlayers?: number; - maxOnlinePlayers?: number; - images?: Record; - publisherName?: string; - contentRatings?: unknown[]; - variants?: Array<{ - id: string; - appStore: string; - storeUrl?: string; - supportedControls?: string[]; - gfn?: { - status?: string; - features?: unknown; - library?: { - status?: string; - selected?: boolean; - lastPlayedDate?: string; - }; - }; - }>; - gfn?: { - playType?: string; - playabilityState?: string; - minimumMembershipTierLabel?: string; - catalogSkuStrings?: GameCatalogSkuStrings; - }; - itemMetadata?: { - campaignIds?: string[]; - }; -} - -interface ServerInfoResponse { - requestStatus?: { - serverId?: string; - }; -} - -interface AppResolution { - numericAppId?: string; - preferredVariantId?: string; - selectedVariantIndex: number; - lastPlayed?: string; - isInLibrary: boolean; -} - -interface CatalogDefinitions { - filterGroups: CatalogFilterGroup[]; - sortOptions: CatalogSortOption[]; - filterPayloadById: Record; -} - -const LANDSCAPE_IMAGE_KEYS = ["MARQUEE_HERO_IMAGE", "HERO_IMAGE", "TV_BANNER", "FEATURE_IMAGE", "KEY_IMAGE", "KEY_ART"] as const; -const POSTER_IMAGE_KEYS = ["GAME_BOX_ART", "KEY_IMAGE", "KEY_ART"] as const; - -function optimizeImage(url: string, width = 272): string { - if (url.includes("img.nvidiagrid.net")) { - return `${url};f=webp;w=${width}`; - } - return url; -} - -function normalizeImageValues(value: string | string[] | undefined, width: number): string[] { - const values = Array.isArray(value) ? value : value ? [value] : []; - return [...new Set(values.map((url) => url.trim()).filter(Boolean).map((url) => optimizeImage(url, width)))]; -} - -function getFirstImage(images: AppData["images"], keys: readonly string[], width: number): string | undefined { - if (!images) return undefined; - for (const key of keys) { - const value = normalizeImageValues(images[key], width)[0]; - if (value) return value; - } - return undefined; -} - -function getImageUrlsByType(images: AppData["images"]): Record | undefined { - if (!images) return undefined; - const entries = Object.entries(images) - .map(([key, value]) => [key, normalizeImageValues(value, 1200)] as const) - .filter(([, urls]) => urls.length > 0); - return entries.length > 0 ? Object.fromEntries(entries) : undefined; -} - -function isNumericId(value: string | undefined): value is string { - if (!value) { - return false; - } - return /^\d+$/.test(value); -} - -async function postGraphQl( - query: string, - variables: Record, - token?: string, - proxyUrl?: string, -): Promise { - const response = await fetchWithOptionalProxy(GRAPHQL_URL, { - method: "POST", - headers: buildGfnGraphQlHeaders(token), - body: JSON.stringify({ query, variables }), - }, proxyUrl); - - if (!response.ok) { - const text = await response.text(); - throw new Error(`GFN GraphQL failed (${response.status}): ${text.slice(0, 400)}`); - } - - return (await response.json()) as T; -} - -async function getVpcId(token: string, providerStreamingBaseUrl?: string, proxyUrl?: string): Promise { - let validatedBaseUrl: URL; - try { - const candidate = new URL(providerStreamingBaseUrl?.trim() || DEFAULT_CLOUDMATCH_BASE_URL); - const hostname = candidate.hostname.toLowerCase(); - if ( - candidate.protocol !== "https:" || - ( - hostname !== "prod.cloudmatchbeta.nvidiagrid.net" && - hostname !== "img.nvidiagrid.net" && - !hostname.endsWith(".geforcenow.nvidiagrid.net") - ) - ) { - validatedBaseUrl = new URL(DEFAULT_CLOUDMATCH_BASE_URL); - } else { - validatedBaseUrl = candidate; - } - } catch { - validatedBaseUrl = new URL(DEFAULT_CLOUDMATCH_BASE_URL); - } - - const serverInfoUrl = new URL("v2/serverInfo", validatedBaseUrl); - - const response = await fetchWithOptionalProxy(serverInfoUrl.toString(), { - headers: buildGfnLcarsHeaders({ - token, - clientType: "NATIVE", - clientStreamer: "NVIDIA-CLASSIC", - includeUserAgent: true, - includeEmptyTokenAuthorization: true, - }), - }, proxyUrl); - - if (!response.ok) { - return "GFN-PC"; - } - - const payload = (await response.json()) as ServerInfoResponse; - return payload.requestStatus?.serverId ?? "GFN-PC"; -} - -function parseFeatureLabel(value: unknown): string | null { - if (typeof value === "string") { - const trimmed = value.trim(); - return trimmed.length > 0 ? trimmed : null; - } - if (value && typeof value === "object") { - const candidate = value as Record; - const keys = ["name", "label", "title", "displayName"]; - for (const key of keys) { - const raw = candidate[key]; - if (typeof raw === "string") { - const trimmed = raw.trim(); - if (trimmed.length > 0) { - return trimmed; - } - } - } - } - return null; -} - -function extractFeatureLabels(app: AppData): string[] { - const buckets: unknown[] = [ - app.features, - app.gameFeatures, - app.appFeatures, - app.genres, - app.tags, - app.gfn?.catalogSkuStrings?.SKU_BASED_TAG, - ]; - - const labels: string[] = []; - for (const bucket of buckets) { - if (!Array.isArray(bucket)) { - continue; - } - for (const entry of bucket) { - const label = parseFeatureLabel(entry); - if (label) { - labels.push(label); - } - } - } - - return [...new Set(labels)]; -} - -function extractGenres(app: AppData): string[] { - if (!Array.isArray(app.genres)) { - return []; - } - - const genres: string[] = []; - for (const entry of app.genres) { - const genre = parseFeatureLabel(entry); - if (genre) { - genres.push(genre); - } - } - - return [...new Set(genres)]; -} - -function extractContentRatings(app: AppData): string[] { - if (!Array.isArray(app.contentRatings)) { - return []; - } - - const labels: string[] = []; - for (const entry of app.contentRatings) { - const label = parseFeatureLabel(entry); - if (label) { - labels.push(label); - } - } - - return [...new Set(labels)]; -} - -function extractStringValues(value: unknown): string[] { - if (!Array.isArray(value)) return []; - return [...new Set(value - .map((entry) => typeof entry === "string" ? entry.trim() : parseFeatureLabel(entry)) - .filter((entry): entry is string => typeof entry === "string" && entry.length > 0))]; -} - -function buildSearchText(title: string, variants: GameVariant[], genres: string[], featureLabels: string[], publisherName?: string, developerName?: string): string { - const stores = variants.map((variant) => variant.store); - return [title, publisherName, developerName, ...stores, ...genres, ...featureLabels] - .filter((value): value is string => typeof value === "string" && value.trim().length > 0) - .join(" ") - .toLowerCase(); -} - -function resolveAppData(app: AppData): AppResolution { - const variants = app.variants ?? []; - const selectedVariantIndex = variants.findIndex((variant) => variant.gfn?.library?.selected === true); - const preferredVariant = selectedVariantIndex >= 0 ? variants[selectedVariantIndex] : undefined; - const numericVariants = variants.filter((variant) => isNumericId(variant.id)); - const preferredNumericVariant = preferredVariant && isNumericId(preferredVariant.id) ? preferredVariant.id : undefined; - const fallbackNumericVariant = numericVariants[0]?.id; - const numericAppId = preferredNumericVariant ?? fallbackNumericVariant ?? (isNumericId(app.id) ? app.id : undefined); - const preferredVariantId = preferredVariant?.id ?? numericAppId ?? variants[0]?.id ?? app.id; - const lastPlayed = variants - .map((variant) => variant.gfn?.library?.lastPlayedDate) - .find((value): value is string => typeof value === "string" && value.length > 0); - const isInLibrary = variants.some((variant) => isOwnedLibraryStatus(variant.gfn?.library?.status)); - - return { - numericAppId, - preferredVariantId, - selectedVariantIndex: selectedVariantIndex >= 0 ? selectedVariantIndex : Math.max(0, variants.findIndex((variant) => variant.id === preferredVariantId)), - lastPlayed, - isInLibrary, - }; -} - -function appToVariants(app: AppData): GameVariant[] { - return app.variants?.map((variant) => { - const supportsPersistence = supportsInGameSettingsPersistence(variant); - return { - id: variant.id, - store: variant.appStore, - storeUrl: variant.storeUrl, - supportedControls: variant.supportedControls ?? [], - ...(supportsPersistence ? { supportsInGameSettingsPersistence: true } : {}), - librarySelected: variant.gfn?.library?.selected, - inLibrary: variant.gfn?.library?.selected === true, - libraryStatus: variant.gfn?.library?.status, - lastPlayedDate: variant.gfn?.library?.lastPlayedDate, - gfnStatus: variant.gfn?.status, - }; - }) ?? []; -} - -function appToGame(app: AppData): GameInfo { - const variants = appToVariants(app); - const resolution = resolveAppData(app); - const heroImageUrl = getFirstImage(app.images, LANDSCAPE_IMAGE_KEYS, 1200); - const posterImageUrl = getFirstImage(app.images, POSTER_IMAGE_KEYS, 900); - const imageUrl = heroImageUrl ?? posterImageUrl; - const screenshotUrls = normalizeImageValues(app.images?.SCREENSHOTS, 720); - const genres = extractGenres(app); - const featureLabels = extractFeatureLabels(app); - const supportedControls = extractStringValues(app.supportedControls); - const nvidiaTech = extractStringValues(app.nvidiaTech); - - return { - id: app.id, - uuid: app.id, - launchAppId: resolution.numericAppId, - title: app.title, - shortName: app.shortName, - description: app.description, - longDescription: app.longDescription, - developerName: app.developerName, - maxLocalPlayers: app.maxLocalPlayers, - maxOnlinePlayers: app.maxOnlinePlayers, - featureLabels, - genres, - supportedControls: supportedControls.length > 0 ? supportedControls : undefined, - nvidiaTech: nvidiaTech.length > 0 ? nvidiaTech : undefined, - imageUrl, - heroImageUrl, - screenshotUrl: screenshotUrls[0], - screenshotUrls: screenshotUrls.length > 0 ? screenshotUrls : undefined, - imageUrlsByType: getImageUrlsByType(app.images), - playType: app.gfn?.playType, - membershipTierLabel: app.gfn?.minimumMembershipTierLabel, - catalogSkuStrings: app.gfn?.catalogSkuStrings, - publisherName: app.publisherName, - contentRatings: extractContentRatings(app), - playabilityState: app.gfn?.playabilityState, - availableStores: [...new Set(variants.map((variant) => variant.store).filter(Boolean))], - searchText: buildSearchText(app.title, variants, genres, featureLabels, app.publisherName, app.developerName), - lastPlayed: resolution.lastPlayed, - isInLibrary: resolution.isInLibrary, - selectedVariantIndex: Math.max(0, Math.min(resolution.selectedVariantIndex, Math.max(variants.length - 1, 0))), - variants, - }; -} - -function mergeAppMetaIntoGame(game: GameInfo, app: AppData): GameInfo { - const merged = appToGame(app); - const selectedVariantId = game.variants[game.selectedVariantIndex]?.id; - const variants = merged.variants.map((variant) => { - const existing = game.variants.find((candidate) => candidate.id === variant.id); - return { - ...variant, - librarySelected: variant.librarySelected ?? existing?.librarySelected, - inLibrary: variant.inLibrary ?? existing?.inLibrary, - libraryStatus: variant.libraryStatus ?? existing?.libraryStatus, - lastPlayedDate: variant.lastPlayedDate ?? existing?.lastPlayedDate, - }; - }); - const selectedVariantIndex = selectedVariantId - ? variants.findIndex((variant) => variant.id === selectedVariantId) - : -1; - - return { - ...game, - ...merged, - id: game.id, - isInLibrary: merged.isInLibrary || game.isInLibrary, - lastPlayed: merged.lastPlayed ?? game.lastPlayed, - variants, - selectedVariantIndex: selectedVariantIndex >= 0 ? selectedVariantIndex : merged.selectedVariantIndex, - }; -} - -function dedupeGames(games: GameInfo[]): GameInfo[] { - const byId = new Map(); - - for (const game of games) { - const existing = byId.get(game.id); - if (!existing) { - byId.set(game.id, game); - continue; - } - - const mergedVariants = new Map(); - for (const variant of [...existing.variants, ...game.variants]) { - mergedVariants.set(variant.id, variant); - } - - const merged: GameInfo = { - ...existing, - ...game, - id: existing.id, - uuid: existing.uuid ?? game.uuid, - launchAppId: existing.launchAppId ?? game.launchAppId, - title: existing.title || game.title, - shortName: existing.shortName ?? game.shortName, - description: existing.description ?? game.description, - longDescription: existing.longDescription ?? game.longDescription, - developerName: existing.developerName ?? game.developerName, - maxLocalPlayers: existing.maxLocalPlayers ?? game.maxLocalPlayers, - maxOnlinePlayers: existing.maxOnlinePlayers ?? game.maxOnlinePlayers, - imageUrl: existing.imageUrl ?? game.imageUrl, - heroImageUrl: existing.heroImageUrl ?? game.heroImageUrl, - screenshotUrl: existing.screenshotUrl ?? game.screenshotUrl, - screenshotUrls: [...new Set([...(existing.screenshotUrls ?? []), ...(game.screenshotUrls ?? [])])], - imageUrlsByType: { - ...(game.imageUrlsByType ?? {}), - ...(existing.imageUrlsByType ?? {}), - }, - playType: existing.playType ?? game.playType, - membershipTierLabel: existing.membershipTierLabel ?? game.membershipTierLabel, - catalogSkuStrings: existing.catalogSkuStrings ?? game.catalogSkuStrings, - publisherName: existing.publisherName ?? game.publisherName, - playabilityState: existing.playabilityState ?? game.playabilityState, - lastPlayed: existing.lastPlayed ?? game.lastPlayed, - isInLibrary: existing.isInLibrary || game.isInLibrary, - variants: [...mergedVariants.values()], - genres: [...new Set([...(existing.genres ?? []), ...(game.genres ?? [])])], - featureLabels: [...new Set([...(existing.featureLabels ?? []), ...(game.featureLabels ?? [])])], - supportedControls: [...new Set([...(existing.supportedControls ?? []), ...(game.supportedControls ?? [])])], - nvidiaTech: [...new Set([...(existing.nvidiaTech ?? []), ...(game.nvidiaTech ?? [])])], - contentRatings: [...new Set([...(existing.contentRatings ?? []), ...(game.contentRatings ?? [])])], - availableStores: [...new Set([...(existing.availableStores ?? []), ...(game.availableStores ?? [])])], - searchText: [existing.searchText, game.searchText].filter(Boolean).join(" ").trim() || undefined, - selectedVariantIndex: Math.max(0, existing.variants[existing.selectedVariantIndex] - ? [...mergedVariants.values()].findIndex((variant) => variant.id === existing.variants[existing.selectedVariantIndex]?.id) - : game.selectedVariantIndex), - }; - - byId.set(game.id, merged); - } - - return [...byId.values()]; -} - -async function fetchAppMetaData( - token: string, - appIds: string[], - vpcId: string, - proxyUrl?: string, -): Promise { - const normalizedIds = [...new Set(appIds.map((id) => id.trim()).filter((id) => id.length > 0))]; - if (normalizedIds.length === 0) { - return { data: { apps: { items: [] } } }; - } - - return await fetchLcarsGraphQl( - "AppDataForAppId", - { - vpcId, - locale: DEFAULT_LOCALE, - appIds: normalizedIds, - }, - token, - proxyUrl, - { context: "App metadata failed" }, - ); -} - -async function enrichGamesWithMetadata(token: string, vpcId: string, games: GameInfo[], proxyUrl?: string): Promise { - const uuids = [...new Set(games.map((game) => game.uuid).filter((uuid): uuid is string => !!uuid))]; - - if (uuids.length === 0) { - return games; - } - - const chunkSize = 40; - const appById = new Map(); - - for (let index = 0; index < uuids.length; index += chunkSize) { - const chunk = uuids.slice(index, index + chunkSize); - const payload = await fetchAppMetaData(token, chunk, vpcId, proxyUrl); - if (payload.errors?.length) { - throw new Error(payload.errors.map((error) => error.message).join(", ")); - } - - for (const app of payload.data?.apps.items ?? []) { - appById.set(app.id, app); - } - } - - return dedupeGames( - games.map((game) => { - const metadata = game.uuid ? appById.get(game.uuid) : undefined; - return metadata ? mergeAppMetaIntoGame(game, metadata) : game; - }), - ); -} - -async function fetchPanels( - token: string, - panelNames: string[], - vpcId: string, - options?: { withLibraryTime?: boolean }, - proxyUrl?: string, -): Promise { - const queryName = panelNames.includes("MARQUEE") - ? "Marquee" - : panelNames.includes("LIBRARY") - ? options?.withLibraryTime === true ? "LibrarySectionWithTime" : "LibrarySection" - : "Main"; - - return await fetchLcarsGraphQl( - queryName, - { - vpcId, - locale: DEFAULT_LOCALE, - panelNames, - }, - token, - proxyUrl, - { context: "Games GraphQL failed" }, - ); -} - -function panelTextMatchesFeatured(value: string | undefined): boolean { - return value?.toLowerCase().includes("featured") ?? false; -} - -function getFeaturedGameIdentity(game: GameInfo): string { - return game.id || game.uuid || game.launchAppId || game.title; -} - -function featuredGamesFromPanels(payload: GraphQlResponse): GameInfo[] { - if (payload.errors?.length) { - throw new Error(payload.errors.map((error) => error.message).join(", ")); - } - - const explicitGames: GameInfo[] = []; - const explicitIds = new Set(); - const curatedGames: GameInfo[] = []; - const curatedIds = new Set(); - - const appendUnique = (target: GameInfo[], seen: Set, game: GameInfo): void => { - const identity = getFeaturedGameIdentity(game); - if (!identity || seen.has(identity)) return; - seen.add(identity); - target.push(game); - }; - - for (const panel of payload.data?.panels ?? []) { - const panelFeatured = panelTextMatchesFeatured(panel.name) || panelTextMatchesFeatured(panel.id); - for (const section of panel.sections ?? []) { - const sectionFeatured = panelFeatured || panelTextMatchesFeatured(section.title) || panelTextMatchesFeatured(section.id); - for (const item of section.items ?? []) { - if (item.__typename !== "GameItem" || !item.app) continue; - const game = appToGame(item.app); - if (!game.id || !game.title || game.variants.length === 0) continue; - appendUnique(curatedGames, curatedIds, game); - if (sectionFeatured) appendUnique(explicitGames, explicitIds, game); - } - } - } - - return explicitGames.length > 0 ? explicitGames : curatedGames; -} - -function flattenPanels(payload: GraphQlResponse): GameInfo[] { - if (payload.errors?.length) { - throw new Error(payload.errors.map((error) => error.message).join(", ")); - } - - const games: GameInfo[] = []; - - for (const panel of payload.data?.panels ?? []) { - for (const section of panel.sections ?? []) { - for (const item of section.items ?? []) { - if (item.__typename === "GameItem" && item.app) { - games.push(appToGame(item.app)); - } - } - } - } - - return dedupeGames(games); -} - -function parsePanelResults(payload: GraphQlResponse): GamePanelResult[] { - if (payload.errors?.length) { - throw new Error(payload.errors.map((error) => error.message).join(", ")); - } - - const panels: GamePanelResult[] = []; - for (const panel of payload.data?.panels ?? []) { - const sections = (panel.sections ?? []) - .map((section) => ({ - id: section.id ?? section.title ?? "", - title: section.title ?? "", - games: (section.items ?? []) - .filter((item) => item.__typename === "GameItem" && item.app) - .map((item) => appToGame(item.app as AppData)) - .filter((game) => game.id && game.title && game.variants.length > 0), - })) - .filter((section) => section.games.length > 0); - - if (sections.length > 0) { - panels.push({ - id: panel.id ?? panel.name, - title: panel.name, - sections, - }); - } - } - return panels; -} - -async function fetchFilterAndSortDefinitions(token?: string, proxyUrl?: string): Promise { - const payload = await fetchLcarsGraphQl( - "FilterGroupAndSortOrderDefinitions", - { locale: DEFAULT_LOCALE }, - token, - proxyUrl, - ); - if (payload.errors?.length) { - throw new Error(payload.errors.map((error) => error.message).join(", ")); - } - - const filterPayloadById: Record = {}; - const filterGroups: CatalogFilterGroup[] = []; - - for (const group of payload.data?.filterGroupDefinitions ?? []) { - const options = (group.filters ?? []).flatMap((entry) => { - const filterJson = entry.filters?.[0]; - if (!filterJson) { - return []; - } - try { - filterPayloadById[entry.id] = JSON.parse(filterJson); - return [{ - id: entry.id, - rawId: entry.id, - label: entry.label, - groupId: group.id, - groupLabel: group.label, - }]; - } catch { - return []; - } - }); - - if (options.length > 0) { - filterGroups.push({ id: group.id, label: group.label, options }); - } - } - - const sortOptions = (payload.data?.sortOrderDefinitions ?? []).map((sort) => ({ - id: sort.id, - label: sort.label, - orderBy: sort.orderBy, - })); - - return { - filterGroups, - sortOptions, - filterPayloadById, - }; -} - -function mergeFilterPayloads(filterIds: string[], filterPayloadById: Record): Record { - const merged: Record = {}; - - for (const filterId of filterIds) { - const payload = filterPayloadById[filterId]; - if (!payload || typeof payload !== "object") { - continue; - } - Object.assign(merged, payload as Record); - } - - return merged; -} - -async function browseCatalogUncached(input: CatalogBrowseRequest): Promise { - const token = input.token; - if (!token) { - throw new Error("Catalog browsing requires an authenticated token"); - } - - const vpcId = await getVpcId(token, input.providerStreamingBaseUrl, input.proxyUrl); - const definitions = await fetchFilterAndSortDefinitions(token, input.proxyUrl); - const normalizedFilterIds = (input.filterIds ?? []).filter((id) => id in definitions.filterPayloadById); - const selectedSort = definitions.sortOptions.find((option) => option.id === input.sortId) - ?? definitions.sortOptions.find((option) => option.id === DEFAULT_SORT_ID) - ?? definitions.sortOptions[0] - ?? { id: DEFAULT_SORT_ID, label: "Relevance", orderBy: "itemMetadata.relevance:DESC,sortName:ASC" }; - const searchQuery = input.searchQuery?.trim() ?? ""; - const fetchCount = Math.max(24, Math.min(input.fetchCount ?? DEFAULT_CATALOG_FETCH_COUNT, 200)); - const filters = mergeFilterPayloads(normalizedFilterIds, definitions.filterPayloadById); - - const appFields = ` - numberReturned - numberSupported - pageInfo { hasNextPage endCursor totalCount } - items { - id - title - images { KEY_ART KEY_IMAGE GAME_BOX_ART TV_BANNER HERO_IMAGE MARQUEE_HERO_IMAGE FEATURE_IMAGE GAME_LOGO SCREENSHOTS } - variants { - id - appStore - storeUrl - supportedControls - gfn { - status - features { -${GFN_FEATURE_FIELDS} - } - library { status selected } - } - } - gfn { - playabilityState - minimumMembershipTierLabel - catalogSkuStrings { - SKU_BASED_TAG - SKU_BASED_PLAYABILITY_TEXT - SKU_BASED_UNPLAYABLE_DIALOG_HEADER - SKU_BASED_UNPLAYABLE_DIALOG_BODY_UPGRADE - SKU_BASED_UNPLAYABLE_DIALOG_BODY_UPGRADE_ECOMM_RESTRICTED - } - } - itemMetadata { campaignIds } - } - `; - - const query = searchQuery.length > 0 - ? `query GetSearchFilterResults( - $vpcId: String!, - $locale: String!, - $sortString: String!, - $fetchCount: Int!, - $cursor: String!, - $searchString: String!, - $filters: AppFilterFields! - ) { - apps( - vpcId: $vpcId, - language: $locale, - orderBy: $sortString, - first: $fetchCount, - after: $cursor, - searchQuery: $searchString, - filters: $filters - ) { -${appFields} - } - }` - : `query GetFilterBrowseResults( - $vpcId: String!, - $locale: String!, - $sortString: String!, - $fetchCount: Int!, - $cursor: String!, - $filters: AppFilterFields! - ) { - apps( - vpcId: $vpcId, - language: $locale, - orderBy: $sortString, - first: $fetchCount, - after: $cursor, - filters: $filters - ) { -${appFields} - } - }`; - - const collectedApps: AppData[] = []; - let numberReturned = 0; - let numberSupported = 0; - let totalCount = 0; - let hasNextPage = false; - let endCursor = ""; - let cursor = ""; - - for (let page = 0; page < MAX_CATALOG_PAGES; page += 1) { - const variables = searchQuery.length > 0 - ? { - vpcId, - locale: DEFAULT_LOCALE, - sortString: selectedSort.orderBy, - fetchCount, - cursor, - searchString: searchQuery, - filters, - } - : { - vpcId, - locale: DEFAULT_LOCALE, - sortString: selectedSort.orderBy, - fetchCount, - cursor, - filters, - }; - const payload = await fetchLcarsGraphQl( - searchQuery.length > 0 ? "AppsWithSearch" : "AppsWithoutSearch", - variables, - token, - input.proxyUrl, - { - context: "GFN catalog query failed", - fallbackQuery: query, - }, - ); - - if (payload.errors?.length) { - throw new Error(payload.errors.map((error) => error.message).join(", ")); - } - - const apps = payload.data?.apps; - const items = apps?.items ?? []; - collectedApps.push(...items); - numberReturned += apps?.numberReturned ?? items.length; - numberSupported = apps?.numberSupported ?? numberSupported; - hasNextPage = apps?.pageInfo?.hasNextPage ?? false; - endCursor = apps?.pageInfo?.endCursor ?? ""; - totalCount = apps?.pageInfo?.totalCount ?? totalCount; - - if (!hasNextPage || !endCursor) { - break; - } - - cursor = endCursor; - } - - let games = dedupeGames(await enrichGamesWithMetadata(token, vpcId, collectedApps.map(appToGame), input.proxyUrl)); - const publicGames = await fetchPublicGames(input.proxyUrl); - const gamesWithPublicVariants = appendPublicGameSearchMatches( - mergePublicGameVariants(games, publicGames), - publicGames, - searchQuery, - ); - - return { - games: gamesWithPublicVariants, - numberReturned, - numberSupported: Math.max(numberSupported, gamesWithPublicVariants.length), - totalCount: Math.max(totalCount, gamesWithPublicVariants.length), - hasNextPage, - endCursor: endCursor || undefined, - searchQuery, - selectedSortId: selectedSort.id, - selectedFilterIds: normalizedFilterIds, - filterGroups: definitions.filterGroups, - sortOptions: definitions.sortOptions, - }; -} - -export async function browseCatalog(input: CatalogBrowseRequest): Promise { - const token = input.token; - if (!token) { - throw new Error("Catalog browsing requires an authenticated token"); - } - - const cached = await peekCachedBrowseCatalog(input); - if (cached) { - return cached; - } - - const result = await browseCatalogUncached(input); - const accountId = resolveAccountCacheId(input.userId, token); - if (!shouldBypassGamesCache(input.proxyUrl)) { - const cacheKey = catalogBrowseCacheKey(input, accountId); - await cacheManager.saveToCache(cacheKey, result); - } - return result; -} - -export async function peekCachedBrowseCatalog(input: CatalogBrowseRequest): Promise { - const token = input.token; - if (!token) { - return null; - } - if (shouldBypassGamesCache(input.proxyUrl)) { - return null; - } - - const accountId = resolveAccountCacheId(input.userId, token); - const cacheKey = catalogBrowseCacheKey(input, accountId); - const cached = await cacheManager.loadFromCache(cacheKey); - return cached?.data ?? null; -} - -export async function peekCachedLibraryGames( - token: string, - providerStreamingBaseUrl?: string, - accountId?: string, - proxyUrl?: string, -): Promise { - const cached = await loadAccountScopedFromCache(LIBRARY_GAMES_CACHE_SCOPE, accountId, token, providerStreamingBaseUrl, proxyUrl); - return cached?.data ?? null; -} - -export async function fetchLibraryGamesFromCache( - token: string, - providerStreamingBaseUrl?: string, - accountId?: string, - proxyUrl?: string, -): Promise { - const cached = await peekCachedLibraryGames(token, providerStreamingBaseUrl, accountId, proxyUrl); - if (!cached) { - return null; - } - return mergePublicGameVariants(cached, await fetchPublicGames(proxyUrl)); -} - -export async function fetchMainGames( - token: string, - providerStreamingBaseUrl?: string, - accountId?: string, - proxyUrl?: string, -): Promise { - const cached = await loadAccountScopedFromCache("main", accountId, token, providerStreamingBaseUrl, proxyUrl); - if (cached) { - return mergePublicGameVariants(cached.data, await fetchPublicGames(proxyUrl)); - } - - const games = await fetchMainGamesUncached(token, providerStreamingBaseUrl, proxyUrl); - if (!shouldBypassGamesCache(proxyUrl)) { - const cacheKey = accountScopedGamesCacheKey("main", resolveAccountCacheId(accountId, token), providerStreamingBaseUrl, proxyUrl); - await cacheManager.saveToCache(cacheKey, games); - } - return games; -} - -export async function fetchFeaturedGames( - token: string, - providerStreamingBaseUrl?: string, - accountId?: string, - proxyUrl?: string, -): Promise { - const cached = await loadAccountScopedFromCache("featured", accountId, token, providerStreamingBaseUrl, proxyUrl); - if (cached) return cached.data; - - const vpcId = await getVpcId(token, providerStreamingBaseUrl, proxyUrl); - const games = featuredGamesFromPanels(await fetchPanels(token, ["MARQUEE"], vpcId, undefined, proxyUrl)).slice(0, 6); - - if (!shouldBypassGamesCache(proxyUrl)) { - const cacheKey = accountScopedGamesCacheKey("featured", resolveAccountCacheId(accountId, token), providerStreamingBaseUrl, proxyUrl); - await cacheManager.saveToCache(cacheKey, games); - } - return games; -} - -export async function fetchStorePanels( - token: string, - providerStreamingBaseUrl?: string, - accountId?: string, - proxyUrl?: string, -): Promise { - const cached = await loadAccountScopedFromCache("store-panels", accountId, token, providerStreamingBaseUrl, proxyUrl); - if (cached) return cached.data; - - const vpcId = await getVpcId(token, providerStreamingBaseUrl, proxyUrl); - const panels = parsePanelResults(await fetchPanels(token, ["MAIN"], vpcId, undefined, proxyUrl)); - if (!shouldBypassGamesCache(proxyUrl)) { - const cacheKey = accountScopedGamesCacheKey("store-panels", resolveAccountCacheId(accountId, token), providerStreamingBaseUrl, proxyUrl); - await cacheManager.saveToCache(cacheKey, panels); - } - return panels; -} - -async function fetchMainGamesUncached(token: string, providerStreamingBaseUrl?: string, proxyUrl?: string): Promise { - const vpcId = await getVpcId(token, providerStreamingBaseUrl, proxyUrl); - const payload = await fetchPanels(token, ["MAIN"], vpcId, undefined, proxyUrl); - const games = flattenPanels(payload); - return mergePublicGameVariants(await enrichGamesWithMetadata(token, vpcId, games, proxyUrl), await fetchPublicGames(proxyUrl)); -} - -export async function fetchLibraryGames( - token: string, - providerStreamingBaseUrl?: string, - accountId?: string, - proxyUrl?: string, -): Promise { - const cached = await loadAccountScopedFromCache(LIBRARY_GAMES_CACHE_SCOPE, accountId, token, providerStreamingBaseUrl, proxyUrl); - if (cached) { - return mergePublicGameVariants(cached.data, await fetchPublicGames(proxyUrl)); - } - - const games = await fetchLibraryGamesUncached(token, providerStreamingBaseUrl, proxyUrl); - if (!shouldBypassGamesCache(proxyUrl)) { - const cacheKey = accountScopedGamesCacheKey(LIBRARY_GAMES_CACHE_SCOPE, resolveAccountCacheId(accountId, token), providerStreamingBaseUrl, proxyUrl); - await cacheManager.saveToCache(cacheKey, games); - } - return games; -} - -async function fetchLibraryGamesUncached( - token: string, - providerStreamingBaseUrl?: string, - proxyUrl?: string, -): Promise { - const vpcId = await getVpcId(token, providerStreamingBaseUrl, proxyUrl); - try { - const apps = await fetchPaginatedLibraryApps(token, vpcId, proxyUrl); - const games = dedupeGames(apps.map(appToGame)); - return mergePublicGameVariants(await enrichGamesWithMetadata(token, vpcId, games, proxyUrl), await fetchPublicGames(proxyUrl)); - } catch (error) { - console.warn("Paginated library query failed, falling back to library panel:", error); - } - - let payload: GraphQlResponse; - - try { - payload = await fetchPanels(token, ["LIBRARY"], vpcId, { withLibraryTime: true }, proxyUrl); - } catch { - payload = await fetchPanels(token, ["LIBRARY"], vpcId, undefined, proxyUrl); - } - - const games = flattenPanels(payload); - return mergePublicGameVariants(await enrichGamesWithMetadata(token, vpcId, games, proxyUrl), await fetchPublicGames(proxyUrl)); -} - -async function fetchPaginatedLibraryApps(token: string, vpcId: string, proxyUrl?: string): Promise { - const query = `query GetLibraryApps( - $vpcId: String!, - $locale: String!, - $sortString: String!, - $fetchCount: Int!, - $cursor: String!, - $filters: AppFilterFields! - ) { - apps( - vpcId: $vpcId, - language: $locale, - orderBy: $sortString, - first: $fetchCount, - after: $cursor, - filters: $filters - ) { - numberReturned - numberSupported - pageInfo { hasNextPage endCursor totalCount } - items { - id - title - images { KEY_ART KEY_IMAGE GAME_BOX_ART TV_BANNER HERO_IMAGE MARQUEE_HERO_IMAGE FEATURE_IMAGE GAME_LOGO SCREENSHOTS } - variants { - id - appStore - storeUrl - supportedControls - gfn { - status - features { -${GFN_FEATURE_FIELDS} - } - library { status selected lastPlayedDate } - } - } - gfn { - playabilityState - minimumMembershipTierLabel - catalogSkuStrings { - SKU_BASED_TAG - SKU_BASED_PLAYABILITY_TEXT - SKU_BASED_UNPLAYABLE_DIALOG_HEADER - SKU_BASED_UNPLAYABLE_DIALOG_BODY_UPGRADE - SKU_BASED_UNPLAYABLE_DIALOG_BODY_UPGRADE_ECOMM_RESTRICTED - } - } - itemMetadata { campaignIds } - } - } - }`; - - const result = await fetchAllAppsPages( - (cursor) => postGraphQl( - query, - { - vpcId, - locale: DEFAULT_LOCALE, - sortString: DEFAULT_LIBRARY_SORT, - fetchCount: LIBRARY_FETCH_COUNT, - cursor, - filters: LIBRARY_APPS_FILTER, - }, - token, - proxyUrl, - ), - { maxPages: MAX_LIBRARY_PAGES }, - ); - return result.items; -} - -export async function fetchPublicGames(proxyUrl?: string): Promise { - if (shouldBypassGamesCache(proxyUrl)) { - return fetchPublicGamesUncached(proxyUrl); - } - - const cacheKey = publicGamesCacheKey(proxyUrl); - const cached = await cacheManager.loadFromCache(cacheKey); - if (cached) { - return cached.data; - } - - const games = await fetchPublicGamesUncached(proxyUrl); - await cacheManager.saveToCache(cacheKey, games); - return games; -} - -export async function resolveLaunchAppId( - token: string, - appIdOrUuid: string, - providerStreamingBaseUrl?: string, - proxyUrl?: string, -): Promise { - if (isNumericId(appIdOrUuid)) { - return appIdOrUuid; - } - - const vpcId = await getVpcId(token, providerStreamingBaseUrl, proxyUrl); - const payload = await fetchAppMetaData(token, [appIdOrUuid], vpcId, proxyUrl); - - if (payload.errors?.length) { - throw new Error(payload.errors.map((error) => error.message).join(", ")); - } - - const app = payload.data?.apps.items?.[0]; - if (!app) { - return null; - } - - return resolveAppData(app).numericAppId ?? null; -} - -export async function resolveStoreUrl( - token: string, - appIdOrUuid: string, - providerStreamingBaseUrl?: string, - options: { variantId?: string; store?: string; proxyUrl?: string } = {}, -): Promise { - const vpcId = await getVpcId(token, providerStreamingBaseUrl, options.proxyUrl); - const payload = await fetchAppMetaData(token, [appIdOrUuid], vpcId, options.proxyUrl); - - if (payload.errors?.length) { - throw new Error(payload.errors.map((error) => error.message).join(", ")); - } - - const app = payload.data?.apps.items?.[0]; - const variants = app?.variants ?? []; - const selectedVariant = options.variantId - ? variants.find((variant) => variant.id === options.variantId) - : undefined; - if (selectedVariant?.storeUrl) return selectedVariant.storeUrl; - - const storeKey = options.store ? normalizeGameStore(options.store) : undefined; - const matchingStoreVariant = storeKey - ? variants.find((variant) => normalizeGameStore(variant.appStore) === storeKey && variant.storeUrl) - : undefined; - if (matchingStoreVariant?.storeUrl) return matchingStoreVariant.storeUrl; - - return variants.find((variant) => variant.storeUrl)?.storeUrl ?? null; -} - -export async function markGameOwned(input: MarkGameOwnedInput): Promise { - const variantId = input.variantId.trim(); - if (!variantId) { - throw new Error("Cannot mark game as owned without a variant ID"); - } - - const payload = await postLcarsMutation( - "AddOwnedVariant", - { - cmsId: variantId, - locale: DEFAULT_LOCALE, - }, - input.token, - input.proxyUrl, - ); - - if (!payload.data?.addOwnedVariant?.app?.id) { - throw new Error("GFN library mutation failed: missing AddOwnedVariant response"); - } - - await invalidateAccountGameCaches({ - userId: input.userId, - providerStreamingBaseUrl: input.providerStreamingBaseUrl, - tokens: [input.token, ...(input.tokens ?? [])], - proxyUrl: input.proxyUrl, - }); - - return { - ok: true, - variantId, - libraryStatus: "MANUAL", - }; -} - -export { - browseCatalogUncached, - fetchMainGamesUncached, - fetchLibraryGamesUncached, - fetchPublicGamesUncached, -}; diff --git a/opennow-stable/src/main/index.ts b/opennow-stable/src/main/index.ts index cf762111c..373a3698a 100644 --- a/opennow-stable/src/main/index.ts +++ b/opennow-stable/src/main/index.ts @@ -9,7 +9,7 @@ import { session, protocol, } from "electron"; -import { fileURLToPath, pathToFileURL } from "node:url"; +import { fileURLToPath } from "node:url"; import { dirname, join } from "node:path"; import { existsSync, readFileSync } from "node:fs"; @@ -20,10 +20,8 @@ import { existsSync, readFileSync } from "node:fs"; // F8 - Toggle mouse/pointer lock (handled in main process via IPC) import { IPC_CHANNELS } from "@shared/ipc"; -import type { CommunityProxyProvisionResult } from "@shared/communityProxy"; -import { provisionZortosCommunityProxy } from "./community/provisionSessionProxy"; import { registerOpenNowMediaProtocol } from "./mediaPaths"; -import { initLogCapture, exportLogs } from "@shared/logger"; +import { initLogCapture } from "@shared/logger"; import { cacheManager } from "./services/cacheManager"; import { refreshScheduler } from "./services/refreshScheduler"; import { cacheEventBus } from "./services/cacheEventBus"; @@ -31,25 +29,19 @@ import { fetchMainGamesUncached, fetchLibraryGamesUncached, fetchPublicGamesUncached, -} from "./gfn/games"; +} from "./platforms/gfn/games"; import type { AppUpdaterState, SessionConflictChoice, - Settings, DirectLaunchRequest, - PingResult, - StreamRegion, - MicrophonePermissionResult, - ThankYouContributor, - ThankYouDataResult, - ThankYouSupporter, } from "@shared/gfn"; import { getSettingsManager, type SettingsManager } from "./settings"; -import { getActiveSessions } from "./gfn/cloudmatch"; -import { AuthService } from "./gfn/auth"; -import { initSessionProxyAuth } from "./gfn/proxyFetch"; +import { getActiveSessions } from "./platforms/gfn/cloudmatch"; +import { AuthService } from "./platforms/gfn/auth"; +import { configureIdentifyAsSteamDeck } from "./platforms/gfn/deviceIdentity"; +import { initSessionProxyAuth } from "./platforms/gfn/proxyFetch"; import { connectDiscordRpc, setActivity, @@ -58,7 +50,6 @@ import { getCurrentActivity, isDiscordRpcConnected, } from "./discordRpc"; -import type { DiscordActivityUpdate } from "@shared/discord"; import { discordMonitorActivityDecision, } from "./discordPresence"; @@ -66,9 +57,8 @@ import { createAppUpdaterController, type AppUpdaterController, } from "./updater"; -import { getAppBuildInfo } from "./appBuildInfo"; import { registerAccountCatalogIpcHandlers } from "./ipc/accountCatalogHandlers"; -import { registerMediaIpcHandlers } from "./ipc/mediaHandlers"; +import { registerCoreIpcHandlers } from "./ipc/coreHandlers"; import { registerSessionIpcHandlers } from "./ipc/sessionHandlers"; import { registerSignalingIpcHandlers, @@ -78,23 +68,15 @@ import { isSessionConflictError, showSessionConflictDialog as showSessionConflictDialogWithDeps, } from "./session/sessionConflict"; -import { fetchWithTimeout, withTimeout } from "./services/requestTimeout"; -import { - fetchPrintedWasteQueue, - fetchPrintedWasteServerMapping, -} from "./services/printedWaste"; -import { pingRegions } from "./services/regionPing"; import { buildChromiumCommandLine, normalizeBootstrapChromiumPreferences, type BootstrapChromiumPreferences, } from "./chromiumCommandLine"; -import { - nextPointerLockEscapeCaptureUntilMs, - shouldCaptureEscapeFullscreenInput, -} from "./escapeFullscreenGuard"; import { parseDirectLaunchArgs, type DirectLaunchArgs } from "@shared/directLaunch"; -import { getReleaseHighlightsPayload, normalizeReleaseVersion, shouldShowReleaseHighlights } from "./releaseHighlights"; +import { getReleaseHighlightsPayload, shouldShowReleaseHighlights } from "./releaseHighlights"; +import { shutdownMainTelemetry, syncMainTelemetry } from "./telemetry/posthog"; +import { createMainWindow } from "./window/mainWindow"; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); @@ -136,14 +118,6 @@ app.commandLine.appendSwitch( chromiumCommandLine.disableFeatures.join(","), ); -app.commandLine.appendSwitch( - "force-fieldtrials", - [ - // Disable send-side pacing — we are receive-only, pacing adds latency to RTCP feedback - "WebRTC-Video-Pacing/Disabled/", - ].join("/"), -); - for (const [name, value] of Object.entries(chromiumCommandLine.switches)) { if (value === true) { app.commandLine.appendSwitch(name); @@ -160,6 +134,9 @@ app.commandLine.appendSwitch("disable-renderer-backgrounding"); app.commandLine.appendSwitch("disable-backgrounding-occluded-windows"); // Remove getUserMedia FPS cap (not strictly needed for receive-only but avoids potential limits) app.commandLine.appendSwitch("max-gum-fps", "999"); +if (!app.isPackaged && process.env.OPENNOW_REMOTE_DEBUG === "1") { + app.commandLine.appendSwitch("remote-debugging-port", "9222"); +} // file:// in <video> is blocked by Chromium for renderer pages; use a privileged custom scheme. protocol.registerSchemesAsPrivileged([ @@ -192,6 +169,8 @@ let pendingDirectLaunchRequest: DirectLaunchRequest | null = createDirectLaunchR // Runtime pointer-lock state (updated by renderer) let isPointerLockActiveRuntime = false; let pointerLockEscapeCaptureUntilMs = 0; +let isStreamInputActiveRuntime = false; +let nativeRawInputOwnsEscapeRuntime = false; function createDirectLaunchRequest(args: DirectLaunchArgs): DirectLaunchRequest { return { @@ -261,6 +240,7 @@ function runShutdownCleanup(reason = "app-quit"): void { }); signalingCoordinator = null; void destroyDiscordRpc(); + void shutdownMainTelemetry(); appUpdater?.dispose(); appUpdater = null; @@ -417,163 +397,37 @@ function emitUpdaterStateToRenderer(state: AppUpdaterState): void { } } -function parseExternalHttpUrl(url: string): URL { - const parsed = new URL(url); - if (parsed.protocol !== "https:" && parsed.protocol !== "http:") { - throw new Error("Only HTTP(S) external URLs can be opened."); - } - return parsed; -} - -function isAppNavigationUrl(url: string): boolean { - try { - const parsed = new URL(url); - if (process.env.ELECTRON_RENDERER_URL) { - return parsed.origin === new URL(process.env.ELECTRON_RENDERER_URL).origin; - } - return parsed.toString() === pathToFileURL(join(__dirname, "../../dist/index.html")).toString(); - } catch { - return false; - } -} - -async function openExternalHttpUrl(url: string): Promise { - await shell.openExternal(parseExternalHttpUrl(url).toString()); -} - -async function createMainWindow(): Promise { - const preloadMjsPath = join(__dirname, "../preload/index.mjs"); - const preloadJsPath = join(__dirname, "../preload/index.js"); - const preloadPath = existsSync(preloadMjsPath) - ? preloadMjsPath - : preloadJsPath; - - const settings = settingsManager.getAll(); - - // Console mode (big picture): mirror GeForce NOW's TV mode by launching - // fullscreen with the controller-oriented shell enabled. - if (settings.launchInConsoleMode && !settings.controllerMode) { - settingsManager.set("controllerMode", true); - } - - // Direct-launch arguments always start fullscreen; the renderer applies the - // console shell for the run without persisting the Controller Mode setting. - const startFullscreen = - settings.launchInConsoleMode || pendingDirectLaunchRequest !== null; - - mainWindow = new BrowserWindow({ - width: settings.windowWidth || 1400, - height: settings.windowHeight || 900, - minWidth: 1024, - minHeight: 680, - fullscreen: startFullscreen, - autoHideMenuBar: true, - backgroundColor: "#0f172a", - webPreferences: { - preload: preloadPath, - contextIsolation: true, - nodeIntegration: false, - sandbox: false, +function createMainWindowDeps() { + return { + mainDir: __dirname, + settingsManager, + getMainWindow: () => mainWindow, + setMainWindow: (window: BrowserWindow | null) => { + mainWindow = window; }, - }); - - mainWindow.webContents.setWindowOpenHandler(({ url }) => { - void openExternalHttpUrl(url).catch((error) => { - console.warn("Blocked non-external window open:", error instanceof Error ? error.message : error); - }); - return { action: "deny" }; - }); - - mainWindow.webContents.on("will-navigate", (event, url) => { - if (isAppNavigationUrl(url)) { - return; - } - - event.preventDefault(); - void openExternalHttpUrl(url).catch((error) => { - console.warn("Blocked app window navigation:", error instanceof Error ? error.message : error); - }); - }); - - if (process.platform === "win32") { - // Keep native window fullscreen in sync with HTML fullscreen so Windows treats - // stream playback like a real fullscreen window instead of only DOM fullscreen. - mainWindow.webContents.on("enter-html-full-screen", () => { - if ( - mainWindow && - !mainWindow.isDestroyed() && - !mainWindow.isFullScreen() - ) { - mainWindow.setFullScreen(true); - } - }); - - mainWindow.webContents.on("leave-html-full-screen", () => { - if (rendererControlledFullscreen) { - return; - } - if ( - mainWindow && - !mainWindow.isDestroyed() && - mainWindow.isFullScreen() - ) { - mainWindow.setFullScreen(false); - } - }); - } - - // Track pointer-lock state from renderer; used to decide whether to swallow - // Escape at the native level (before Chromium handles it). - ipcMain.on( - IPC_CHANNELS.POINTER_LOCK_CHANGE, - (_ev, active: boolean, suppressEscapeFullscreenGrace?: boolean) => { - isPointerLockActiveRuntime = Boolean(active); - pointerLockEscapeCaptureUntilMs = nextPointerLockEscapeCaptureUntilMs( - isPointerLockActiveRuntime, - Boolean(suppressEscapeFullscreenGrace), - Date.now(), - ); + getRendererControlledFullscreen: () => rendererControlledFullscreen, + setRendererControlledFullscreen: (value: boolean) => { + rendererControlledFullscreen = value; }, - ); - - // Intercept Escape early to avoid Chromium exiting fullscreen before the - // renderer can forward the key to the remote session. Keep a short fullscreen - // grace window after pointer lock drops so rapid repeated Escape presses cannot - // win the race before the renderer re-locks the pointer. - mainWindow.webContents.on("before-input-event", (event, input) => { - try { - if (shouldCaptureEscapeFullscreenInput(input, { - allowEscapeToExitFullscreen: Boolean(settingsManager?.get("allowEscapeToExitFullscreen")), - pointerLockActive: isPointerLockActiveRuntime, - windowFullscreen: Boolean(mainWindow && !mainWindow.isDestroyed() && mainWindow.isFullScreen()), - pointerLockEscapeCaptureUntilMs, - nowMs: Date.now(), - })) { - event.preventDefault(); - if (mainWindow && mainWindow.webContents) { - mainWindow.webContents.send(IPC_CHANNELS.EXTERNAL_ESCAPE); - } - } - } catch { - // ignore errors - interception is best-effort - } - }); - - if (process.env.ELECTRON_RENDERER_URL) { - await mainWindow.loadURL(process.env.ELECTRON_RENDERER_URL); - } else { - await mainWindow.loadFile(join(__dirname, "../../dist/index.html")); - } - if (pendingDirectLaunchRequest) { - emitDirectLaunchRequest(pendingDirectLaunchRequest); - } - - mainWindow.on("closed", () => { - mainWindow = null; - rendererControlledFullscreen = false; - isPointerLockActiveRuntime = false; - pointerLockEscapeCaptureUntilMs = 0; - }); + getPendingDirectLaunchRequest: () => pendingDirectLaunchRequest, + emitDirectLaunchRequest, + getPointerLockActive: () => isPointerLockActiveRuntime, + setPointerLockActive: (active: boolean) => { + isPointerLockActiveRuntime = active; + }, + getPointerLockEscapeCaptureUntilMs: () => pointerLockEscapeCaptureUntilMs, + setPointerLockEscapeCaptureUntilMs: (value: number) => { + pointerLockEscapeCaptureUntilMs = value; + }, + getStreamInputActive: () => isStreamInputActiveRuntime, + setStreamInputActive: (active: boolean) => { + isStreamInputActiveRuntime = active; + }, + getNativeRawInputOwnsEscape: () => nativeRawInputOwnsEscapeRuntime, + setNativeRawInputOwnsEscape: (ownsEscape: boolean) => { + nativeRawInputOwnsEscapeRuntime = ownsEscape; + }, + }; } async function resolveJwt(token?: string): Promise { @@ -587,274 +441,6 @@ async function showSessionConflictDialog(): Promise { }); } -const THANKS_CONTRIBUTORS_URL = - "https://api.github.com/repos/OpenCloudGaming/OpenNOW/contributors?per_page=100"; -const THANKS_SUPPORTERS_URL = "https://github.com/sponsors/zortos293"; -const THANKS_REQUEST_HEADERS = { - Accept: "application/vnd.github+json", - "User-Agent": "OpenNOW-DesktopClient", -} as const; -const THANKS_EXCLUDED_PATTERN = /(copilot|claude|cappy)/i; -const THANKS_FETCH_TIMEOUT_MS = 8000; -const THANKS_CUSTOM_SUPPORTERS: readonly ThankYouSupporter[] = [ - { - name: "DarkevilPT", - avatarUrl: "https://github.com/DarkevilPT.png?size=96", - profileUrl: "https://github.com/DarkevilPT", - isPrivate: false, - source: "custom", - }, -] as const; - -interface GitHubContributorResponse { - login?: string; - avatar_url?: string; - html_url?: string; - contributions?: number; - type?: string; - name?: string | null; -} - -function decodeHtmlEntities(value: string): string { - return value - .replace(/&/g, "&") - .replace(/"/g, '"') - .replace(/'/g, "'") - .replace(/</g, "<") - .replace(/>/g, ">"); -} - -function stripHtml(value: string): string { - return decodeHtmlEntities(value.replace(/<[^>]+>/g, " ")) - .replace(/\s+/g, " ") - .trim(); -} - -function normalizeUrl(value: string | undefined): string | undefined { - if (!value) return undefined; - const decoded = decodeHtmlEntities(value.trim()); - if (!decoded) return undefined; - if (decoded.startsWith("//")) return `https:${decoded}`; - if (decoded.startsWith("/")) return `https://github.com${decoded}`; - return decoded; -} - -function shouldExcludeContributor( - contributor: GitHubContributorResponse, -): boolean { - const login = contributor.login?.trim() ?? ""; - const name = contributor.name?.trim() ?? ""; - if (!login || !contributor.avatar_url || !contributor.html_url) return true; - if (contributor.type === "Bot") return true; - if (/\[bot\]$/i.test(login)) return true; - if (THANKS_EXCLUDED_PATTERN.test(login) || THANKS_EXCLUDED_PATTERN.test(name)) - return true; - return false; -} - -async function fetchThanksContributors(): Promise { - const response = await fetchWithTimeout( - THANKS_CONTRIBUTORS_URL, - { headers: THANKS_REQUEST_HEADERS }, - THANKS_FETCH_TIMEOUT_MS, - "GitHub contributors request", - ); - if (!response.ok) { - throw new Error(`GitHub contributors request failed (${response.status})`); - } - - const payload = (await withTimeout( - response.json() as Promise, - THANKS_FETCH_TIMEOUT_MS, - "GitHub contributors response", - )) as GitHubContributorResponse[]; - if (!Array.isArray(payload)) { - throw new Error("GitHub contributors response was not an array"); - } - - const contributors = payload - .filter((contributor) => !shouldExcludeContributor(contributor)) - .map((contributor) => ({ - login: contributor.login!.trim(), - avatarUrl: contributor.avatar_url!, - profileUrl: contributor.html_url!, - contributions: - typeof contributor.contributions === "number" - ? contributor.contributions - : 0, - })) - .sort( - (a, b) => - b.contributions - a.contributions || a.login.localeCompare(b.login), - ); - return contributors; -} - -function parseSupporterName(entryHtml: string): { - name: string; - isPrivate: boolean; -} { - const privateHrefMatch = entryHtml.match( - /href="https:\/\/docs\.github\.com\/sponsors\/sponsoring-open-source-contributors\/managing-your-sponsorship#managing-the-privacy-setting-for-your-sponsorship"/i, - ); - const privateTooltipMatch = entryHtml.match( - /]*>\s*Private Sponsor\s*<\/tool-tip>/i, - ); - const privateAriaMatch = entryHtml.match(/aria-label="Private Sponsor"/i); - if (privateHrefMatch || privateTooltipMatch || privateAriaMatch) { - return { name: "Private", isPrivate: true }; - } - - const altMatch = entryHtml.match(/]+alt="([^"]+)"/i); - const altText = altMatch ? stripHtml(altMatch[1]) : ""; - const normalizedAlt = altText.replace(/^@/, "").trim(); - if (normalizedAlt) { - return { name: normalizedAlt, isPrivate: false }; - } - - const ariaMatch = entryHtml.match(/aria-label="([^"]+)"/i); - const ariaText = ariaMatch ? stripHtml(ariaMatch[1]) : ""; - const normalizedAria = ariaText.replace(/^@/, "").trim(); - if (normalizedAria && !/private sponsor/i.test(normalizedAria)) { - return { name: normalizedAria, isPrivate: false }; - } - - const hrefMatch = entryHtml.match(/]+href="\/([^"/?#]+)"/i); - const normalizedHref = hrefMatch - ? decodeHtmlEntities(hrefMatch[1]).trim() - : ""; - if (normalizedHref && !/sponsors/i.test(normalizedHref)) { - return { name: normalizedHref.replace(/^@/, ""), isPrivate: false }; - } - - return { name: "Private", isPrivate: true }; -} - -function parseSupportersFromHtml(html: string): ThankYouSupporter[] { - const sponsorsSectionMatch = html.match( - /
([\s\S]*?)<\/remote-pagination>/i, - ); - if (!sponsorsSectionMatch) { - return []; - } - - const listHtml = sponsorsSectionMatch[1]; - const entryMatches = - listHtml.match(/
]*>[\s\S]*?<\/div>/gi) ?? - []; - const supporters: ThankYouSupporter[] = []; - const seenKeys = new Set(); - - for (const entryHtml of entryMatches) { - const { name, isPrivate } = parseSupporterName(entryHtml); - const hrefMatch = entryHtml.match(/]+href="([^"]+)"/i); - const profileUrl = isPrivate ? undefined : normalizeUrl(hrefMatch?.[1]); - const avatarMatch = entryHtml.match(/]+src="([^"]+)"/i); - const avatarUrl = normalizeUrl(avatarMatch?.[1]); - const dedupeKey = `${name}|${profileUrl ?? ""}|${avatarUrl ?? ""}`; - if (seenKeys.has(dedupeKey)) continue; - seenKeys.add(dedupeKey); - supporters.push({ - name: name || "Private", - avatarUrl, - profileUrl, - isPrivate: isPrivate || !name, - source: isPrivate || !name ? "private" : "github", - }); - } - - return supporters; -} - -function getSupporterDedupeKey(supporter: ThankYouSupporter): string { - const profileUrl = supporter.profileUrl?.trim().toLowerCase(); - if (profileUrl) return `profile:${profileUrl}`; - return `name:${supporter.name.trim().toLowerCase()}|private:${supporter.isPrivate}`; -} - -function mergeThanksSupporters( - ...supporterGroups: readonly (readonly ThankYouSupporter[])[] -): ThankYouSupporter[] { - const supporters: ThankYouSupporter[] = []; - const seenKeys = new Set(); - - for (const group of supporterGroups) { - for (const supporter of group) { - const dedupeKey = getSupporterDedupeKey(supporter); - if (seenKeys.has(dedupeKey)) continue; - seenKeys.add(dedupeKey); - supporters.push({ ...supporter }); - } - } - - return supporters; -} - -async function fetchThanksSupporters(): Promise { - const response = await fetchWithTimeout( - THANKS_SUPPORTERS_URL, - { - headers: { - ...THANKS_REQUEST_HEADERS, - Accept: "text/html,application/xhtml+xml", - }, - }, - THANKS_FETCH_TIMEOUT_MS, - "GitHub sponsors request", - ); - if (!response.ok) { - throw new Error(`GitHub sponsors page request failed (${response.status})`); - } - - const html = await withTimeout( - response.text(), - THANKS_FETCH_TIMEOUT_MS, - "GitHub sponsors response", - ); - const supporters = parseSupportersFromHtml(html); - return supporters; -} - -async function fetchThanksData(): Promise { - const result: ThankYouDataResult = { - contributors: [], - supporters: [], - }; - - const [contributorsResult, supportersResult] = await Promise.allSettled([ - fetchThanksContributors(), - fetchThanksSupporters(), - ]); - - if (contributorsResult.status === "fulfilled") { - result.contributors = contributorsResult.value; - } else { - result.contributorsError = - contributorsResult.reason instanceof Error - ? contributorsResult.reason.message - : "Unable to load contributors right now."; - } - - if (supportersResult.status === "fulfilled") { - result.supporters = mergeThanksSupporters( - THANKS_CUSTOM_SUPPORTERS, - supportersResult.value, - ); - if (result.supporters.length === 0) { - result.supportersError = - "No public supporters were found on GitHub Sponsors."; - } - } else { - result.supporters = mergeThanksSupporters(THANKS_CUSTOM_SUPPORTERS); - result.supportersError = - supportersResult.reason instanceof Error - ? supportersResult.reason.message - : "Unable to load supporters right now."; - } - - return result; -} - function registerIpcHandlers(): void { registerAccountCatalogIpcHandlers({ ipcMain, @@ -881,365 +467,27 @@ function registerIpcHandlers(): void { getMainWindow: () => mainWindow, }); - ipcMain.handle(IPC_CHANNELS.DISCORD_CLEAR_ACTIVITY, async () => { - void clearActivity(); - }); - - ipcMain.handle(IPC_CHANNELS.DISCORD_SET_ACTIVITY, async (_event, activity: DiscordActivityUpdate) => { - if (!settingsManager.get("discordRichPresence")) { - return; - } - - void setActivity({ - ...activity, - startTimestamp: activity.startTimestampMs ? new Date(activity.startTimestampMs) : undefined, - }); - }); - - // Toggle fullscreen via IPC (for completeness) - ipcMain.handle(IPC_CHANNELS.TOGGLE_FULLSCREEN, async () => { - if (mainWindow && !mainWindow.isDestroyed()) { - const isFullScreen = mainWindow.isFullScreen(); - const nextFullscreen = !isFullScreen; - mainWindow.setFullScreen(nextFullscreen); - rendererControlledFullscreen = nextFullscreen; - } - }); - - ipcMain.handle( - IPC_CHANNELS.SET_FULLSCREEN, - async (_event, value: boolean) => { - if (mainWindow && !mainWindow.isDestroyed()) { - try { - const nextFullscreen = Boolean(value); - mainWindow.setFullScreen(nextFullscreen); - rendererControlledFullscreen = nextFullscreen; - } catch (err) { - console.warn("Failed to set fullscreen:", err); - } - } - }, - ); - - // Toggle pointer lock via IPC (F8 shortcut) - ipcMain.handle(IPC_CHANNELS.TOGGLE_POINTER_LOCK, async () => { - if (mainWindow && !mainWindow.isDestroyed()) { - mainWindow.webContents.send("app:toggle-pointer-lock"); - } - }); - - ipcMain.handle(IPC_CHANNELS.QUIT_APP, async () => { - requestAppShutdown({ - reason: "renderer-explicit-exit", - forceExitFallback: true, - }); - }); - - ipcMain.handle(IPC_CHANNELS.OPEN_EXTERNAL_URL, async (_event, url: string): Promise => { - await openExternalHttpUrl(url); - }); - - ipcMain.handle( - IPC_CHANNELS.DIRECT_LAUNCH_GET_PENDING, - async (): Promise => { - const request = pendingDirectLaunchRequest; - pendingDirectLaunchRequest = null; - return request; - }, - ); - - ipcMain.handle( - IPC_CHANNELS.APP_UPDATER_GET_STATE, - async (): Promise => { - const buildInfo = getAppBuildInfo(); - return ( - appUpdater?.getState() ?? { - status: "disabled", - currentVersion: buildInfo.version, - currentDisplayVersion: buildInfo.displayVersion, - currentBuildNumber: buildInfo.buildNumber, - updateSource: "github-releases", - canCheck: false, - canDownload: false, - canInstall: false, - isPackaged: app.isPackaged, - message: "Updater is unavailable.", - } - ); - }, - ); - - ipcMain.handle( - IPC_CHANNELS.APP_UPDATER_CHECK, - async (): Promise => { - const buildInfo = getAppBuildInfo(); - return ( - appUpdater?.checkForUpdates("manual") ?? { - status: "disabled", - currentVersion: buildInfo.version, - currentDisplayVersion: buildInfo.displayVersion, - currentBuildNumber: buildInfo.buildNumber, - updateSource: "github-releases", - canCheck: false, - canDownload: false, - canInstall: false, - isPackaged: app.isPackaged, - message: "Updater is unavailable.", - } - ); - }, - ); - - ipcMain.handle( - IPC_CHANNELS.APP_UPDATER_DOWNLOAD, - async (): Promise => { - const buildInfo = getAppBuildInfo(); - return ( - appUpdater?.downloadUpdate() ?? { - status: "disabled", - currentVersion: buildInfo.version, - currentDisplayVersion: buildInfo.displayVersion, - currentBuildNumber: buildInfo.buildNumber, - updateSource: "github-releases", - canCheck: false, - canDownload: false, - canInstall: false, - isPackaged: app.isPackaged, - message: "Updater is unavailable.", - } - ); - }, - ); - - ipcMain.handle( - IPC_CHANNELS.APP_UPDATER_INSTALL, - async (): Promise => { - const buildInfo = getAppBuildInfo(); - return ( - appUpdater?.quitAndInstall() ?? { - status: "disabled", - currentVersion: buildInfo.version, - currentDisplayVersion: buildInfo.displayVersion, - currentBuildNumber: buildInfo.buildNumber, - updateSource: "github-releases", - canCheck: false, - canDownload: false, - canInstall: false, - isPackaged: app.isPackaged, - message: "Updater is unavailable.", - } - ); - }, - ); - - // Settings IPC handlers - ipcMain.handle(IPC_CHANNELS.SETTINGS_GET, async (): Promise => { - return settingsManager.getAll(); - }); - - ipcMain.handle(IPC_CHANNELS.CLIPBOARD_READ_TEXT, async (): Promise => { - return clipboard.readText(); - }); - - ipcMain.handle( - IPC_CHANNELS.SETTINGS_SET, - async ( - _event: Electron.IpcMainInvokeEvent, - key: K, - value: Settings[K], - ) => { - settingsManager.set(key, value); - const appliedValue = settingsManager.get(key); - // React to certain setting changes immediately in main process - try { - if (key === "autoCheckForUpdates") { - appUpdater?.setAutomaticChecksEnabled(appliedValue as boolean); - } - signalingCoordinator?.applySettingsChange(key, appliedValue); - if (key === "discordRichPresence") { - if (appliedValue) { - void connectDiscordRpc().then(() => discordMonitor.start()); - } else { - discordMonitor.stop(); - void destroyDiscordRpc(); - } - } - } catch (err) { - console.warn("Failed to apply setting change in main process:", err); - } - }, - ); - - ipcMain.handle(IPC_CHANNELS.SETTINGS_RESET, async (): Promise => { - const resetSettings = settingsManager.reset(); - appUpdater?.setAutomaticChecksEnabled(resetSettings.autoCheckForUpdates); - signalingCoordinator?.stopNativeStreamer("settings reset"); - signalingCoordinator?.resetNativeStreamerContext(); - return resetSettings; - }); - - ipcMain.handle( - IPC_CHANNELS.SETTINGS_SELECT_NATIVE_STREAMER_EXECUTABLE, - async (): Promise => { - const filters = - process.platform === "win32" - ? [ - { name: "Executable", extensions: ["exe"] }, - { name: "All Files", extensions: ["*"] }, - ] - : [{ name: "All Files", extensions: ["*"] }]; - - const options: Electron.OpenDialogOptions = { - title: "Select OpenNOW streamer executable", - properties: ["openFile"], - filters, - }; - const result = - mainWindow && !mainWindow.isDestroyed() - ? await dialog.showOpenDialog(mainWindow, options) - : await dialog.showOpenDialog(options); - - if (result.canceled || result.filePaths.length === 0) { - return null; - } - return result.filePaths[0] ?? null; - }, - ); - - ipcMain.handle( - IPC_CHANNELS.MICROPHONE_PERMISSION_GET, - async (): Promise => { - if (process.platform !== "darwin") { - return { - platform: process.platform, - isMacOs: false, - status: "not-applicable", - granted: false, - canRequest: false, - shouldUseBrowserApi: true, - }; - } - - const currentStatus = - systemPreferences.getMediaAccessStatus("microphone"); - console.log("[Main] macOS microphone permission status:", currentStatus); - - if (currentStatus === "granted") { - return { - platform: process.platform, - isMacOs: true, - status: "granted", - granted: true, - canRequest: false, - shouldUseBrowserApi: true, - }; - } - - if (currentStatus === "not-determined") { - const granted = await systemPreferences.askForMediaAccess("microphone"); - const nextStatus = systemPreferences.getMediaAccessStatus("microphone"); - console.log( - "[Main] Requested macOS microphone permission:", - granted, - nextStatus, - ); - return { - platform: process.platform, - isMacOs: true, - status: nextStatus, - granted, - canRequest: nextStatus === "not-determined", - shouldUseBrowserApi: granted, - }; - } - - return { - platform: process.platform, - isMacOs: true, - status: currentStatus, - granted: false, - canRequest: false, - shouldUseBrowserApi: false, - }; - }, - ); - - // Logs export IPC handler - ipcMain.handle( - IPC_CHANNELS.LOGS_EXPORT, - async (_event, format: "text" | "json" = "text"): Promise => { - return exportLogs(format); - }, - ); - - registerMediaIpcHandlers({ + registerCoreIpcHandlers({ ipcMain, + app, dialog, shell, + clipboard, + systemPreferences, + settingsManager, + refreshScheduler, getMainWindow: () => mainWindow, - }); - - ipcMain.handle(IPC_CHANNELS.CACHE_REFRESH_MANUAL, async (): Promise => { - await refreshScheduler.manualRefresh(); - }); - - ipcMain.handle(IPC_CHANNELS.CACHE_DELETE_ALL, async (): Promise => { - await cacheManager.deleteAll(); - console.log("[IPC] Cache deletion completed successfully"); - }); - - ipcMain.handle( - IPC_CHANNELS.COMMUNITY_GET_THANKS, - async (): Promise => { - return fetchThanksData(); + setRendererControlledFullscreen: (value) => { + rendererControlledFullscreen = value; }, - ); - - ipcMain.handle( - IPC_CHANNELS.COMMUNITY_PROVISION_SESSION_PROXY, - async (): Promise => { - return provisionZortosCommunityProxy(); - }, - ); - - ipcMain.handle( - IPC_CHANNELS.PING_REGIONS, - async (_event, regions: StreamRegion[]): Promise => { - return pingRegions(regions); + getPendingDirectLaunchRequest: () => pendingDirectLaunchRequest, + setPendingDirectLaunchRequest: (request) => { + pendingDirectLaunchRequest = request; }, - ); - - // PrintedWaste queue API — fetched from main process so User-Agent can be set - ipcMain.handle(IPC_CHANNELS.PRINTEDWASTE_QUEUE_FETCH, async () => { - return fetchPrintedWasteQueue(app.getVersion()); - }); - - ipcMain.handle(IPC_CHANNELS.PRINTEDWASTE_SERVER_MAPPING_FETCH, async () => { - return fetchPrintedWasteServerMapping(app.getVersion()); - }); - - // Release highlights IPC handlers - ipcMain.handle( - IPC_CHANNELS.RELEASE_HIGHLIGHTS_GET, - async (_event, version?: string): Promise => { - const appVersion = normalizeReleaseVersion(app.getVersion()) ?? "0.0.0"; - const targetVersion = normalizeReleaseVersion(version ?? appVersion) ?? appVersion; - return getReleaseHighlightsPayload(targetVersion); - }, - ); - - ipcMain.handle(IPC_CHANNELS.RELEASE_HIGHLIGHTS_ACK, async (): Promise => { - settingsManager.set("lastSeenReleaseHighlightsVersion", app.getVersion().replace(/^v/, "")); - }); - - // Save window size when it changes (skip fullscreen so the saved size - // stays meaningful for windowed launches, e.g. after console mode) - mainWindow?.on("resize", () => { - if (mainWindow && !mainWindow.isDestroyed() && !mainWindow.isFullScreen()) { - const [width, height] = mainWindow.getSize(); - settingsManager.set("windowWidth", width); - settingsManager.set("windowHeight", height); - } + getAppUpdater: () => appUpdater, + getSignalingCoordinator: () => signalingCoordinator, + discordMonitor, + requestAppShutdown, }); } @@ -1272,9 +520,11 @@ app.whenReady().then(async () => { await authService.initialize(); settingsManager = getSettingsManager(); + configureIdentifyAsSteamDeck(() => settingsManager.get("identifyAsSteamDeck")); appUpdater = createAppUpdaterController({ onStateChanged: emitUpdaterStateToRenderer, automaticChecksEnabled: settingsManager.get("autoCheckForUpdates"), + updateChannel: settingsManager.get("updateChannel"), onBeforeQuitAndInstall: () => { isUpdaterInstallQuitInProgress = true; clearExplicitShutdownFallback(); @@ -1289,6 +539,9 @@ app.whenReady().then(async () => { void connectDiscordRpc().then(() => discordMonitor.start()); } + // Start anonymous error reporting only when the user has granted consent + syncMainTelemetry(settingsManager); + // Set up permission handlers for getUserMedia, fullscreen, pointer lock session.defaultSession.setPermissionRequestHandler( (webContents, permission, callback) => { @@ -1366,7 +619,7 @@ app.whenReady().then(async () => { refreshScheduler.start(); - await createMainWindow(); + await createMainWindow(createMainWindowDeps()); appUpdater.initialize(); // Fire-and-forget: check if we should show release highlights after the window loads @@ -1401,7 +654,7 @@ app.whenReady().then(async () => { return; } if (BrowserWindow.getAllWindows().length === 0) { - await createMainWindow(); + await createMainWindow(createMainWindowDeps()); } }); }); diff --git a/opennow-stable/src/main/ipc/accountCatalogHandlers.ts b/opennow-stable/src/main/ipc/accountCatalogHandlers.ts index db8fdf454..bc8d6b96a 100644 --- a/opennow-stable/src/main/ipc/accountCatalogHandlers.ts +++ b/opennow-stable/src/main/ipc/accountCatalogHandlers.ts @@ -1,436 +1,436 @@ -import type { IpcMain } from "electron"; -import { IPC_CHANNELS } from "@shared/ipc"; -import type { - AuthLoginRequest, - AuthDeviceLoginAttemptRequest, - AuthDeviceLoginPollRequest, - AuthDeviceLoginStartRequest, - AuthSessionRequest, - CatalogBrowseRequest, - GamesFetchRequest, - MarkGameOwnedRequest, - RegionsFetchRequest, - ResolveLaunchIdRequest, - ResolveStoreUrlRequest, - SubscriptionFetchRequest, - PersistentStorageLocationsFetchRequest, - PersistentStorageResetRequest, - GameAccountOperationRequest, -} from "@shared/gfn"; -import type { AuthService } from "../gfn/auth"; -import { - browseCatalog, - fetchFeaturedGames, - fetchLibraryGames, - fetchMainGames, - fetchPublicGames, - fetchStorePanels, - peekCachedBrowseCatalog, - fetchLibraryGamesFromCache, - markGameOwned, - resolveLaunchAppId, - resolveStoreUrl, -} from "../gfn/games"; -import { fetchSubscription, fetchDynamicRegions } from "../gfn/subscription"; -import { fetchPersistentStorageLocations, resetPersistentStorage } from "../gfn/persistentStorage"; -import { - fetchGameAccountConnections, - linkGameAccount, - resyncGameAccount, - unlinkGameAccount, -} from "../gfn/accountConnections"; - -interface RefreshSchedulerAuthContextUpdater { - updateAuthContext(token: string, userId: string, providerStreamingBaseUrl?: string, proxyUrl?: string): void; -} - -async function resolveGamesFetchContext( - deps: Pick, - payload: GamesFetchRequest | CatalogBrowseRequest = {}, - options: { networkRequired?: boolean } = {}, -): Promise<{ - token: string; - streamingBaseUrl: string; - userId: string; - proxyUrl?: string; -}> { - let session = deps.authService.getSession(); - if (!session || options.networkRequired) { - session = await deps.authService.ensureValidSession(); - } - if (!session) { - throw new Error("No authenticated session available"); - } - - const token = await deps.resolveJwt(payload?.token); - const streamingBaseUrl = - payload?.providerStreamingBaseUrl ?? - deps.authService.getSelectedProvider().streamingServiceUrl; - const userId = payload.userId ?? session.user.userId; - const proxyUrl = payload.proxyUrl; - deps.refreshScheduler.updateAuthContext(token, userId, streamingBaseUrl, proxyUrl); - return { token, streamingBaseUrl, userId, proxyUrl }; -} - -function savedSessionTokens( - session: NonNullable>, -): { token: string; userId: string } | null { - const token = session.tokens.idToken ?? session.tokens.accessToken; - if (!token) { - return null; - } - return { token, userId: session.user.userId }; -} - -function sessionTokenCandidates( - session: NonNullable>>, -): [string, ...string[]] { - const candidates = [ - session.tokens.idToken, - session.tokens.accessToken, - ].filter((token): token is string => Boolean(token)); - if (!candidates[0]) { - throw new Error("No authenticated token available"); - } - return candidates as [string, ...string[]]; -} - -export interface AccountCatalogIpcHandlerDeps { - ipcMain: IpcMain; - authService: AuthService; - resolveJwt(token?: string): Promise; - refreshScheduler: RefreshSchedulerAuthContextUpdater; -} - -export function registerAccountCatalogIpcHandlers( - deps: AccountCatalogIpcHandlerDeps, -): void { - const { ipcMain, authService, refreshScheduler, resolveJwt } = deps; - - const resolveGamesContext = (payload: GamesFetchRequest | CatalogBrowseRequest = {}) => - resolveGamesFetchContext(deps, payload); - - ipcMain.handle( - IPC_CHANNELS.AUTH_GET_SESSION, - async (_event, payload: AuthSessionRequest = {}) => { - return authService.ensureValidSessionWithStatus( - Boolean(payload.forceRefresh), - ); - }, - ); - - ipcMain.handle(IPC_CHANNELS.AUTH_GET_PROVIDERS, async () => { - return authService.getProviders(); - }); - - ipcMain.handle( - IPC_CHANNELS.AUTH_GET_REGIONS, - async (_event, payload: RegionsFetchRequest) => { - return authService.getRegions(payload?.token); - }, - ); - - ipcMain.handle( - IPC_CHANNELS.AUTH_LOGIN, - async (_event, payload: AuthLoginRequest) => { - return authService.login(payload); - }, - ); - - ipcMain.handle( - IPC_CHANNELS.AUTH_DEVICE_LOGIN_START, - async (_event, payload: AuthDeviceLoginStartRequest) => { - return authService.startDeviceLogin(payload); - }, - ); - - ipcMain.handle( - IPC_CHANNELS.AUTH_DEVICE_LOGIN_POLL, - async (_event, payload: AuthDeviceLoginPollRequest) => { - return authService.pollDeviceLogin(payload); - }, - ); - - ipcMain.handle( - IPC_CHANNELS.AUTH_DEVICE_LOGIN_COMPLETE, - async (_event, payload: AuthDeviceLoginAttemptRequest) => { - return authService.completeDeviceLogin(payload); - }, - ); - - ipcMain.handle( - IPC_CHANNELS.AUTH_DEVICE_LOGIN_CANCEL, - async (_event, payload: AuthDeviceLoginAttemptRequest) => { - authService.cancelDeviceLogin(payload); - }, - ); - - ipcMain.handle(IPC_CHANNELS.AUTH_LOGOUT, async () => { - await authService.logout(); - }); - - ipcMain.handle(IPC_CHANNELS.AUTH_LOGOUT_ALL, async () => { - await authService.logoutAll(); - }); - - ipcMain.handle(IPC_CHANNELS.AUTH_GET_SAVED_ACCOUNTS, async () => { - return authService.getSavedAccounts(); - }); - - ipcMain.handle( - IPC_CHANNELS.AUTH_SWITCH_ACCOUNT, - async (_event, userId: string) => { - return authService.switchAccount(userId); - }, - ); - - ipcMain.handle( - IPC_CHANNELS.AUTH_REMOVE_ACCOUNT, - async (_event, userId: string) => { - await authService.removeAccount(userId); - }, - ); - - ipcMain.handle( - IPC_CHANNELS.SUBSCRIPTION_FETCH, - async (_event, payload: SubscriptionFetchRequest) => { - const token = await resolveJwt(payload?.token); - const streamingBaseUrl = - payload?.providerStreamingBaseUrl ?? - authService.getSelectedProvider().streamingServiceUrl; - const userId = payload.userId; - - const { vpcId } = await fetchDynamicRegions(token, streamingBaseUrl); - - return fetchSubscription(token, userId, vpcId ?? undefined); - }, - ); - - ipcMain.handle( - IPC_CHANNELS.PERSISTENT_STORAGE_LOCATIONS_FETCH, - async (_event, payload: PersistentStorageLocationsFetchRequest = {}) => { - const session = await authService.ensureValidSession(); - if (!session) { - throw new Error("No authenticated session available"); - } - - let vpcId = payload.serverRegionId ?? undefined; - if (!vpcId) { - const streamingBaseUrl = authService.getSelectedProvider().streamingServiceUrl; - const dynamicRegions = await fetchDynamicRegions(session.tokens.accessToken, streamingBaseUrl); - vpcId = dynamicRegions.vpcId ?? undefined; - } - - const [idToken, ...idTokenAlternates] = sessionTokenCandidates(session); - return fetchPersistentStorageLocations({ - idToken, - idTokenAlternates, - vpcId, - locale: payload.locale, - currentRegionCode: payload.currentRegionCode, - currentRegionName: payload.currentRegionName, - }); - }, - ); - - ipcMain.handle( - IPC_CHANNELS.PERSISTENT_STORAGE_RESET, - async (_event, payload: PersistentStorageResetRequest = {}) => { - const session = await authService.ensureValidSession(); - if (!session) { - throw new Error("No authenticated session available"); - } - - const [idToken, ...idTokenAlternates] = sessionTokenCandidates(session); - const result = await resetPersistentStorage({ - idToken, - idTokenAlternates, - storageRegion: payload.storageRegion ?? null, - }); - authService.clearSubscriptionCache(); - return result; - }, - ); - - const ensureGameAccountSession = async () => { - const session = await authService.ensureValidSession(); - if (!session) { - throw new Error("No authenticated session available"); - } - return session; - }; - - ipcMain.handle(IPC_CHANNELS.GAME_ACCOUNTS_FETCH, async () => { - const session = await ensureGameAccountSession(); - return fetchGameAccountConnections(session); - }); - - ipcMain.handle( - IPC_CHANNELS.GAME_ACCOUNT_LINK, - async (_event, payload: GameAccountOperationRequest) => { - const session = await ensureGameAccountSession(); - return linkGameAccount(session, payload.provider, payload.proxyUrl); - }, - ); - - ipcMain.handle( - IPC_CHANNELS.GAME_ACCOUNT_UNLINK, - async (_event, payload: GameAccountOperationRequest) => { - const session = await ensureGameAccountSession(); - return unlinkGameAccount(session, payload.provider, payload.proxyUrl); - }, - ); - - ipcMain.handle( - IPC_CHANNELS.GAME_ACCOUNT_RESYNC, - async (_event, payload: GameAccountOperationRequest) => { - const session = await ensureGameAccountSession(); - return resyncGameAccount(session, payload.provider, payload.proxyUrl); - }, - ); - - ipcMain.handle( - IPC_CHANNELS.GAMES_FETCH_MAIN, - async (_event, payload: GamesFetchRequest) => { - const { token, streamingBaseUrl, userId, proxyUrl } = await resolveGamesContext(payload); - return fetchMainGames(token, streamingBaseUrl, userId, proxyUrl); - }, - ); - - ipcMain.handle( - IPC_CHANNELS.GAMES_FETCH_FEATURED, - async (_event, payload: GamesFetchRequest) => { - const { token, streamingBaseUrl, userId, proxyUrl } = await resolveGamesContext(payload); - return fetchFeaturedGames(token, streamingBaseUrl, userId, proxyUrl); - }, - ); - - ipcMain.handle( - IPC_CHANNELS.GAMES_FETCH_STORE_PANELS, - async (_event, payload: GamesFetchRequest) => { - const { token, streamingBaseUrl, userId, proxyUrl } = await resolveGamesContext(payload); - return fetchStorePanels(token, streamingBaseUrl, userId, proxyUrl); - }, - ); - - ipcMain.handle( - IPC_CHANNELS.GAMES_FETCH_LIBRARY, - async (_event, payload: GamesFetchRequest) => { - const savedSession = authService.getSession(); - const streamingBaseUrl = - payload?.providerStreamingBaseUrl ?? - authService.getSelectedProvider().streamingServiceUrl; - if (savedSession) { - const tokens = savedSessionTokens(savedSession); - if (tokens) { - const userId = payload.userId ?? tokens.userId; - const cachedLibrary = await fetchLibraryGamesFromCache(tokens.token, streamingBaseUrl, userId, payload.proxyUrl); - if (cachedLibrary) { - refreshScheduler.updateAuthContext(tokens.token, userId, streamingBaseUrl, payload.proxyUrl); - return cachedLibrary; - } - } - } - - const { token, streamingBaseUrl: resolvedBaseUrl, userId, proxyUrl } = await resolveGamesFetchContext( - deps, - payload, - { networkRequired: true }, - ); - return fetchLibraryGames(token, resolvedBaseUrl, userId, proxyUrl); - }, - ); - - ipcMain.handle( - IPC_CHANNELS.GAMES_BROWSE_CATALOG, - async (_event, payload: CatalogBrowseRequest) => { - const savedSession = authService.getSession(); - const streamingBaseUrl = - payload?.providerStreamingBaseUrl ?? - authService.getSelectedProvider().streamingServiceUrl; - if (savedSession) { - const tokens = savedSessionTokens(savedSession); - if (tokens) { - const userId = payload.userId ?? tokens.userId; - const cached = await peekCachedBrowseCatalog({ - ...payload, - token: tokens.token, - userId, - providerStreamingBaseUrl: streamingBaseUrl, - }); - if (cached) { - refreshScheduler.updateAuthContext(tokens.token, userId, streamingBaseUrl, payload.proxyUrl); - return cached; - } - } - } - - const { token, streamingBaseUrl: resolvedBaseUrl, userId, proxyUrl } = await resolveGamesFetchContext( - deps, - payload, - { networkRequired: true }, - ); - return browseCatalog({ - ...payload, - token, - userId, - providerStreamingBaseUrl: resolvedBaseUrl, - proxyUrl, - }); - }, - ); - - ipcMain.handle(IPC_CHANNELS.GAMES_FETCH_PUBLIC, async () => { - return fetchPublicGames(); - }); - - ipcMain.handle( - IPC_CHANNELS.GAMES_RESOLVE_LAUNCH_ID, - async (_event, payload: ResolveLaunchIdRequest) => { - const token = await resolveJwt(payload?.token); - const streamingBaseUrl = - payload?.providerStreamingBaseUrl ?? - authService.getSelectedProvider().streamingServiceUrl; - return resolveLaunchAppId(token, payload.appIdOrUuid, streamingBaseUrl, payload.proxyUrl); - }, - ); - - ipcMain.handle( - IPC_CHANNELS.GAMES_RESOLVE_STORE_URL, - async (_event, payload: ResolveStoreUrlRequest) => { - const { token, streamingBaseUrl } = await resolveGamesContext(payload); - return resolveStoreUrl(token, payload.appIdOrUuid, streamingBaseUrl, { - variantId: payload.variantId, - store: payload.store, - proxyUrl: payload.proxyUrl, - }); - }, - ); - - ipcMain.handle( - IPC_CHANNELS.GAMES_MARK_OWNED, - async (_event, payload: MarkGameOwnedRequest) => { - const session = await authService.ensureValidSession(); - if (!session) { - throw new Error("No authenticated session available"); - } - - const token = await resolveJwt(payload?.token ?? session.tokens.idToken ?? session.tokens.accessToken); - const streamingBaseUrl = - payload?.providerStreamingBaseUrl ?? - authService.getSelectedProvider().streamingServiceUrl; - const userId = payload.userId ?? session.user.userId; - const proxyUrl = payload.proxyUrl; - refreshScheduler.updateAuthContext(token, userId, streamingBaseUrl, proxyUrl); - - return markGameOwned({ - token, - userId, - variantId: payload.variantId, - providerStreamingBaseUrl: streamingBaseUrl, - proxyUrl, - tokens: [session.tokens.idToken, session.tokens.accessToken], - }); - }, - ); -} +import type { IpcMain } from "electron"; +import { IPC_CHANNELS } from "@shared/ipc"; +import type { + AuthLoginRequest, + AuthDeviceLoginAttemptRequest, + AuthDeviceLoginPollRequest, + AuthDeviceLoginStartRequest, + AuthSessionRequest, + CatalogBrowseRequest, + GamesFetchRequest, + MarkGameOwnedRequest, + RegionsFetchRequest, + ResolveLaunchIdRequest, + ResolveStoreUrlRequest, + SubscriptionFetchRequest, + PersistentStorageLocationsFetchRequest, + PersistentStorageResetRequest, + GameAccountOperationRequest, +} from "@shared/gfn"; +import type { AuthService } from "../platforms/gfn/auth"; +import { + browseCatalog, + fetchFeaturedGames, + fetchLibraryGames, + fetchMainGames, + fetchPublicGames, + fetchStorePanels, + peekCachedBrowseCatalog, + fetchLibraryGamesFromCache, + markGameOwned, + resolveLaunchAppId, + resolveStoreUrl, +} from "../platforms/gfn/games"; +import { fetchSubscription, fetchDynamicRegions } from "../platforms/gfn/subscription"; +import { fetchPersistentStorageLocations, resetPersistentStorage } from "../platforms/gfn/persistentStorage"; +import { + fetchGameAccountConnections, + linkGameAccount, + resyncGameAccount, + unlinkGameAccount, +} from "../platforms/gfn/accountConnections"; + +interface RefreshSchedulerAuthContextUpdater { + updateAuthContext(token: string, userId: string, providerStreamingBaseUrl?: string, proxyUrl?: string): void; +} + +async function resolveGamesFetchContext( + deps: Pick, + payload: GamesFetchRequest | CatalogBrowseRequest = {}, + options: { networkRequired?: boolean } = {}, +): Promise<{ + token: string; + streamingBaseUrl: string; + userId: string; + proxyUrl?: string; +}> { + let session = deps.authService.getSession(); + if (!session || options.networkRequired) { + session = await deps.authService.ensureValidSession(); + } + if (!session) { + throw new Error("No authenticated session available"); + } + + const token = await deps.resolveJwt(payload?.token); + const streamingBaseUrl = + payload?.providerStreamingBaseUrl ?? + deps.authService.getSelectedProvider().streamingServiceUrl; + const userId = payload.userId ?? session.user.userId; + const proxyUrl = payload.proxyUrl; + deps.refreshScheduler.updateAuthContext(token, userId, streamingBaseUrl, proxyUrl); + return { token, streamingBaseUrl, userId, proxyUrl }; +} + +function savedSessionTokens( + session: NonNullable>, +): { token: string; userId: string } | null { + const token = session.tokens.idToken ?? session.tokens.accessToken; + if (!token) { + return null; + } + return { token, userId: session.user.userId }; +} + +function sessionTokenCandidates( + session: NonNullable>>, +): [string, ...string[]] { + const candidates = [ + session.tokens.idToken, + session.tokens.accessToken, + ].filter((token): token is string => Boolean(token)); + if (!candidates[0]) { + throw new Error("No authenticated token available"); + } + return candidates as [string, ...string[]]; +} + +export interface AccountCatalogIpcHandlerDeps { + ipcMain: IpcMain; + authService: AuthService; + resolveJwt(token?: string): Promise; + refreshScheduler: RefreshSchedulerAuthContextUpdater; +} + +export function registerAccountCatalogIpcHandlers( + deps: AccountCatalogIpcHandlerDeps, +): void { + const { ipcMain, authService, refreshScheduler, resolveJwt } = deps; + + const resolveGamesContext = (payload: GamesFetchRequest | CatalogBrowseRequest = {}) => + resolveGamesFetchContext(deps, payload); + + ipcMain.handle( + IPC_CHANNELS.AUTH_GET_SESSION, + async (_event, payload: AuthSessionRequest = {}) => { + return authService.ensureValidSessionWithStatus( + Boolean(payload.forceRefresh), + ); + }, + ); + + ipcMain.handle(IPC_CHANNELS.AUTH_GET_PROVIDERS, async () => { + return authService.getProviders(); + }); + + ipcMain.handle( + IPC_CHANNELS.AUTH_GET_REGIONS, + async (_event, payload: RegionsFetchRequest) => { + return authService.getRegions(payload?.token); + }, + ); + + ipcMain.handle( + IPC_CHANNELS.AUTH_LOGIN, + async (_event, payload: AuthLoginRequest) => { + return authService.login(payload); + }, + ); + + ipcMain.handle( + IPC_CHANNELS.AUTH_DEVICE_LOGIN_START, + async (_event, payload: AuthDeviceLoginStartRequest) => { + return authService.startDeviceLogin(payload); + }, + ); + + ipcMain.handle( + IPC_CHANNELS.AUTH_DEVICE_LOGIN_POLL, + async (_event, payload: AuthDeviceLoginPollRequest) => { + return authService.pollDeviceLogin(payload); + }, + ); + + ipcMain.handle( + IPC_CHANNELS.AUTH_DEVICE_LOGIN_COMPLETE, + async (_event, payload: AuthDeviceLoginAttemptRequest) => { + return authService.completeDeviceLogin(payload); + }, + ); + + ipcMain.handle( + IPC_CHANNELS.AUTH_DEVICE_LOGIN_CANCEL, + async (_event, payload: AuthDeviceLoginAttemptRequest) => { + authService.cancelDeviceLogin(payload); + }, + ); + + ipcMain.handle(IPC_CHANNELS.AUTH_LOGOUT, async () => { + await authService.logout(); + }); + + ipcMain.handle(IPC_CHANNELS.AUTH_LOGOUT_ALL, async () => { + await authService.logoutAll(); + }); + + ipcMain.handle(IPC_CHANNELS.AUTH_GET_SAVED_ACCOUNTS, async () => { + return authService.getSavedAccounts(); + }); + + ipcMain.handle( + IPC_CHANNELS.AUTH_SWITCH_ACCOUNT, + async (_event, userId: string) => { + return authService.switchAccount(userId); + }, + ); + + ipcMain.handle( + IPC_CHANNELS.AUTH_REMOVE_ACCOUNT, + async (_event, userId: string) => { + await authService.removeAccount(userId); + }, + ); + + ipcMain.handle( + IPC_CHANNELS.SUBSCRIPTION_FETCH, + async (_event, payload: SubscriptionFetchRequest) => { + const token = await resolveJwt(payload?.token); + const streamingBaseUrl = + payload?.providerStreamingBaseUrl ?? + authService.getSelectedProvider().streamingServiceUrl; + const userId = payload.userId; + + const { vpcId } = await fetchDynamicRegions(token, streamingBaseUrl); + + return fetchSubscription(token, userId, vpcId ?? undefined); + }, + ); + + ipcMain.handle( + IPC_CHANNELS.PERSISTENT_STORAGE_LOCATIONS_FETCH, + async (_event, payload: PersistentStorageLocationsFetchRequest = {}) => { + const session = await authService.ensureValidSession(); + if (!session) { + throw new Error("No authenticated session available"); + } + + let vpcId = payload.serverRegionId ?? undefined; + if (!vpcId) { + const streamingBaseUrl = authService.getSelectedProvider().streamingServiceUrl; + const dynamicRegions = await fetchDynamicRegions(session.tokens.accessToken, streamingBaseUrl); + vpcId = dynamicRegions.vpcId ?? undefined; + } + + const [idToken, ...idTokenAlternates] = sessionTokenCandidates(session); + return fetchPersistentStorageLocations({ + idToken, + idTokenAlternates, + vpcId, + locale: payload.locale, + currentRegionCode: payload.currentRegionCode, + currentRegionName: payload.currentRegionName, + }); + }, + ); + + ipcMain.handle( + IPC_CHANNELS.PERSISTENT_STORAGE_RESET, + async (_event, payload: PersistentStorageResetRequest = {}) => { + const session = await authService.ensureValidSession(); + if (!session) { + throw new Error("No authenticated session available"); + } + + const [idToken, ...idTokenAlternates] = sessionTokenCandidates(session); + const result = await resetPersistentStorage({ + idToken, + idTokenAlternates, + storageRegion: payload.storageRegion ?? null, + }); + authService.clearSubscriptionCache(); + return result; + }, + ); + + const ensureGameAccountSession = async () => { + const session = await authService.ensureValidSession(); + if (!session) { + throw new Error("No authenticated session available"); + } + return session; + }; + + ipcMain.handle(IPC_CHANNELS.GAME_ACCOUNTS_FETCH, async () => { + const session = await ensureGameAccountSession(); + return fetchGameAccountConnections(session); + }); + + ipcMain.handle( + IPC_CHANNELS.GAME_ACCOUNT_LINK, + async (_event, payload: GameAccountOperationRequest) => { + const session = await ensureGameAccountSession(); + return linkGameAccount(session, payload.provider, payload.proxyUrl); + }, + ); + + ipcMain.handle( + IPC_CHANNELS.GAME_ACCOUNT_UNLINK, + async (_event, payload: GameAccountOperationRequest) => { + const session = await ensureGameAccountSession(); + return unlinkGameAccount(session, payload.provider, payload.proxyUrl); + }, + ); + + ipcMain.handle( + IPC_CHANNELS.GAME_ACCOUNT_RESYNC, + async (_event, payload: GameAccountOperationRequest) => { + const session = await ensureGameAccountSession(); + return resyncGameAccount(session, payload.provider, payload.proxyUrl); + }, + ); + + ipcMain.handle( + IPC_CHANNELS.GAMES_FETCH_MAIN, + async (_event, payload: GamesFetchRequest) => { + const { token, streamingBaseUrl, userId, proxyUrl } = await resolveGamesContext(payload); + return fetchMainGames(token, streamingBaseUrl, userId, proxyUrl); + }, + ); + + ipcMain.handle( + IPC_CHANNELS.GAMES_FETCH_FEATURED, + async (_event, payload: GamesFetchRequest) => { + const { token, streamingBaseUrl, userId, proxyUrl } = await resolveGamesContext(payload); + return fetchFeaturedGames(token, streamingBaseUrl, userId, proxyUrl); + }, + ); + + ipcMain.handle( + IPC_CHANNELS.GAMES_FETCH_STORE_PANELS, + async (_event, payload: GamesFetchRequest) => { + const { token, streamingBaseUrl, userId, proxyUrl } = await resolveGamesContext(payload); + return fetchStorePanels(token, streamingBaseUrl, userId, proxyUrl); + }, + ); + + ipcMain.handle( + IPC_CHANNELS.GAMES_FETCH_LIBRARY, + async (_event, payload: GamesFetchRequest) => { + const savedSession = authService.getSession(); + const streamingBaseUrl = + payload?.providerStreamingBaseUrl ?? + authService.getSelectedProvider().streamingServiceUrl; + if (savedSession) { + const tokens = savedSessionTokens(savedSession); + if (tokens) { + const userId = payload.userId ?? tokens.userId; + const cachedLibrary = await fetchLibraryGamesFromCache(tokens.token, streamingBaseUrl, userId, payload.proxyUrl); + if (cachedLibrary) { + refreshScheduler.updateAuthContext(tokens.token, userId, streamingBaseUrl, payload.proxyUrl); + return cachedLibrary; + } + } + } + + const { token, streamingBaseUrl: resolvedBaseUrl, userId, proxyUrl } = await resolveGamesFetchContext( + deps, + payload, + { networkRequired: true }, + ); + return fetchLibraryGames(token, resolvedBaseUrl, userId, proxyUrl); + }, + ); + + ipcMain.handle( + IPC_CHANNELS.GAMES_BROWSE_CATALOG, + async (_event, payload: CatalogBrowseRequest) => { + const savedSession = authService.getSession(); + const streamingBaseUrl = + payload?.providerStreamingBaseUrl ?? + authService.getSelectedProvider().streamingServiceUrl; + if (savedSession) { + const tokens = savedSessionTokens(savedSession); + if (tokens) { + const userId = payload.userId ?? tokens.userId; + const cached = await peekCachedBrowseCatalog({ + ...payload, + token: tokens.token, + userId, + providerStreamingBaseUrl: streamingBaseUrl, + }); + if (cached) { + refreshScheduler.updateAuthContext(tokens.token, userId, streamingBaseUrl, payload.proxyUrl); + return cached; + } + } + } + + const { token, streamingBaseUrl: resolvedBaseUrl, userId, proxyUrl } = await resolveGamesFetchContext( + deps, + payload, + { networkRequired: true }, + ); + return browseCatalog({ + ...payload, + token, + userId, + providerStreamingBaseUrl: resolvedBaseUrl, + proxyUrl, + }); + }, + ); + + ipcMain.handle(IPC_CHANNELS.GAMES_FETCH_PUBLIC, async () => { + return fetchPublicGames(); + }); + + ipcMain.handle( + IPC_CHANNELS.GAMES_RESOLVE_LAUNCH_ID, + async (_event, payload: ResolveLaunchIdRequest) => { + const token = await resolveJwt(payload?.token); + const streamingBaseUrl = + payload?.providerStreamingBaseUrl ?? + authService.getSelectedProvider().streamingServiceUrl; + return resolveLaunchAppId(token, payload.appIdOrUuid, streamingBaseUrl, payload.proxyUrl); + }, + ); + + ipcMain.handle( + IPC_CHANNELS.GAMES_RESOLVE_STORE_URL, + async (_event, payload: ResolveStoreUrlRequest) => { + const { token, streamingBaseUrl } = await resolveGamesContext(payload); + return resolveStoreUrl(token, payload.appIdOrUuid, streamingBaseUrl, { + variantId: payload.variantId, + store: payload.store, + proxyUrl: payload.proxyUrl, + }); + }, + ); + + ipcMain.handle( + IPC_CHANNELS.GAMES_MARK_OWNED, + async (_event, payload: MarkGameOwnedRequest) => { + const session = await authService.ensureValidSession(); + if (!session) { + throw new Error("No authenticated session available"); + } + + const token = await resolveJwt(payload?.token ?? session.tokens.idToken ?? session.tokens.accessToken); + const streamingBaseUrl = + payload?.providerStreamingBaseUrl ?? + authService.getSelectedProvider().streamingServiceUrl; + const userId = payload.userId ?? session.user.userId; + const proxyUrl = payload.proxyUrl; + refreshScheduler.updateAuthContext(token, userId, streamingBaseUrl, proxyUrl); + + return markGameOwned({ + token, + userId, + variantId: payload.variantId, + providerStreamingBaseUrl: streamingBaseUrl, + proxyUrl, + tokens: [session.tokens.idToken, session.tokens.accessToken], + }); + }, + ); +} diff --git a/opennow-stable/src/main/ipc/coreHandlers.ts b/opennow-stable/src/main/ipc/coreHandlers.ts new file mode 100644 index 000000000..b5f198b4f --- /dev/null +++ b/opennow-stable/src/main/ipc/coreHandlers.ts @@ -0,0 +1,438 @@ +import type { + App, + BrowserWindow, + Clipboard, + Dialog, + IpcMain, + Shell, + SystemPreferences, +} from "electron"; +import { IPC_CHANNELS } from "@shared/ipc"; +import type { CommunityProxyProvisionResult } from "@shared/communityProxy"; +import type { DiscordActivityUpdate } from "@shared/discord"; +import type { + AppUpdaterState, + DirectLaunchRequest, + MicrophonePermissionResult, + PingResult, + Settings, + StreamRegion, + ThankYouDataResult, +} from "@shared/gfn"; +import { exportLogs } from "@shared/logger"; +import { provisionZortosCommunityProxy } from "../community/provisionSessionProxy"; +import { + connectDiscordRpc, + destroyDiscordRpc, + setActivity, + clearActivity, +} from "../discordRpc"; +import { registerMediaIpcHandlers } from "./mediaHandlers"; +import { getAppBuildInfo } from "../appBuildInfo"; +import { getReleaseHighlightsPayload, normalizeReleaseVersion } from "../releaseHighlights"; +import { cacheManager } from "../services/cacheManager"; +import { + fetchPrintedWasteQueue, + fetchPrintedWasteServerMapping, +} from "../services/printedWaste"; +import { pingRegions } from "../services/regionPing"; +import type { SettingsManager } from "../settings"; +import type { AppUpdaterController } from "../updater"; +import type { SignalingCoordinator } from "../signaling/signalingCoordinator"; +import { fetchThanksData } from "../thanks/fetchThanksData"; +import { applyTelemetrySettingsChange, syncMainTelemetry } from "../telemetry/posthog"; +import { openExternalHttpUrl } from "../window/externalUrl"; + +type DiscordMonitor = { + start(): void; + stop(): void; +}; + +export interface CoreIpcHandlerDeps { + ipcMain: IpcMain; + app: App; + dialog: Dialog; + shell: Shell; + clipboard: Clipboard; + systemPreferences: SystemPreferences; + settingsManager: SettingsManager; + refreshScheduler: { manualRefresh(): Promise }; + getMainWindow(): BrowserWindow | null; + setRendererControlledFullscreen(value: boolean): void; + getPendingDirectLaunchRequest(): DirectLaunchRequest | null; + setPendingDirectLaunchRequest(request: DirectLaunchRequest | null): void; + getAppUpdater(): AppUpdaterController | null; + getSignalingCoordinator(): SignalingCoordinator | null; + discordMonitor: DiscordMonitor; + requestAppShutdown(options?: { + reason?: string; + forceExitFallback?: boolean; + exitCode?: number; + }): void; +} + +function disabledUpdaterState(app: App): AppUpdaterState { + const buildInfo = getAppBuildInfo(); + return { + status: "disabled", + currentVersion: buildInfo.version, + currentDisplayVersion: buildInfo.displayVersion, + currentBuildNumber: buildInfo.buildNumber, + updateSource: "github-releases", + canCheck: false, + canDownload: false, + canInstall: false, + isPackaged: app.isPackaged, + message: "Updater is unavailable.", + }; +} + +export function registerCoreIpcHandlers(deps: CoreIpcHandlerDeps): void { + const { + ipcMain, + app, + dialog, + shell, + clipboard, + systemPreferences, + settingsManager, + refreshScheduler, + discordMonitor, + } = deps; + + ipcMain.handle(IPC_CHANNELS.DISCORD_CLEAR_ACTIVITY, async () => { + void clearActivity(); + }); + + ipcMain.handle( + IPC_CHANNELS.DISCORD_SET_ACTIVITY, + async (_event, activity: DiscordActivityUpdate) => { + if (!settingsManager.get("discordRichPresence")) { + return; + } + + void setActivity({ + ...activity, + startTimestamp: activity.startTimestampMs + ? new Date(activity.startTimestampMs) + : undefined, + }); + }, + ); + + // Toggle fullscreen via IPC (for completeness) + ipcMain.handle(IPC_CHANNELS.TOGGLE_FULLSCREEN, async () => { + const mainWindow = deps.getMainWindow(); + if (mainWindow && !mainWindow.isDestroyed()) { + const isFullScreen = mainWindow.isFullScreen(); + const nextFullscreen = !isFullScreen; + mainWindow.setFullScreen(nextFullscreen); + deps.setRendererControlledFullscreen(nextFullscreen); + } + }); + + ipcMain.handle( + IPC_CHANNELS.SET_FULLSCREEN, + async (_event, value: boolean) => { + const mainWindow = deps.getMainWindow(); + if (mainWindow && !mainWindow.isDestroyed()) { + try { + const nextFullscreen = Boolean(value); + mainWindow.setFullScreen(nextFullscreen); + deps.setRendererControlledFullscreen(nextFullscreen); + } catch (err) { + console.warn("Failed to set fullscreen:", err); + } + } + }, + ); + + // Toggle pointer lock via IPC (F8 shortcut) + ipcMain.handle(IPC_CHANNELS.TOGGLE_POINTER_LOCK, async () => { + const mainWindow = deps.getMainWindow(); + if (mainWindow && !mainWindow.isDestroyed()) { + mainWindow.webContents.send("app:toggle-pointer-lock"); + } + }); + + ipcMain.handle(IPC_CHANNELS.QUIT_APP, async () => { + deps.requestAppShutdown({ + reason: "renderer-explicit-exit", + forceExitFallback: true, + }); + }); + + ipcMain.handle( + IPC_CHANNELS.OPEN_EXTERNAL_URL, + async (_event, url: string): Promise => { + await openExternalHttpUrl(url); + }, + ); + + ipcMain.handle( + IPC_CHANNELS.DIRECT_LAUNCH_GET_PENDING, + async (): Promise => { + const request = deps.getPendingDirectLaunchRequest(); + deps.setPendingDirectLaunchRequest(null); + return request; + }, + ); + + ipcMain.handle( + IPC_CHANNELS.APP_UPDATER_GET_STATE, + async (): Promise => { + return deps.getAppUpdater()?.getState() ?? disabledUpdaterState(app); + }, + ); + + ipcMain.handle( + IPC_CHANNELS.APP_UPDATER_CHECK, + async (): Promise => { + return ( + deps.getAppUpdater()?.checkForUpdates("manual") ?? + disabledUpdaterState(app) + ); + }, + ); + + ipcMain.handle( + IPC_CHANNELS.APP_UPDATER_DOWNLOAD, + async (): Promise => { + return deps.getAppUpdater()?.downloadUpdate() ?? disabledUpdaterState(app); + }, + ); + + ipcMain.handle( + IPC_CHANNELS.APP_UPDATER_INSTALL, + async (): Promise => { + return deps.getAppUpdater()?.quitAndInstall() ?? disabledUpdaterState(app); + }, + ); + + // Settings IPC handlers + ipcMain.handle(IPC_CHANNELS.SETTINGS_GET, async (): Promise => { + return settingsManager.getAll(); + }); + + ipcMain.handle(IPC_CHANNELS.CLIPBOARD_READ_TEXT, async (): Promise => { + return clipboard.readText(); + }); + + ipcMain.handle( + IPC_CHANNELS.SETTINGS_SET, + async ( + _event: Electron.IpcMainInvokeEvent, + key: K, + value: Settings[K], + ) => { + settingsManager.set(key, value); + const appliedValue = settingsManager.get(key); + // React to certain setting changes immediately in main process + try { + if (key === "autoCheckForUpdates") { + deps.getAppUpdater()?.setAutomaticChecksEnabled(appliedValue as boolean); + } + if (key === "updateChannel") { + deps.getAppUpdater()?.setUpdateChannel(appliedValue as Settings["updateChannel"]); + } + deps.getSignalingCoordinator()?.applySettingsChange(key, appliedValue); + if (key === "discordRichPresence") { + if (appliedValue) { + void connectDiscordRpc().then(() => discordMonitor.start()); + } else { + discordMonitor.stop(); + void destroyDiscordRpc(); + } + } + applyTelemetrySettingsChange(settingsManager, key, appliedValue); + } catch (err) { + console.warn("Failed to apply setting change in main process:", err); + } + }, + ); + + ipcMain.handle(IPC_CHANNELS.SETTINGS_RESET, async (): Promise => { + const resetSettings = settingsManager.reset(); + deps + .getAppUpdater() + ?.setAutomaticChecksEnabled(resetSettings.autoCheckForUpdates); + deps.getAppUpdater()?.setUpdateChannel(resetSettings.updateChannel); + deps.getSignalingCoordinator()?.stopNativeStreamer("settings reset"); + deps.getSignalingCoordinator()?.resetNativeStreamerContext(); + syncMainTelemetry(settingsManager); + return resetSettings; + }); + + ipcMain.handle( + IPC_CHANNELS.SETTINGS_SELECT_NATIVE_STREAMER_EXECUTABLE, + async (): Promise => { + const filters = + process.platform === "win32" + ? [ + { name: "Executable", extensions: ["exe"] }, + { name: "All Files", extensions: ["*"] }, + ] + : [{ name: "All Files", extensions: ["*"] }]; + + const options: Electron.OpenDialogOptions = { + title: "Select OpenNOW streamer executable", + properties: ["openFile"], + filters, + }; + const mainWindow = deps.getMainWindow(); + const result = + mainWindow && !mainWindow.isDestroyed() + ? await dialog.showOpenDialog(mainWindow, options) + : await dialog.showOpenDialog(options); + + if (result.canceled || result.filePaths.length === 0) { + return null; + } + return result.filePaths[0] ?? null; + }, + ); + + ipcMain.handle( + IPC_CHANNELS.MICROPHONE_PERMISSION_GET, + async (): Promise => { + if (process.platform !== "darwin") { + return { + platform: process.platform, + isMacOs: false, + status: "not-applicable", + granted: false, + canRequest: false, + shouldUseBrowserApi: true, + }; + } + + const currentStatus = + systemPreferences.getMediaAccessStatus("microphone"); + console.log("[Main] macOS microphone permission status:", currentStatus); + + if (currentStatus === "granted") { + return { + platform: process.platform, + isMacOs: true, + status: "granted", + granted: true, + canRequest: false, + shouldUseBrowserApi: true, + }; + } + + if (currentStatus === "not-determined") { + const granted = await systemPreferences.askForMediaAccess("microphone"); + const nextStatus = systemPreferences.getMediaAccessStatus("microphone"); + console.log( + "[Main] Requested macOS microphone permission:", + granted, + nextStatus, + ); + return { + platform: process.platform, + isMacOs: true, + status: nextStatus, + granted, + canRequest: nextStatus === "not-determined", + shouldUseBrowserApi: granted, + }; + } + + return { + platform: process.platform, + isMacOs: true, + status: currentStatus, + granted: false, + canRequest: false, + shouldUseBrowserApi: false, + }; + }, + ); + + // Logs export IPC handler + ipcMain.handle( + IPC_CHANNELS.LOGS_EXPORT, + async (_event, format: "text" | "json" = "text"): Promise => { + return exportLogs(format); + }, + ); + + registerMediaIpcHandlers({ + ipcMain, + dialog, + shell, + getMainWindow: deps.getMainWindow, + }); + + ipcMain.handle(IPC_CHANNELS.CACHE_REFRESH_MANUAL, async (): Promise => { + await refreshScheduler.manualRefresh(); + }); + + ipcMain.handle(IPC_CHANNELS.CACHE_DELETE_ALL, async (): Promise => { + await cacheManager.deleteAll(); + console.log("[IPC] Cache deletion completed successfully"); + }); + + ipcMain.handle( + IPC_CHANNELS.COMMUNITY_GET_THANKS, + async (): Promise => { + return fetchThanksData(); + }, + ); + + ipcMain.handle( + IPC_CHANNELS.COMMUNITY_PROVISION_SESSION_PROXY, + async (): Promise => { + return provisionZortosCommunityProxy(); + }, + ); + + ipcMain.handle( + IPC_CHANNELS.PING_REGIONS, + async (_event, regions: StreamRegion[]): Promise => { + return pingRegions(regions); + }, + ); + + // PrintedWaste queue API — fetched from main process so User-Agent can be set + ipcMain.handle(IPC_CHANNELS.PRINTEDWASTE_QUEUE_FETCH, async () => { + return fetchPrintedWasteQueue(app.getVersion()); + }); + + ipcMain.handle(IPC_CHANNELS.PRINTEDWASTE_SERVER_MAPPING_FETCH, async () => { + return fetchPrintedWasteServerMapping(app.getVersion()); + }); + + // Release highlights IPC handlers + ipcMain.handle( + IPC_CHANNELS.RELEASE_HIGHLIGHTS_GET, + async ( + _event, + version?: string, + ): Promise => { + const appVersion = normalizeReleaseVersion(app.getVersion()) ?? "0.0.0"; + const targetVersion = + normalizeReleaseVersion(version ?? appVersion) ?? appVersion; + return getReleaseHighlightsPayload(targetVersion); + }, + ); + + ipcMain.handle(IPC_CHANNELS.RELEASE_HIGHLIGHTS_ACK, async (): Promise => { + settingsManager.set( + "lastSeenReleaseHighlightsVersion", + app.getVersion().replace(/^v/, ""), + ); + }); + + // Save window size when it changes (skip fullscreen so the saved size + // stays meaningful for windowed launches, e.g. after console mode) + deps.getMainWindow()?.on("resize", () => { + const mainWindow = deps.getMainWindow(); + if (mainWindow && !mainWindow.isDestroyed() && !mainWindow.isFullScreen()) { + const [width, height] = mainWindow.getSize(); + settingsManager.set("windowWidth", width); + settingsManager.set("windowHeight", height); + } + }); +} diff --git a/opennow-stable/src/main/ipc/sessionHandlers.ts b/opennow-stable/src/main/ipc/sessionHandlers.ts index 2218ab3a4..2faf435d6 100644 --- a/opennow-stable/src/main/ipc/sessionHandlers.ts +++ b/opennow-stable/src/main/ipc/sessionHandlers.ts @@ -10,7 +10,7 @@ import type { SessionStopRequest, } from "@shared/gfn"; import { formatErrorChainForLog } from "@shared/networkError"; -import type { AuthService } from "../gfn/auth"; +import type { AuthService } from "../platforms/gfn/auth"; import { claimSession, createSession, @@ -19,8 +19,8 @@ import { reportSessionAd, shouldEnableInGameSettingsPersistence, stopSession, -} from "../gfn/cloudmatch"; -import { SessionError } from "../gfn/errorCodes"; +} from "../platforms/gfn/cloudmatch"; +import { SessionError } from "../platforms/gfn/errorCodes"; import type { SettingsManager } from "../settings"; import { rethrowSerializedSessionError, @@ -58,8 +58,8 @@ export function registerSessionIpcHandlers(deps: SessionIpcHandlerDeps): void { clearActivity, } = deps; - const setLaunchPresence = (session: SessionInfo, gameName: string): void => { - const activity = discordActivityFromSession(session, gameName); + const setLaunchPresence = (session: SessionInfo, gameName: string, gameImageUrl?: string): void => { + const activity = discordActivityFromSession(session, gameName, gameImageUrl); if (!settingsManager.get("discordRichPresence") || !activity) { return; } @@ -187,7 +187,7 @@ export function registerSessionIpcHandlers(deps: SessionIpcHandlerDeps): void { if (!forceNewSession) { const preChecked = await tryClaimExisting(); if (preChecked) { - setLaunchPresence(preChecked, payload.internalTitle || payload.appId); + setLaunchPresence(preChecked, payload.internalTitle || payload.appId, payload.discordGameImageUrl); return preChecked; } } @@ -206,7 +206,7 @@ export function registerSessionIpcHandlers(deps: SessionIpcHandlerDeps): void { token, streamingBaseUrl, }); - setLaunchPresence(sessionResult, payload.internalTitle || payload.appId); + setLaunchPresence(sessionResult, payload.internalTitle || payload.appId, payload.discordGameImageUrl); return sessionResult; } catch (error) { if ( @@ -219,7 +219,7 @@ export function registerSessionIpcHandlers(deps: SessionIpcHandlerDeps): void { ); const fallback = await tryClaimExisting(); if (fallback) { - setLaunchPresence(fallback, payload.internalTitle || payload.appId); + setLaunchPresence(fallback, payload.internalTitle || payload.appId, payload.discordGameImageUrl); return fallback; } } diff --git a/opennow-stable/src/main/nativeStreamer/capabilities.test.ts b/opennow-stable/src/main/nativeStreamer/capabilities.test.ts new file mode 100644 index 000000000..19cea38a7 --- /dev/null +++ b/opennow-stable/src/main/nativeStreamer/capabilities.test.ts @@ -0,0 +1,71 @@ +import assert from "node:assert/strict"; +import test from "node:test"; + +import type { NativeVideoBackendCapability } from "@shared/gfn"; +import { + createNativeStreamerStatus, + resolveActiveVideoBackend, +} from "./capabilities"; + +function backend( + name: string, + platform: string, + available = true, +): NativeVideoBackendCapability { + return { + backend: name, + platform, + available, + codecs: [{ codec: "h264", available: true }], + zeroCopyModes: [], + }; +} + +test("capability selection honors an available explicit preference", () => { + const backends = [ + backend("d3d12", "windows"), + backend("software", "cross-platform"), + ]; + + assert.equal(resolveActiveVideoBackend(backends, "software", "win32")?.backend, "software"); +}); + +test("capability selection uses injected platform before cross-platform fallback", () => { + const backends = [ + backend("d3d12", "windows"), + backend("vaapi", "linux"), + backend("software", "cross-platform"), + ]; + + assert.equal(resolveActiveVideoBackend(backends, "auto", "linux")?.backend, "vaapi"); + assert.equal(resolveActiveVideoBackend(backends, "auto", "darwin")?.backend, "software"); +}); + +test("capability selection skips unavailable preferred and platform backends", () => { + const backends = [ + backend("vaapi", "linux", false), + backend("software", "cross-platform"), + ]; + + assert.equal(resolveActiveVideoBackend(backends, "vaapi", "linux")?.backend, "software"); +}); + +test("status formatting reports selected video path and codec summary", () => { + const status = createNativeStreamerStatus({ + protocolVersion: 4, + backend: "gstreamer", + supportsOfferAnswer: true, + supportsRemoteIce: true, + supportsLocalIce: true, + supportsInput: true, + videoBackends: [backend("vaapi", "linux")], + }, { + source: "system", + bundled: false, + message: "System runtime", + }, "auto", "linux"); + + assert.equal(status.activeVideoBackend?.backend, "vaapi"); + assert.equal(status.codecSummary, "H.264"); + assert.match(status.message, /Video path: VAAPI/); +}); diff --git a/opennow-stable/src/main/nativeStreamer/capabilities.ts b/opennow-stable/src/main/nativeStreamer/capabilities.ts new file mode 100644 index 000000000..b1324b3fe --- /dev/null +++ b/opennow-stable/src/main/nativeStreamer/capabilities.ts @@ -0,0 +1,180 @@ +import type { + NativeGstreamerRuntimeStatus, + NativeStreamerStatus, + NativeVideoBackendCapability, + NativeVideoBackendPreference, +} from "@shared/gfn"; +import type { NativeStreamerCapabilities } from "@shared/nativeStreamer"; +import { linuxInstallInstructions } from "./runtime"; + +function formatVideoBackendName(backend: string | undefined): string { + switch (backend) { + case "d3d12": + return "D3D12"; + case "d3d11": + return "D3D11"; + case "videotoolbox": + return "VideoToolbox"; + case "vaapi": + return "VAAPI"; + case "v4l2": + return "V4L2"; + case "vulkan": + return "Vulkan"; + case "software": + return "Software"; + default: + return backend ?? "Unknown"; + } +} + +function formatVideoCodec(codec: string): string { + switch (codec.toLowerCase()) { + case "h264": + return "H.264"; + case "h265": + return "H.265"; + case "av1": + return "AV1"; + default: + return codec.toUpperCase(); + } +} + +function capabilityPlatform(platform: NodeJS.Platform): NativeVideoBackendCapability["platform"] | "other" { + if (platform === "win32") return "windows"; + if (platform === "darwin") return "macos"; + if (platform === "linux") return "linux"; + return "other"; +} + +export function resolveActiveVideoBackend( + videoBackends: NativeVideoBackendCapability[], + preferredBackend: NativeVideoBackendPreference = "auto", + platform = process.platform, +): NativeVideoBackendCapability | undefined { + if (preferredBackend !== "auto") { + const preferred = videoBackends.find((candidate) => + candidate.available && candidate.backend === preferredBackend, + ); + if (preferred) return preferred; + } + + const currentPlatform = capabilityPlatform(platform); + return videoBackends.find((candidate) => candidate.available && candidate.platform === currentPlatform) + ?? videoBackends.find((candidate) => candidate.available && candidate.platform === "cross-platform") + ?? videoBackends.find((candidate) => candidate.available); +} + +function summarizeCodecs(backend: NativeVideoBackendCapability | undefined): string { + const codecs = backend?.codecs + .filter((codec) => codec.available) + .map((codec) => formatVideoCodec(codec.codec)) ?? []; + return codecs.length > 0 ? codecs.join(", ") : "No hardware codec path"; +} + +function summarizeZeroCopy(backend: NativeVideoBackendCapability | undefined): string { + if (!backend) { + return "Not available"; + } + return backend.zeroCopyModes.length > 0 + ? `Hardware memory: ${backend.zeroCopyModes.join(", ")}` + : "System memory"; +} + +export function formatError(error: unknown): string { + return error instanceof Error ? error.message : String(error); +} + +function isWindowsDllLoadFailure(error: unknown, platform: NodeJS.Platform): boolean { + const message = formatError(error); + return platform === "win32" + && (message.includes("3221225781") || message.toLowerCase().includes("0xc0000135")); +} + +function formatNativeStreamerDetectionFailure( + error: unknown, + runtime: NativeGstreamerRuntimeStatus | null, + platform: NodeJS.Platform, +): string { + if (isWindowsDllLoadFailure(error, platform)) { + return runtime?.bundled + ? `Native streamer could not load a required DLL even though bundled GStreamer was detected at ${runtime.path}. The packaged runtime may be incomplete or blocked. ${formatError(error)}` + : `Native streamer could not load a required DLL and no bundled GStreamer runtime was detected. ${formatError(error)}`; + } + return `Native streamer was not detected: ${formatError(error)}`; +} + +export function createNativeStreamerStatus( + capabilities: NativeStreamerCapabilities | null, + runtimeStatus: NativeGstreamerRuntimeStatus | null, + preferredBackend: NativeVideoBackendPreference, + platform = process.platform, +): NativeStreamerStatus { + const backend = capabilities?.backend; + const gstreamerAvailable = backend === "gstreamer" && capabilities?.supportsOfferAnswer === true; + const videoBackends = capabilities?.videoBackends ?? []; + const activeVideoBackend = resolveActiveVideoBackend(videoBackends, preferredBackend, platform); + const runtime = runtimeStatus ?? { + source: "unknown", + bundled: false, + message: "GStreamer runtime has not been checked yet.", + installInstructions: linuxInstallInstructions(platform), + } satisfies NativeGstreamerRuntimeStatus; + const effectiveRuntime: NativeGstreamerRuntimeStatus = gstreamerAvailable + ? runtime.bundled + ? runtime + : { + ...runtime, + source: "system", + message: "Using system GStreamer runtime; packaged Windows/macOS builds should use the bundled runtime.", + } + : { + ...runtime, + source: runtime.bundled ? "bundled" : platform === "linux" ? "missing" : runtime.source, + message: runtime.bundled + ? "Bundled GStreamer runtime was found, but the GStreamer backend is not ready." + : platform === "linux" + ? "GStreamer is not ready. Install distro GStreamer packages so plugins match the host GPU/driver stack." + : runtime.message, + installInstructions: runtime.installInstructions ?? linuxInstallInstructions(platform), + }; + + return { + detected: true, + gstreamerAvailable, + supportsOfferAnswer: capabilities?.supportsOfferAnswer === true, + backend, + fallbackReason: capabilities?.fallbackReason, + videoBackends, + activeVideoBackend, + codecSummary: summarizeCodecs(activeVideoBackend), + zeroCopySummary: summarizeZeroCopy(activeVideoBackend), + gstreamerRuntime: effectiveRuntime, + message: gstreamerAvailable + ? `${effectiveRuntime.message} Video path: ${formatVideoBackendName(activeVideoBackend?.backend)}.` + : capabilities?.fallbackReason ?? effectiveRuntime.message, + }; +} + +export function createNativeStreamerDetectionFailureStatus( + error: unknown, + runtimeStatus: NativeGstreamerRuntimeStatus | null, + platform = process.platform, +): NativeStreamerStatus { + const runtime = runtimeStatus ?? { + source: platform === "linux" ? "missing" : "unknown", + bundled: false, + message: platform === "linux" + ? "GStreamer is not ready. Linux uses distro packages because private AppImage GStreamer bundling is unreliable across glibc, libdrm/VAAPI/Vulkan, and GPU driver stacks." + : "GStreamer runtime could not be checked because the native streamer did not start.", + installInstructions: linuxInstallInstructions(platform), + } satisfies NativeGstreamerRuntimeStatus; + return { + detected: false, + gstreamerAvailable: false, + supportsOfferAnswer: false, + gstreamerRuntime: runtime, + message: formatNativeStreamerDetectionFailure(error, runtime, platform), + }; +} diff --git a/opennow-stable/src/main/nativeStreamer/executableDiscovery.ts b/opennow-stable/src/main/nativeStreamer/executableDiscovery.ts new file mode 100644 index 000000000..32987f8b5 --- /dev/null +++ b/opennow-stable/src/main/nativeStreamer/executableDiscovery.ts @@ -0,0 +1,132 @@ +import { join, resolve } from "node:path"; + +import { + hasBundledRuntimeNextToExecutable, + isExistingFile, + isPathInside, + nativeStreamerExecutableName, + nativeStreamerPlatformKey, +} from "./runtime"; +import { + materializePackagedNativeStreamerCache, + type PackagedNativeStreamerCacheContext, +} from "./runtimeCache"; + +export interface NativeStreamerExecutableDiscoveryOptions { + platform: NodeJS.Platform; + arch: string; + resourcesPath: string; + appPath: string; + mainDir: string; + isPackaged: boolean; + envExecutablePath: string | undefined; + getConfiguredPath(): string; + cacheContext: PackagedNativeStreamerCacheContext; +} + +export function shouldIgnorePackagedExecutableOverride( + configuredPath: string, + options: Pick< + NativeStreamerExecutableDiscoveryOptions, + "resourcesPath" | "appPath" | "mainDir" | "platform" + >, +): boolean { + if (hasBundledRuntimeNextToExecutable(configuredPath)) { + return false; + } + + const packagedRoots = [ + join(options.resourcesPath, "native", "opennow-streamer"), + resolve(options.appPath, "../native/opennow-streamer"), + resolve(options.mainDir, "../../../dist-release/win-unpacked/resources/native/opennow-streamer"), + resolve(options.mainDir, "../../../dist-release/win-unpacked/resources/app.asar.unpacked/native/opennow-streamer"), + ]; + + return packagedRoots.some((root) => isPathInside(root, configuredPath, options.platform)); +} + +export function resolveNativeStreamerExecutableCandidates( + options: NativeStreamerExecutableDiscoveryOptions, +): string[] { + const exeName = nativeStreamerExecutableName(options.platform); + const platformKey = nativeStreamerPlatformKey(options.platform, options.arch); + const bundledCandidates = [ + join(options.resourcesPath, "native", "opennow-streamer", platformKey, exeName), + join(options.resourcesPath, "native", "opennow-streamer", exeName), + ]; + const candidates: string[] = []; + const addCandidate = (candidate: string | undefined): void => { + if (!candidate || !isExistingFile(candidate) || candidates.includes(candidate)) { + return; + } + candidates.push(candidate); + }; + + if (options.isPackaged) { + for (const candidate of bundledCandidates) { + if (!isExistingFile(candidate) || !hasBundledRuntimeNextToExecutable(candidate)) { + continue; + } + addCandidate( + materializePackagedNativeStreamerCache( + candidate, + platformKey, + exeName, + options.cacheContext, + ) ?? undefined, + ); + } + } + bundledCandidates.forEach(addCandidate); + if (options.isPackaged && candidates.length > 0) { + const packagedBundledCandidates = candidates.filter((candidate) => + hasBundledRuntimeNextToExecutable(candidate), + ); + return packagedBundledCandidates.length > 0 ? packagedBundledCandidates : candidates; + } + + const configuredPath = options.getConfiguredPath().trim(); + if (configuredPath) { + if (isExistingFile(configuredPath)) { + if (!shouldIgnorePackagedExecutableOverride(configuredPath, options)) { + addCandidate(configuredPath); + } else { + console.warn( + "[NativeStreamer] Ignoring packaged executable override without bundled runtime:", + configuredPath, + ); + } + } else { + throw new Error(`Configured native streamer executable was not found: ${configuredPath}`); + } + } + + [ + options.envExecutablePath, + ...bundledCandidates, + resolve(options.mainDir, "../../../native/opennow-streamer/bin", platformKey, exeName), + resolve(options.mainDir, "../../../native/opennow-streamer/bin", exeName), + resolve(options.mainDir, "../../../native/opennow-streamer/dist", platformKey, exeName), + resolve(options.mainDir, "../../../native/opennow-streamer/dist", exeName), + resolve(options.mainDir, "../../../native/opennow-streamer/target/release", platformKey, exeName), + resolve(options.mainDir, "../../../native/opennow-streamer/target/release", exeName), + resolve(options.mainDir, "../../../native/opennow-streamer/target/debug", platformKey, exeName), + resolve(options.mainDir, "../../../native/opennow-streamer/target/debug", exeName), + resolve(options.appPath, "../native/opennow-streamer/bin", platformKey, exeName), + resolve(options.appPath, "../native/opennow-streamer/bin", exeName), + resolve(options.appPath, "../native/opennow-streamer/dist", platformKey, exeName), + resolve(options.appPath, "../native/opennow-streamer/dist", exeName), + resolve(options.appPath, "../native/opennow-streamer/target/release", platformKey, exeName), + resolve(options.appPath, "../native/opennow-streamer/target/release", exeName), + resolve(options.appPath, "../native/opennow-streamer/target/debug", platformKey, exeName), + resolve(options.appPath, "../native/opennow-streamer/target/debug", exeName), + ] + .filter((candidate): candidate is string => Boolean(candidate)) + .forEach(addCandidate); + + if (candidates.length > 0) { + return candidates; + } + + throw new Error(`Native streamer binary not found. Checked: ${candidates.join(", ")}`); +} diff --git a/opennow-stable/src/main/nativeStreamer/manager.ts b/opennow-stable/src/main/nativeStreamer/manager.ts index 1892d4585..215738652 100644 --- a/opennow-stable/src/main/nativeStreamer/manager.ts +++ b/opennow-stable/src/main/nativeStreamer/manager.ts @@ -1,25 +1,12 @@ import { app } from "electron"; -import { createHash, randomUUID } from "node:crypto"; +import { randomUUID } from "node:crypto"; import { tmpdir } from "node:os"; -import { - cpSync, - existsSync, - mkdirSync, - readFileSync, - realpathSync, - renameSync, - rmSync, - statSync, - writeFileSync, -} from "node:fs"; -import { dirname, resolve, join, delimiter, sep } from "node:path"; import { spawn, type ChildProcessWithoutNullStreams } from "node:child_process"; import { createUnsupportedNativeStreamerStatus, isNativeStreamerSupportedPlatform, NATIVE_STREAMER_WINDOWS_ONLY_MESSAGE, - nativeStreamerFeatureModeToEnvValue, type IceCandidatePayload, type KeyframeRequest, type MainToRendererSignalingEvent, @@ -28,10 +15,8 @@ import { type NativeVideoBackendPreference, type NativeStreamerStatus, type NativeGstreamerRuntimeStatus, - type NativeGstreamerInstallInstruction, type NativeRenderSurface, type NativeStreamerSessionContext, - type NativeVideoBackendCapability, type SendAnswerRequest, } from "@shared/gfn"; import { @@ -44,12 +29,19 @@ import { type NativeStreamerResponse, } from "@shared/nativeStreamer"; import type { NativeStreamerShortcutBindings } from "@shared/gfn"; - -type NativeStreamerCommandInput = NativeStreamerCommand extends infer T - ? T extends NativeStreamerCommand - ? Omit - : never - : never; +import { + createNativeStreamerDetectionFailureStatus, + createNativeStreamerStatus, + formatError, +} from "./capabilities"; +import { resolveNativeStreamerExecutableCandidates } from "./executableDiscovery"; +import { + isNativeStreamerEvent, + isNativeStreamerResponse, + type NativeStreamerCommandInput, +} from "./protocol"; +import { createNativeStreamerRuntimeEnvironment } from "./runtime"; +import { NativeSurfaceUpdateQueue } from "./surfaceUpdateQueue"; interface NativeStreamerCallbacks { sendAnswer(payload: SendAnswerRequest): Promise; @@ -85,182 +77,6 @@ const MAX_INPUT_STDIN_BUFFER_BYTES = 64 * 1024; const MIN_NATIVE_BITRATE_KBPS = 5_000; const MAX_NATIVE_BITRATE_KBPS = 150_000; -function nativeStreamerExecutableName(): string { - return process.platform === "win32" ? "opennow-streamer.exe" : "opennow-streamer"; -} - -function nativeStreamerPlatformKey(): string { - return `${process.platform}-${process.arch}`; -} - -function isExistingFile(path: string): boolean { - try { - return existsSync(path) && statSync(path).isFile(); - } catch { - return false; - } -} - -function isExistingDirectory(path: string): boolean { - try { - return existsSync(path) && statSync(path).isDirectory(); - } catch { - return false; - } -} - -function normalizePathForComparison(path: string): string { - let resolvedPath = resolve(path); - try { - resolvedPath = realpathSync.native(resolvedPath); - } catch { - // The caller may compare a path that does not exist yet, such as a cache - // destination. Falling back to resolve still keeps comparisons stable. - } - return process.platform === "win32" ? resolvedPath.toLowerCase() : resolvedPath; -} - -function isPathInside(parent: string, child: string): boolean { - const normalizedParent = normalizePathForComparison(parent); - const normalizedChild = normalizePathForComparison(child); - return normalizedChild === normalizedParent || normalizedChild.startsWith(`${normalizedParent}${sep}`); -} - -function hasBundledRuntimeNextToExecutable(executablePath: string): boolean { - return isExistingDirectory(join(dirname(executablePath), "gstreamer")); -} - -function safePathSegment(value: string): string { - return value.replace(/[^a-zA-Z0-9._-]/g, "_") || "unknown"; -} - -function fileSha256(path: string): string { - return createHash("sha256").update(readFileSync(path)).digest("hex"); -} - -function prependEnvPath(env: NodeJS.ProcessEnv, key: string, directory: string): void { - env[key] = env[key] ? `${directory}${delimiter}${env[key]}` : directory; -} - -function prependProcessPath(env: NodeJS.ProcessEnv, directory: string): void { - const pathKey = Object.keys(env).find((key) => key.toLowerCase() === "path") || "PATH"; - prependEnvPath(env, pathKey, directory); -} - -const LINUX_GSTREAMER_INSTALL_INSTRUCTIONS: NativeGstreamerInstallInstruction[] = [ - { - distro: "Debian / Ubuntu / Mint / Pop!_OS / KDE neon", - command: "sudo apt update && sudo apt install libgstreamer1.0-0 libgstreamer-plugins-base1.0-0 gstreamer1.0-tools gstreamer1.0-libav gstreamer1.0-plugins-base gstreamer1.0-plugins-good gstreamer1.0-plugins-bad gstreamer1.0-plugins-ugly gstreamer1.0-gl gstreamer1.0-vaapi gstreamer1.0-x gstreamer1.0-alsa libva2 libva-drm2 libvulkan1 mesa-vulkan-drivers", - }, - { - distro: "Fedora / RHEL / Nobara / Bazzite", - command: "sudo dnf install gstreamer1 gstreamer1-plugins-base gstreamer1-plugins-good gstreamer1-plugins-bad-free gstreamer1-plugins-bad-freeworld gstreamer1-plugins-ugly gstreamer1-libav gstreamer1-vaapi gstreamer1-plugin-openh264 mesa-vulkan-drivers libva", - note: "RPM Fusion may be required for libav, ugly, or bad-freeworld packages.", - }, - { - distro: "Arch / Manjaro / EndeavourOS / SteamOS", - command: "sudo pacman -S --needed gstreamer gst-plugins-base gst-plugins-good gst-plugins-bad gst-plugins-ugly gst-libav gst-plugin-va libva mesa vulkan-radeon", - note: "NVIDIA users should use their distro NVIDIA/Vulkan driver packages instead of vulkan-radeon.", - }, - { - distro: "openSUSE Tumbleweed / Leap", - command: "sudo zypper install gstreamer gstreamer-plugins-base gstreamer-plugins-good gstreamer-plugins-bad gstreamer-plugins-ugly gstreamer-plugins-libav gstreamer-plugins-vaapi libva2 Mesa-vulkan-device-select", - }, -]; - -function linuxInstallInstructions(): NativeGstreamerInstallInstruction[] | undefined { - return process.platform === "linux" ? LINUX_GSTREAMER_INSTALL_INSTRUCTIONS : undefined; -} - -function configureBundledGstreamerRuntime( - env: NodeJS.ProcessEnv, - executablePath: string, -): NativeGstreamerRuntimeStatus { - const runtimeRoot = join(dirname(executablePath), "gstreamer"); - if (!isExistingDirectory(runtimeRoot)) { - return { - source: "system", - bundled: false, - message: process.platform === "linux" - ? "No bundled GStreamer runtime was found. Linux uses distro GStreamer packages so VAAPI/V4L2/Vulkan plugins match the host driver stack." - : "No bundled GStreamer runtime was found; using the system runtime if available.", - installInstructions: linuxInstallInstructions(), - }; - } - - const binDir = join(runtimeRoot, "bin"); - const libDir = join(runtimeRoot, "lib"); - const pluginDir = join(runtimeRoot, "lib", "gstreamer-1.0"); - const scanner = join( - runtimeRoot, - "libexec", - "gstreamer-1.0", - process.platform === "win32" ? "gst-plugin-scanner.exe" : "gst-plugin-scanner", - ); - const gioModulesDir = join(runtimeRoot, "lib", "gio", "modules"); - - if (process.platform === "win32") prependProcessPath(env, dirname(executablePath)); - if (isExistingDirectory(binDir)) prependProcessPath(env, binDir); - if (isExistingDirectory(pluginDir)) { - env.GST_PLUGIN_PATH = pluginDir; - env.GST_PLUGIN_PATH_1_0 = pluginDir; - env.GST_PLUGIN_SYSTEM_PATH = pluginDir; - env.GST_PLUGIN_SYSTEM_PATH_1_0 = pluginDir; - } - if (isExistingFile(scanner)) { - env.GST_PLUGIN_SCANNER = scanner; - env.GST_PLUGIN_SCANNER_1_0 = scanner; - } - env.GST_REGISTRY_REUSE_PLUGIN_SCANNER = "no"; - if (isExistingDirectory(gioModulesDir)) { - env.GIO_MODULE_DIR = gioModulesDir; - env.GIO_EXTRA_MODULES = gioModulesDir; - } - const registryDir = join(app.getPath("userData"), "native-streamer", "gstreamer"); - const registryPath = join(registryDir, `${nativeStreamerPlatformKey()}-registry.bin`); - mkdirSync(registryDir, { recursive: true }); - env.GST_REGISTRY = registryPath; - if (process.platform === "linux") { - if (isExistingDirectory(libDir)) prependEnvPath(env, "LD_LIBRARY_PATH", libDir); - if (isExistingDirectory(binDir)) prependEnvPath(env, "LD_LIBRARY_PATH", binDir); - } - if (process.platform === "darwin") { - if (isExistingDirectory(libDir)) { - prependEnvPath(env, "DYLD_LIBRARY_PATH", libDir); - prependEnvPath(env, "DYLD_FALLBACK_LIBRARY_PATH", libDir); - } - if (isExistingDirectory(binDir)) { - prependEnvPath(env, "DYLD_LIBRARY_PATH", binDir); - prependEnvPath(env, "DYLD_FALLBACK_LIBRARY_PATH", binDir); - } - } - - return { - source: "bundled", - bundled: true, - path: runtimeRoot, - message: "Using bundled GStreamer runtime next to the native streamer executable.", - }; -} - -function isWindowsDllLoadFailure(error: unknown): boolean { - const message = formatError(error); - return process.platform === "win32" && (message.includes("3221225781") || message.toLowerCase().includes("0xc0000135")); -} - -function formatNativeStreamerDetectionFailure(error: unknown, runtime: NativeGstreamerRuntimeStatus | null): string { - if (isWindowsDllLoadFailure(error)) { - return runtime?.bundled - ? `Native streamer could not load a required DLL even though bundled GStreamer was detected at ${runtime.path}. The packaged runtime may be incomplete or blocked. ${formatError(error)}` - : `Native streamer could not load a required DLL and no bundled GStreamer runtime was detected. ${formatError(error)}`; - } - return `Native streamer was not detected: ${formatError(error)}`; -} - -function formatError(error: unknown): string { - return error instanceof Error ? error.message : String(error); -} - function normalizeBitrateKbps(value: number): number { if (!Number.isFinite(value)) { return MIN_NATIVE_BITRATE_KBPS; @@ -272,217 +88,6 @@ function normalizeBitrateKbps(value: number): number { ); } -function formatVideoBackendName(backend: string | undefined): string { - switch (backend) { - case "d3d12": - return "D3D12"; - case "d3d11": - return "D3D11"; - case "videotoolbox": - return "VideoToolbox"; - case "vaapi": - return "VAAPI"; - case "v4l2": - return "V4L2"; - case "vulkan": - return "Vulkan"; - case "software": - return "Software"; - default: - return backend ?? "Unknown"; - } -} - -function formatVideoCodec(codec: string): string { - switch (codec.toLowerCase()) { - case "h264": - return "H.264"; - case "h265": - return "H.265"; - case "av1": - return "AV1"; - default: - return codec.toUpperCase(); - } -} - -function resolveActiveVideoBackend( - videoBackends: NativeVideoBackendCapability[], - preferredBackend: NativeVideoBackendPreference = "auto", -): NativeVideoBackendCapability | undefined { - const currentPlatform = process.platform === "win32" - ? "windows" - : process.platform === "darwin" - ? "macos" - : process.platform === "linux" - ? "linux" - : "other"; - - if (preferredBackend !== "auto") { - const preferred = videoBackends.find((candidate) => candidate.available && candidate.backend === preferredBackend); - if (preferred) return preferred; - } - - return videoBackends.find((candidate) => candidate.available && candidate.platform === currentPlatform) - ?? videoBackends.find((candidate) => candidate.available && candidate.platform === "cross-platform") - ?? videoBackends.find((candidate) => candidate.available); -} - -function summarizeCodecs(backend: NativeVideoBackendCapability | undefined): string { - const codecs = backend?.codecs - .filter((codec) => codec.available) - .map((codec) => formatVideoCodec(codec.codec)) ?? []; - return codecs.length > 0 ? codecs.join(", ") : "No hardware codec path"; -} - -function summarizeZeroCopy(backend: NativeVideoBackendCapability | undefined): string { - if (!backend) { - return "Not available"; - } - return backend.zeroCopyModes.length > 0 - ? `Hardware memory: ${backend.zeroCopyModes.join(", ")}` - : "System memory"; -} - -function isRecord(value: unknown): value is Record { - return typeof value === "object" && value !== null; -} - -function isResponse(message: NativeStreamerMessage): message is NativeStreamerResponse { - return isRecord(message) && typeof (message as Record)["id"] === "string"; -} - -function isEvent(message: NativeStreamerMessage): message is NativeStreamerEvent { - return isRecord(message) && typeof (message as Record)["id"] !== "string"; -} - -interface PackagedNativeStreamerCacheMarker { - appVersion: string; - platformKey: string; - exeName: string; - exeSha256: string; - bundledRuntime: boolean; - runtimeManifestSha256?: string; -} - -function shouldUseStablePackagedNativeStreamerCache(): boolean { - return app.isPackaged - && process.platform === "win32" - && isPathInside(tmpdir(), process.resourcesPath); -} - -function buildPackagedNativeStreamerCacheMarker( - sourceDirectory: string, - exeName: string, - platformKey: string, -): PackagedNativeStreamerCacheMarker { - const runtimeManifest = join(sourceDirectory, "gstreamer", "OPENNOW-GSTREAMER-RUNTIME.txt"); - return { - appVersion: app.getVersion(), - platformKey, - exeName, - exeSha256: fileSha256(join(sourceDirectory, exeName)), - bundledRuntime: isExistingDirectory(join(sourceDirectory, "gstreamer")), - runtimeManifestSha256: isExistingFile(runtimeManifest) ? fileSha256(runtimeManifest) : undefined, - }; -} - -function readCacheMarker(markerPath: string): PackagedNativeStreamerCacheMarker | null { - try { - return JSON.parse(readFileSync(markerPath, "utf8")) as PackagedNativeStreamerCacheMarker; - } catch { - return null; - } -} - -function isSameCacheMarker( - left: PackagedNativeStreamerCacheMarker | null, - right: PackagedNativeStreamerCacheMarker, -): boolean { - if (!left) { - return false; - } - - return left.appVersion === right.appVersion - && left.platformKey === right.platformKey - && left.exeName === right.exeName - && left.exeSha256 === right.exeSha256 - && left.bundledRuntime === right.bundledRuntime - && left.runtimeManifestSha256 === right.runtimeManifestSha256; -} - -function materializePackagedNativeStreamerCache( - sourceExecutablePath: string, - platformKey: string, - exeName: string, -): string | null { - if (!shouldUseStablePackagedNativeStreamerCache()) { - return null; - } - - const sourceDirectory = dirname(sourceExecutablePath); - const cacheDirectory = join( - app.getPath("userData"), - "native-streamer", - "runtime", - safePathSegment(app.getVersion()), - safePathSegment(platformKey), - ); - const cachedExecutablePath = join(cacheDirectory, exeName); - const markerPath = join(cacheDirectory, ".opennow-native-runtime.json"); - let stagingDirectory: string | null = null; - - try { - const expectedMarker = buildPackagedNativeStreamerCacheMarker(sourceDirectory, exeName, platformKey); - const cachedMarker = readCacheMarker(markerPath); - if ( - isExistingFile(cachedExecutablePath) - && isSameCacheMarker(cachedMarker, expectedMarker) - && (!expectedMarker.bundledRuntime || hasBundledRuntimeNextToExecutable(cachedExecutablePath)) - ) { - return cachedExecutablePath; - } - - stagingDirectory = `${cacheDirectory}.tmp-${process.pid}-${Date.now()}`; - rmSync(stagingDirectory, { recursive: true, force: true }); - mkdirSync(dirname(stagingDirectory), { recursive: true }); - cpSync(sourceDirectory, stagingDirectory, { - recursive: true, - force: true, - dereference: true, - filter: (entry) => { - const lower = entry.toLowerCase(); - return !lower.endsWith(".pdb") && !lower.endsWith(".lib") && !lower.endsWith(".a"); - }, - }); - writeFileSync( - join(stagingDirectory, ".opennow-native-runtime.json"), - `${JSON.stringify(expectedMarker, null, 2)}\n`, - "utf8", - ); - - if (!isExistingFile(join(stagingDirectory, exeName))) { - throw new Error(`Cached native streamer executable was not created: ${join(stagingDirectory, exeName)}`); - } - if (expectedMarker.bundledRuntime && !hasBundledRuntimeNextToExecutable(join(stagingDirectory, exeName))) { - throw new Error("Cached native streamer runtime is missing its bundled GStreamer directory."); - } - - rmSync(cacheDirectory, { recursive: true, force: true }); - renameSync(stagingDirectory, cacheDirectory); - stagingDirectory = null; - console.log("[NativeStreamer] Cached packaged native streamer in stable runtime path:", cachedExecutablePath); - return cachedExecutablePath; - } catch (error) { - console.warn("[NativeStreamer] Failed to prepare stable packaged runtime cache; using packaged resource path:", error); - return null; - } finally { - if (stagingDirectory) { - rmSync(stagingDirectory, { recursive: true, force: true }); - } - } -} - export class NativeStreamerManager { private child: ChildProcessWithoutNullStreams | null = null; private startupPromise: Promise | null = null; @@ -497,11 +102,14 @@ export class NativeStreamerManager { private queuedLocalIce: IceCandidatePayload[] = []; private queuedRemoteIceSessionId: string | null = null; private queuedRemoteIce: IceCandidatePayload[] = []; - private lastSurface: NativeRenderSurface | null = null; - private surfaceUpdateInFlight = false; - private surfaceUpdateQueued = false; + private readonly surfaceUpdates: NativeSurfaceUpdateQueue; - constructor(private readonly options: NativeStreamerManagerOptions) {} + constructor(private readonly options: NativeStreamerManagerOptions) { + this.surfaceUpdates = new NativeSurfaceUpdateQueue( + (surface) => this.request({ type: "surface", surface }, SURFACE_UPDATE_TIMEOUT_MS).then(() => undefined), + (error) => console.warn("[NativeStreamer] Failed to update native render surface:", error), + ); + } isRunning(): boolean { return this.child !== null; @@ -598,70 +206,18 @@ export class NativeStreamerManager { try { await this.ensureProcess(); - const backend = this.capabilities?.backend; - const gstreamerAvailable = backend === "gstreamer" && this.capabilities?.supportsOfferAnswer === true; - const videoBackends = this.capabilities?.videoBackends ?? []; - const activeVideoBackend = resolveActiveVideoBackend( - videoBackends, + return createNativeStreamerStatus( + this.capabilities, + this.gstreamerRuntime, this.options.getVideoBackendPreference(), + process.platform, ); - const codecSummary = summarizeCodecs(activeVideoBackend); - const zeroCopySummary = summarizeZeroCopy(activeVideoBackend); - const runtime = this.gstreamerRuntime ?? { - source: "unknown", - bundled: false, - message: "GStreamer runtime has not been checked yet.", - installInstructions: linuxInstallInstructions(), - } satisfies NativeGstreamerRuntimeStatus; - const effectiveRuntime: NativeGstreamerRuntimeStatus = gstreamerAvailable - ? runtime.bundled - ? runtime - : { - ...runtime, - source: "system", - message: "Using system GStreamer runtime; packaged Windows/macOS builds should use the bundled runtime.", - } - : { - ...runtime, - source: runtime.bundled ? "bundled" : process.platform === "linux" ? "missing" : runtime.source, - message: runtime.bundled - ? "Bundled GStreamer runtime was found, but the GStreamer backend is not ready." - : process.platform === "linux" - ? "GStreamer is not ready. Install distro GStreamer packages so plugins match the host GPU/driver stack." - : runtime.message, - installInstructions: runtime.installInstructions ?? linuxInstallInstructions(), - }; - return { - detected: true, - gstreamerAvailable, - supportsOfferAnswer: this.capabilities?.supportsOfferAnswer === true, - backend, - fallbackReason: this.capabilities?.fallbackReason, - videoBackends, - activeVideoBackend, - codecSummary, - zeroCopySummary, - gstreamerRuntime: effectiveRuntime, - message: gstreamerAvailable - ? `${effectiveRuntime.message} Video path: ${formatVideoBackendName(activeVideoBackend?.backend)}.` - : this.capabilities?.fallbackReason ?? effectiveRuntime.message, - }; } catch (error) { - const runtime = this.gstreamerRuntime ?? { - source: process.platform === "linux" ? "missing" : "unknown", - bundled: false, - message: process.platform === "linux" - ? "GStreamer is not ready. Linux uses distro packages because private AppImage GStreamer bundling is unreliable across glibc, libdrm/VAAPI/Vulkan, and GPU driver stacks." - : "GStreamer runtime could not be checked because the native streamer did not start.", - installInstructions: linuxInstallInstructions(), - } satisfies NativeGstreamerRuntimeStatus; - return { - detected: false, - gstreamerAvailable: false, - supportsOfferAnswer: false, - gstreamerRuntime: runtime, - message: formatNativeStreamerDetectionFailure(error, runtime), - }; + return createNativeStreamerDetectionFailureStatus( + error, + this.gstreamerRuntime, + process.platform, + ); } } @@ -731,8 +287,7 @@ export class NativeStreamerManager { } updateSurface(surface: NativeRenderSurface): void { - this.lastSurface = surface; - void this.flushSurfaceUpdate(); + this.surfaceUpdates.update(surface); } updateBitrateLimit(maxBitrateKbps: number): void { @@ -778,6 +333,7 @@ export class NativeStreamerManager { const child = this.child; this.activeSessionId = null; this.capabilities = null; + this.surfaceUpdates.markNotReady(); this.clearQueuedRemoteIce(); if (!child) { @@ -796,6 +352,7 @@ export class NativeStreamerManager { dispose(reason = "disposed"): void { this.activeSessionId = null; this.capabilities = null; + this.surfaceUpdates.markNotReady(); this.clearQueuedRemoteIce(); this.rejectPending(new Error(`Native streamer ${reason}.`)); this.terminateProcess(); @@ -827,7 +384,24 @@ export class NativeStreamerManager { const backendPreference = this.options.getBackendPreference(); let lastError: Error | null = null; - for (const executablePath of this.resolveExecutableCandidates()) { + for (const executablePath of resolveNativeStreamerExecutableCandidates({ + platform: process.platform, + arch: process.arch, + resourcesPath: process.resourcesPath, + appPath: app.getAppPath(), + mainDir: this.options.mainDir, + isPackaged: app.isPackaged, + envExecutablePath: process.env.OPENNOW_NATIVE_STREAMER, + getConfiguredPath: () => this.options.getExecutablePathOverride(), + cacheContext: { + appVersion: app.getVersion(), + isPackaged: app.isPackaged, + platform: process.platform, + resourcesPath: process.resourcesPath, + tempDirectory: tmpdir(), + userDataPath: app.getPath("userData"), + }, + })) { try { await this.startProcess(executablePath, backendPreference); return; @@ -866,24 +440,21 @@ export class NativeStreamerManager { const videoBackendPreference = this.options.getVideoBackendPreference(); console.log("[NativeStreamer] Video backend preference:", videoBackendPreference); - const childEnv: NodeJS.ProcessEnv = { - ...process.env, - OPENNOW_NATIVE_STREAMER_PROTOCOL: String(NATIVE_STREAMER_PROTOCOL_VERSION), - }; - delete childEnv.OPENNOW_NATIVE_VIDEO_API; - delete childEnv.OPENNOW_NATIVE_VIDEO_BACKEND; - if (videoBackendPreference !== "auto") { - childEnv.OPENNOW_NATIVE_VIDEO_BACKEND = videoBackendPreference; - } - if (process.platform === "win32") { - childEnv.OPENNOW_NATIVE_EXTERNAL_RENDERER = this.options.getExternalRendererEnabled() ? "1" : "0"; - } - childEnv.OPENNOW_NATIVE_CLOUD_GSYNC = nativeStreamerFeatureModeToEnvValue(this.options.getCloudGsyncMode()); - childEnv.OPENNOW_NATIVE_D3D_FULLSCREEN = nativeStreamerFeatureModeToEnvValue(this.options.getD3dFullscreenMode()); - if (backendPreference !== "auto") { - childEnv.OPENNOW_NATIVE_STREAMER_BACKEND = backendPreference; - } - const runtimeStatus = configureBundledGstreamerRuntime(childEnv, executablePath); + const { env: childEnv, runtimeStatus } = createNativeStreamerRuntimeEnvironment({ + executablePath, + baseEnv: process.env, + platform: process.platform, + arch: process.arch, + userDataPath: app.getPath("userData"), + protocolVersion: NATIVE_STREAMER_PROTOCOL_VERSION, + backendPreference, + videoBackendPreference, + externalRendererEnabled: process.platform === "win32" + ? this.options.getExternalRendererEnabled() + : false, + cloudGsyncMode: this.options.getCloudGsyncMode(), + d3dFullscreenMode: this.options.getD3dFullscreenMode(), + }); this.gstreamerRuntime = runtimeStatus; if (runtimeStatus.bundled) { console.log("[NativeStreamer] Using bundled GStreamer runtime:", runtimeStatus.path); @@ -944,7 +515,7 @@ export class NativeStreamerManager { ); } this.assertBackendPreference(response.capabilities, backendPreference); - await this.flushSurfaceUpdate(); + await this.surfaceUpdates.markReady(); } private assertBackendPreference( @@ -961,98 +532,6 @@ export class NativeStreamerManager { ); } - private resolveExecutableCandidates(): string[] { - const exeName = nativeStreamerExecutableName(); - const platformKey = nativeStreamerPlatformKey(); - const bundledCandidates = [ - join(process.resourcesPath, "native", "opennow-streamer", platformKey, exeName), - join(process.resourcesPath, "native", "opennow-streamer", exeName), - ]; - const candidates: string[] = []; - const addCandidate = (candidate: string | undefined): void => { - if (!candidate || !isExistingFile(candidate) || candidates.includes(candidate)) { - return; - } - candidates.push(candidate); - }; - - if (app.isPackaged) { - for (const candidate of bundledCandidates) { - if (!isExistingFile(candidate) || !hasBundledRuntimeNextToExecutable(candidate)) { - continue; - } - addCandidate(materializePackagedNativeStreamerCache(candidate, platformKey, exeName) ?? undefined); - } - } - bundledCandidates.forEach(addCandidate); - if (app.isPackaged && candidates.length > 0) { - const packagedBundledCandidates = candidates.filter((candidate) => - hasBundledRuntimeNextToExecutable(candidate), - ); - return packagedBundledCandidates.length > 0 ? packagedBundledCandidates : candidates; - } - - const configuredPath = this.options.getExecutablePathOverride().trim(); - if (configuredPath) { - if (isExistingFile(configuredPath)) { - if (!this.shouldIgnorePackagedExecutableOverride(configuredPath)) { - addCandidate(configuredPath); - } else { - console.warn( - "[NativeStreamer] Ignoring packaged executable override without bundled runtime:", - configuredPath, - ); - } - } else { - throw new Error(`Configured native streamer executable was not found: ${configuredPath}`); - } - } - - [ - process.env.OPENNOW_NATIVE_STREAMER, - ...bundledCandidates, - resolve(this.options.mainDir, "../../../native/opennow-streamer/bin", platformKey, exeName), - resolve(this.options.mainDir, "../../../native/opennow-streamer/bin", exeName), - resolve(this.options.mainDir, "../../../native/opennow-streamer/dist", platformKey, exeName), - resolve(this.options.mainDir, "../../../native/opennow-streamer/dist", exeName), - resolve(this.options.mainDir, "../../../native/opennow-streamer/target/release", platformKey, exeName), - resolve(this.options.mainDir, "../../../native/opennow-streamer/target/release", exeName), - resolve(this.options.mainDir, "../../../native/opennow-streamer/target/debug", platformKey, exeName), - resolve(this.options.mainDir, "../../../native/opennow-streamer/target/debug", exeName), - resolve(app.getAppPath(), "../native/opennow-streamer/bin", platformKey, exeName), - resolve(app.getAppPath(), "../native/opennow-streamer/bin", exeName), - resolve(app.getAppPath(), "../native/opennow-streamer/dist", platformKey, exeName), - resolve(app.getAppPath(), "../native/opennow-streamer/dist", exeName), - resolve(app.getAppPath(), "../native/opennow-streamer/target/release", platformKey, exeName), - resolve(app.getAppPath(), "../native/opennow-streamer/target/release", exeName), - resolve(app.getAppPath(), "../native/opennow-streamer/target/debug", platformKey, exeName), - resolve(app.getAppPath(), "../native/opennow-streamer/target/debug", exeName), - ] - .filter((candidate): candidate is string => Boolean(candidate)) - .forEach(addCandidate); - - if (candidates.length > 0) { - return candidates; - } - - throw new Error(`Native streamer binary not found. Checked: ${candidates.join(", ")}`); - } - - private shouldIgnorePackagedExecutableOverride(configuredPath: string): boolean { - if (hasBundledRuntimeNextToExecutable(configuredPath)) { - return false; - } - - const packagedRoots = [ - join(process.resourcesPath, "native", "opennow-streamer"), - resolve(app.getAppPath(), "../native/opennow-streamer"), - resolve(this.options.mainDir, "../../../dist-release/win-unpacked/resources/native/opennow-streamer"), - resolve(this.options.mainDir, "../../../dist-release/win-unpacked/resources/app.asar.unpacked/native/opennow-streamer"), - ]; - - return packagedRoots.some((root) => isPathInside(root, configuredPath)); - } - private request(input: NativeStreamerCommandInput, timeoutMs: number): Promise { const child = this.child; if (!child || child.killed || !child.stdin.writable) { @@ -1094,32 +573,6 @@ export class NativeStreamerManager { }); } - private async flushSurfaceUpdate(): Promise { - if (this.surfaceUpdateInFlight) { - this.surfaceUpdateQueued = true; - return; - } - - while (this.child && this.lastSurface) { - this.surfaceUpdateInFlight = true; - this.surfaceUpdateQueued = false; - const surface = this.lastSurface; - - try { - await this.request({ type: "surface", surface }, SURFACE_UPDATE_TIMEOUT_MS); - } catch (error) { - console.warn("[NativeStreamer] Failed to update native render surface:", error); - break; - } finally { - this.surfaceUpdateInFlight = false; - } - - if (!this.surfaceUpdateQueued || this.lastSurface === surface) { - break; - } - } - } - private handleStdout(chunk: string): void { this.stdoutBuffer += chunk; const lines = this.stdoutBuffer.split(/\r?\n/); @@ -1143,12 +596,12 @@ export class NativeStreamerManager { return; } - if (isResponse(message)) { + if (isNativeStreamerResponse(message)) { this.handleResponse(message); return; } - if (isEvent(message)) { + if (isNativeStreamerEvent(message)) { this.handleEvent(message); } } @@ -1303,6 +756,7 @@ export class NativeStreamerManager { this.stderrTail = []; this.activeSessionId = null; this.capabilities = null; + this.surfaceUpdates.markNotReady(); this.clearQueuedRemoteIce(); this.rejectPending(new Error(`Native streamer process ended (${reason}).${tail}`)); @@ -1378,6 +832,7 @@ export class NativeStreamerManager { } private terminateProcess(): void { + this.surfaceUpdates.markNotReady(); const child = this.child; if (!child) { return; diff --git a/opennow-stable/src/main/nativeStreamer/protocol.test.ts b/opennow-stable/src/main/nativeStreamer/protocol.test.ts new file mode 100644 index 000000000..02e18496b --- /dev/null +++ b/opennow-stable/src/main/nativeStreamer/protocol.test.ts @@ -0,0 +1,18 @@ +import assert from "node:assert/strict"; +import test from "node:test"; + +import type { NativeStreamerMessage } from "@shared/nativeStreamer"; +import { + isNativeStreamerEvent, + isNativeStreamerResponse, +} from "./protocol"; + +test("protocol guards distinguish request responses from events by id", () => { + const response = { id: "request-1", type: "ok" } as NativeStreamerMessage; + const event = { type: "status", status: "ready" } as NativeStreamerMessage; + + assert.equal(isNativeStreamerResponse(response), true); + assert.equal(isNativeStreamerEvent(response), false); + assert.equal(isNativeStreamerResponse(event), false); + assert.equal(isNativeStreamerEvent(event), true); +}); diff --git a/opennow-stable/src/main/nativeStreamer/protocol.ts b/opennow-stable/src/main/nativeStreamer/protocol.ts new file mode 100644 index 000000000..fa03d4f40 --- /dev/null +++ b/opennow-stable/src/main/nativeStreamer/protocol.ts @@ -0,0 +1,28 @@ +import type { + NativeStreamerCommand, + NativeStreamerEvent, + NativeStreamerMessage, + NativeStreamerResponse, +} from "@shared/nativeStreamer"; + +export type NativeStreamerCommandInput = NativeStreamerCommand extends infer T + ? T extends NativeStreamerCommand + ? Omit + : never + : never; + +function isRecord(value: unknown): value is Record { + return typeof value === "object" && value !== null; +} + +export function isNativeStreamerResponse( + message: NativeStreamerMessage, +): message is NativeStreamerResponse { + return isRecord(message) && typeof (message as Record)["id"] === "string"; +} + +export function isNativeStreamerEvent( + message: NativeStreamerMessage, +): message is NativeStreamerEvent { + return isRecord(message) && typeof (message as Record)["id"] !== "string"; +} diff --git a/opennow-stable/src/main/nativeStreamer/runtime.test.ts b/opennow-stable/src/main/nativeStreamer/runtime.test.ts new file mode 100644 index 000000000..0a12a921d --- /dev/null +++ b/opennow-stable/src/main/nativeStreamer/runtime.test.ts @@ -0,0 +1,44 @@ +import assert from "node:assert/strict"; +import { mkdtempSync, mkdirSync, rmSync, symlinkSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import test from "node:test"; + +import { + isPathInside, + nativeStreamerExecutableName, + nativeStreamerPlatformKey, + normalizePathForComparison, +} from "./runtime"; + +test("normalizes real and symlinked paths to the same comparison path", (t) => { + const root = mkdtempSync(join(tmpdir(), "opennow-native-path-")); + t.after(() => rmSync(root, { recursive: true, force: true })); + const runtime = join(root, "runtime"); + const alias = join(root, "runtime-alias"); + mkdirSync(runtime); + mkdirSync(join(runtime, "gstreamer")); + symlinkSync(runtime, alias, "dir"); + + assert.equal( + normalizePathForComparison(alias), + normalizePathForComparison(runtime), + ); + assert.equal(isPathInside(alias, join(runtime, "gstreamer")), true); +}); + +test("path containment rejects sibling names that only share a prefix", (t) => { + const root = mkdtempSync(join(tmpdir(), "opennow-native-path-")); + t.after(() => rmSync(root, { recursive: true, force: true })); + const runtime = join(root, "runtime"); + + assert.equal(isPathInside(runtime, runtime), true); + assert.equal(isPathInside(runtime, join(runtime, "cached", "streamer")), true); + assert.equal(isPathInside(runtime, `${runtime}-old/streamer`), false); +}); + +test("runtime executable names and platform keys accept explicit platforms", () => { + assert.equal(nativeStreamerExecutableName("win32"), "opennow-streamer.exe"); + assert.equal(nativeStreamerExecutableName("linux"), "opennow-streamer"); + assert.equal(nativeStreamerPlatformKey("darwin", "arm64"), "darwin-arm64"); +}); diff --git a/opennow-stable/src/main/nativeStreamer/runtime.ts b/opennow-stable/src/main/nativeStreamer/runtime.ts new file mode 100644 index 000000000..bd4c65b22 --- /dev/null +++ b/opennow-stable/src/main/nativeStreamer/runtime.ts @@ -0,0 +1,238 @@ +import { + existsSync, + mkdirSync, + realpathSync, + statSync, +} from "node:fs"; +import { delimiter, dirname, join, resolve, sep } from "node:path"; + +import { + nativeStreamerFeatureModeToEnvValue, + type NativeGstreamerInstallInstruction, + type NativeGstreamerRuntimeStatus, + type NativeStreamerBackendPreference, + type NativeStreamerFeatureMode, + type NativeVideoBackendPreference, +} from "@shared/gfn"; + +export interface NativeStreamerRuntimeEnvironmentOptions { + executablePath: string; + baseEnv: NodeJS.ProcessEnv; + platform: NodeJS.Platform; + arch: string; + userDataPath: string; + protocolVersion: number; + backendPreference: NativeStreamerBackendPreference; + videoBackendPreference: NativeVideoBackendPreference; + externalRendererEnabled: boolean; + cloudGsyncMode: NativeStreamerFeatureMode; + d3dFullscreenMode: NativeStreamerFeatureMode; +} + +export interface NativeStreamerRuntimeEnvironment { + env: NodeJS.ProcessEnv; + runtimeStatus: NativeGstreamerRuntimeStatus; +} + +const LINUX_GSTREAMER_INSTALL_INSTRUCTIONS: NativeGstreamerInstallInstruction[] = [ + { + distro: "Debian / Ubuntu / Mint / Pop!_OS / KDE neon", + command: "sudo apt update && sudo apt install libgstreamer1.0-0 libgstreamer-plugins-base1.0-0 gstreamer1.0-tools gstreamer1.0-libav gstreamer1.0-plugins-base gstreamer1.0-plugins-good gstreamer1.0-plugins-bad gstreamer1.0-plugins-ugly gstreamer1.0-nice gstreamer1.0-gl gstreamer1.0-vaapi gstreamer1.0-x gstreamer1.0-alsa libva2 libva-drm2 libvulkan1 mesa-vulkan-drivers", + }, + { + distro: "Fedora / RHEL / Nobara / Bazzite", + command: "sudo dnf install gstreamer1 gstreamer1-plugins-base gstreamer1-plugins-good gstreamer1-plugins-bad-free gstreamer1-plugins-bad-freeworld gstreamer1-plugins-ugly gstreamer1-libav gstreamer1-vaapi gstreamer1-plugin-openh264 libnice-gstreamer1 mesa-vulkan-drivers libva", + note: "RPM Fusion may be required for libav, ugly, or bad-freeworld packages.", + }, + { + distro: "Arch / Manjaro / EndeavourOS / SteamOS", + command: "sudo pacman -S --needed gstreamer gst-plugins-base gst-plugins-good gst-plugins-bad gst-plugins-ugly gst-libav gst-plugin-va libnice libva mesa vulkan-radeon", + note: "NVIDIA users should use their distro NVIDIA/Vulkan driver packages instead of vulkan-radeon.", + }, + { + distro: "openSUSE Tumbleweed / Leap", + command: "sudo zypper install gstreamer gstreamer-plugins-base gstreamer-plugins-good gstreamer-plugins-bad gstreamer-plugins-ugly gstreamer-plugins-libav gstreamer-plugins-vaapi gstreamer-libnice libva2 Mesa-vulkan-device-select", + }, +]; + +export function nativeStreamerExecutableName(platform = process.platform): string { + return platform === "win32" ? "opennow-streamer.exe" : "opennow-streamer"; +} + +export function nativeStreamerPlatformKey( + platform: NodeJS.Platform = process.platform, + arch: string = process.arch, +): string { + return `${platform}-${arch}`; +} + +export function isExistingFile(path: string): boolean { + try { + return existsSync(path) && statSync(path).isFile(); + } catch { + return false; + } +} + +export function isExistingDirectory(path: string): boolean { + try { + return existsSync(path) && statSync(path).isDirectory(); + } catch { + return false; + } +} + +export function normalizePathForComparison( + path: string, + platform = process.platform, +): string { + let resolvedPath = resolve(path); + try { + resolvedPath = realpathSync.native(resolvedPath); + } catch { + // Cache destinations and configured overrides may not exist yet. + } + return platform === "win32" ? resolvedPath.toLowerCase() : resolvedPath; +} + +export function isPathInside( + parent: string, + child: string, + platform = process.platform, +): boolean { + const normalizedParent = normalizePathForComparison(parent, platform); + const normalizedChild = normalizePathForComparison(child, platform); + return normalizedChild === normalizedParent || normalizedChild.startsWith(`${normalizedParent}${sep}`); +} + +export function hasBundledRuntimeNextToExecutable(executablePath: string): boolean { + return isExistingDirectory(join(dirname(executablePath), "gstreamer")); +} + +export function linuxInstallInstructions( + platform = process.platform, +): NativeGstreamerInstallInstruction[] | undefined { + return platform === "linux" ? LINUX_GSTREAMER_INSTALL_INSTRUCTIONS : undefined; +} + +function prependEnvPath(env: NodeJS.ProcessEnv, key: string, directory: string): void { + env[key] = env[key] ? `${directory}${delimiter}${env[key]}` : directory; +} + +function prependProcessPath(env: NodeJS.ProcessEnv, directory: string): void { + const pathKey = Object.keys(env).find((key) => key.toLowerCase() === "path") || "PATH"; + prependEnvPath(env, pathKey, directory); +} + +function configureBundledGstreamerRuntime( + env: NodeJS.ProcessEnv, + executablePath: string, + platform: NodeJS.Platform, + arch: string, + userDataPath: string, +): NativeGstreamerRuntimeStatus { + const runtimeRoot = join(dirname(executablePath), "gstreamer"); + if (!isExistingDirectory(runtimeRoot)) { + return { + source: "system", + bundled: false, + message: platform === "linux" + ? "No bundled GStreamer runtime was found. Linux uses distro GStreamer packages so VAAPI/V4L2/Vulkan plugins match the host driver stack." + : "No bundled GStreamer runtime was found; using the system runtime if available.", + installInstructions: linuxInstallInstructions(platform), + }; + } + + const binDir = join(runtimeRoot, "bin"); + const libDir = join(runtimeRoot, "lib"); + const pluginDir = join(runtimeRoot, "lib", "gstreamer-1.0"); + const scanner = join( + runtimeRoot, + "libexec", + "gstreamer-1.0", + platform === "win32" ? "gst-plugin-scanner.exe" : "gst-plugin-scanner", + ); + const gioModulesDir = join(runtimeRoot, "lib", "gio", "modules"); + + if (platform === "win32") prependProcessPath(env, dirname(executablePath)); + if (isExistingDirectory(binDir)) prependProcessPath(env, binDir); + if (isExistingDirectory(pluginDir)) { + env.GST_PLUGIN_PATH = pluginDir; + env.GST_PLUGIN_PATH_1_0 = pluginDir; + env.GST_PLUGIN_SYSTEM_PATH = pluginDir; + env.GST_PLUGIN_SYSTEM_PATH_1_0 = pluginDir; + } + if (isExistingFile(scanner)) { + env.GST_PLUGIN_SCANNER = scanner; + env.GST_PLUGIN_SCANNER_1_0 = scanner; + } + env.GST_REGISTRY_REUSE_PLUGIN_SCANNER = "no"; + if (isExistingDirectory(gioModulesDir)) { + env.GIO_MODULE_DIR = gioModulesDir; + env.GIO_EXTRA_MODULES = gioModulesDir; + } + const registryDir = join(userDataPath, "native-streamer", "gstreamer"); + const registryPath = join(registryDir, `${nativeStreamerPlatformKey(platform, arch)}-registry.bin`); + mkdirSync(registryDir, { recursive: true }); + env.GST_REGISTRY = registryPath; + if (platform === "linux") { + if (isExistingDirectory(libDir)) prependEnvPath(env, "LD_LIBRARY_PATH", libDir); + if (isExistingDirectory(binDir)) prependEnvPath(env, "LD_LIBRARY_PATH", binDir); + } + if (platform === "darwin") { + if (isExistingDirectory(libDir)) { + prependEnvPath(env, "DYLD_LIBRARY_PATH", libDir); + prependEnvPath(env, "DYLD_FALLBACK_LIBRARY_PATH", libDir); + } + if (isExistingDirectory(binDir)) { + prependEnvPath(env, "DYLD_LIBRARY_PATH", binDir); + prependEnvPath(env, "DYLD_FALLBACK_LIBRARY_PATH", binDir); + } + } + + return { + source: "bundled", + bundled: true, + path: runtimeRoot, + message: "Using bundled GStreamer runtime next to the native streamer executable.", + }; +} + +export function createNativeStreamerRuntimeEnvironment( + options: NativeStreamerRuntimeEnvironmentOptions, +): NativeStreamerRuntimeEnvironment { + const env: NodeJS.ProcessEnv = { + ...options.baseEnv, + OPENNOW_NATIVE_STREAMER_PROTOCOL: String(options.protocolVersion), + }; + delete env.OPENNOW_NATIVE_VIDEO_API; + delete env.OPENNOW_NATIVE_VIDEO_BACKEND; + if (options.videoBackendPreference !== "auto") { + env.OPENNOW_NATIVE_VIDEO_BACKEND = options.videoBackendPreference; + } + if (options.platform === "linux") { + env.OPENNOW_NATIVE_EXTERNAL_RENDERER = "0"; + if ((options.arch === "arm64" || options.arch === "arm") && !env.GST_V4L2_ENABLE_PROBE) { + env.GST_V4L2_ENABLE_PROBE = "1"; + } + } else if (options.platform === "win32") { + env.OPENNOW_NATIVE_EXTERNAL_RENDERER = options.externalRendererEnabled ? "1" : "0"; + env.OPENNOW_NATIVE_D3D_ALLOW_TEARING = "1"; + } + env.OPENNOW_NATIVE_CLOUD_GSYNC = nativeStreamerFeatureModeToEnvValue(options.cloudGsyncMode); + env.OPENNOW_NATIVE_D3D_FULLSCREEN = nativeStreamerFeatureModeToEnvValue(options.d3dFullscreenMode); + if (options.backendPreference !== "auto") { + env.OPENNOW_NATIVE_STREAMER_BACKEND = options.backendPreference; + } + + return { + env, + runtimeStatus: configureBundledGstreamerRuntime( + env, + options.executablePath, + options.platform, + options.arch, + options.userDataPath, + ), + }; +} diff --git a/opennow-stable/src/main/nativeStreamer/runtimeCache.test.ts b/opennow-stable/src/main/nativeStreamer/runtimeCache.test.ts new file mode 100644 index 000000000..899c32595 --- /dev/null +++ b/opennow-stable/src/main/nativeStreamer/runtimeCache.test.ts @@ -0,0 +1,70 @@ +import assert from "node:assert/strict"; +import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import test from "node:test"; + +import { + buildPackagedNativeStreamerCacheMarker, + isSamePackagedNativeStreamerCacheMarker, + shouldUseStablePackagedNativeStreamerCache, +} from "./runtimeCache"; + +test("cache markers compare every executable and runtime identity field", (t) => { + const source = mkdtempSync(join(tmpdir(), "opennow-native-marker-")); + t.after(() => rmSync(source, { recursive: true, force: true })); + const runtime = join(source, "gstreamer"); + mkdirSync(runtime); + writeFileSync(join(source, "opennow-streamer.exe"), "streamer-v1"); + writeFileSync(join(runtime, "OPENNOW-GSTREAMER-RUNTIME.txt"), "runtime-v1"); + + const marker = buildPackagedNativeStreamerCacheMarker( + source, + "opennow-streamer.exe", + "win32-x64", + "1.2.3", + ); + const sameMarker = buildPackagedNativeStreamerCacheMarker( + source, + "opennow-streamer.exe", + "win32-x64", + "1.2.3", + ); + assert.equal(isSamePackagedNativeStreamerCacheMarker(marker, sameMarker), true); + + writeFileSync(join(runtime, "OPENNOW-GSTREAMER-RUNTIME.txt"), "runtime-v2"); + const changedRuntimeMarker = buildPackagedNativeStreamerCacheMarker( + source, + "opennow-streamer.exe", + "win32-x64", + "1.2.3", + ); + assert.equal(isSamePackagedNativeStreamerCacheMarker(marker, changedRuntimeMarker), false); + assert.equal(isSamePackagedNativeStreamerCacheMarker(null, marker), false); +}); + +test("stable packaged cache selection is constrained to Windows temporary resources", (t) => { + const temporaryRoot = mkdtempSync(join(tmpdir(), "opennow-native-cache-")); + t.after(() => rmSync(temporaryRoot, { recursive: true, force: true })); + const resourcesPath = join(temporaryRoot, "resources"); + mkdirSync(resourcesPath); + + assert.equal(shouldUseStablePackagedNativeStreamerCache({ + isPackaged: true, + platform: "win32", + resourcesPath, + tempDirectory: tmpdir(), + }), true); + assert.equal(shouldUseStablePackagedNativeStreamerCache({ + isPackaged: true, + platform: "linux", + resourcesPath, + tempDirectory: tmpdir(), + }), false); + assert.equal(shouldUseStablePackagedNativeStreamerCache({ + isPackaged: false, + platform: "win32", + resourcesPath, + tempDirectory: tmpdir(), + }), false); +}); diff --git a/opennow-stable/src/main/nativeStreamer/runtimeCache.ts b/opennow-stable/src/main/nativeStreamer/runtimeCache.ts new file mode 100644 index 000000000..572e998d2 --- /dev/null +++ b/opennow-stable/src/main/nativeStreamer/runtimeCache.ts @@ -0,0 +1,175 @@ +import { createHash } from "node:crypto"; +import { + cpSync, + mkdirSync, + readFileSync, + renameSync, + rmSync, + writeFileSync, +} from "node:fs"; +import { dirname, join } from "node:path"; + +import { + hasBundledRuntimeNextToExecutable, + isExistingDirectory, + isExistingFile, + isPathInside, +} from "./runtime"; + +export interface PackagedNativeStreamerCacheMarker { + appVersion: string; + platformKey: string; + exeName: string; + exeSha256: string; + bundledRuntime: boolean; + runtimeManifestSha256?: string; +} + +export interface PackagedNativeStreamerCacheContext { + appVersion: string; + isPackaged: boolean; + platform: NodeJS.Platform; + resourcesPath: string; + tempDirectory: string; + userDataPath: string; +} + +function safePathSegment(value: string): string { + return value.replace(/[^a-zA-Z0-9._-]/g, "_") || "unknown"; +} + +function fileSha256(path: string): string { + return createHash("sha256").update(readFileSync(path)).digest("hex"); +} + +export function shouldUseStablePackagedNativeStreamerCache( + context: Pick< + PackagedNativeStreamerCacheContext, + "isPackaged" | "platform" | "resourcesPath" | "tempDirectory" + >, +): boolean { + return context.isPackaged + && context.platform === "win32" + && isPathInside(context.tempDirectory, context.resourcesPath, context.platform); +} + +export function buildPackagedNativeStreamerCacheMarker( + sourceDirectory: string, + exeName: string, + platformKey: string, + appVersion: string, +): PackagedNativeStreamerCacheMarker { + const runtimeManifest = join(sourceDirectory, "gstreamer", "OPENNOW-GSTREAMER-RUNTIME.txt"); + return { + appVersion, + platformKey, + exeName, + exeSha256: fileSha256(join(sourceDirectory, exeName)), + bundledRuntime: isExistingDirectory(join(sourceDirectory, "gstreamer")), + runtimeManifestSha256: isExistingFile(runtimeManifest) ? fileSha256(runtimeManifest) : undefined, + }; +} + +export function readPackagedNativeStreamerCacheMarker( + markerPath: string, +): PackagedNativeStreamerCacheMarker | null { + try { + return JSON.parse(readFileSync(markerPath, "utf8")) as PackagedNativeStreamerCacheMarker; + } catch { + return null; + } +} + +export function isSamePackagedNativeStreamerCacheMarker( + left: PackagedNativeStreamerCacheMarker | null, + right: PackagedNativeStreamerCacheMarker, +): boolean { + if (!left) { + return false; + } + + return left.appVersion === right.appVersion + && left.platformKey === right.platformKey + && left.exeName === right.exeName + && left.exeSha256 === right.exeSha256 + && left.bundledRuntime === right.bundledRuntime + && left.runtimeManifestSha256 === right.runtimeManifestSha256; +} + +export function materializePackagedNativeStreamerCache( + sourceExecutablePath: string, + platformKey: string, + exeName: string, + context: PackagedNativeStreamerCacheContext, +): string | null { + if (!shouldUseStablePackagedNativeStreamerCache(context)) { + return null; + } + + const sourceDirectory = dirname(sourceExecutablePath); + const cacheDirectory = join( + context.userDataPath, + "native-streamer", + "runtime", + safePathSegment(context.appVersion), + safePathSegment(platformKey), + ); + const cachedExecutablePath = join(cacheDirectory, exeName); + const markerPath = join(cacheDirectory, ".opennow-native-runtime.json"); + let stagingDirectory: string | null = null; + + try { + const expectedMarker = buildPackagedNativeStreamerCacheMarker( + sourceDirectory, + exeName, + platformKey, + context.appVersion, + ); + const cachedMarker = readPackagedNativeStreamerCacheMarker(markerPath); + if ( + isExistingFile(cachedExecutablePath) + && isSamePackagedNativeStreamerCacheMarker(cachedMarker, expectedMarker) + && (!expectedMarker.bundledRuntime || hasBundledRuntimeNextToExecutable(cachedExecutablePath)) + ) { + return cachedExecutablePath; + } + + stagingDirectory = `${cacheDirectory}.tmp-${process.pid}-${Date.now()}`; + rmSync(stagingDirectory, { recursive: true, force: true }); + mkdirSync(dirname(stagingDirectory), { recursive: true }); + cpSync(sourceDirectory, stagingDirectory, { + recursive: true, + force: true, + dereference: true, + filter: (entry) => { + const lower = entry.toLowerCase(); + return !lower.endsWith(".pdb") && !lower.endsWith(".lib") && !lower.endsWith(".a"); + }, + }); + writeFileSync( + join(stagingDirectory, ".opennow-native-runtime.json"), + `${JSON.stringify(expectedMarker, null, 2)}\n`, + "utf8", + ); + + if (!isExistingFile(join(stagingDirectory, exeName))) { + throw new Error(`Cached native streamer executable was not created: ${join(stagingDirectory, exeName)}`); + } + if (expectedMarker.bundledRuntime && !hasBundledRuntimeNextToExecutable(join(stagingDirectory, exeName))) { + throw new Error("Cached native streamer runtime is missing its bundled GStreamer directory."); + } + + rmSync(cacheDirectory, { recursive: true, force: true }); + renameSync(stagingDirectory, cacheDirectory); + stagingDirectory = null; + console.log("[NativeStreamer] Cached packaged native streamer in stable runtime path:", cachedExecutablePath); + return cachedExecutablePath; + } catch (error) { + console.warn("[NativeStreamer] Failed to prepare stable packaged runtime cache; using packaged resource path:", error); + return null; + } finally { + if (stagingDirectory) { + rmSync(stagingDirectory, { recursive: true, force: true }); + } + } +} diff --git a/opennow-stable/src/main/nativeStreamer/surfaceUpdateQueue.test.ts b/opennow-stable/src/main/nativeStreamer/surfaceUpdateQueue.test.ts new file mode 100644 index 000000000..bc03eb865 --- /dev/null +++ b/opennow-stable/src/main/nativeStreamer/surfaceUpdateQueue.test.ts @@ -0,0 +1,89 @@ +import assert from "node:assert/strict"; +import test from "node:test"; + +import type { NativeRenderSurface } from "@shared/gfn"; +import { NativeSurfaceUpdateQueue } from "./surfaceUpdateQueue"; + +function surface(width: number): NativeRenderSurface { + return { + rect: { x: 0, y: 0, width, height: 720 }, + visible: true, + deviceScaleFactor: 1, + }; +} + +function deferred(): { promise: Promise; resolve(): void } { + let resolve!: () => void; + const promise = new Promise((resolvePromise) => { + resolve = resolvePromise; + }); + return { promise, resolve }; +} + +test("holds the latest surface until the native handshake is ready", async () => { + const sent: NativeRenderSurface[] = []; + const queue = new NativeSurfaceUpdateQueue(async (value) => { + sent.push(value); + }, (error) => assert.fail(String(error))); + + queue.update(surface(1280)); + queue.update(surface(1920)); + assert.deepEqual(sent, []); + + await queue.markReady(); + assert.deepEqual(sent, [surface(1920)]); +}); + +test("coalesces updates without losing one that arrives during a send", async () => { + const firstSend = deferred(); + const sent: NativeRenderSurface[] = []; + const queue = new NativeSurfaceUpdateQueue(async (value) => { + sent.push(value); + if (sent.length === 1) { + await firstSend.promise; + } + }, (error) => assert.fail(String(error))); + + queue.update(surface(1280)); + const ready = queue.markReady(); + await Promise.resolve(); + queue.update(surface(1600)); + queue.update(surface(2560)); + firstSend.resolve(); + await ready; + + assert.deepEqual(sent, [surface(1280), surface(2560)]); +}); + +test("reapplies the current surface after the native process restarts", async () => { + const sent: NativeRenderSurface[] = []; + const queue = new NativeSurfaceUpdateQueue(async (value) => { + sent.push(value); + }, (error) => assert.fail(String(error))); + + queue.update(surface(2560)); + await queue.markReady(); + queue.markNotReady(); + await queue.markReady(); + + assert.deepEqual(sent, [surface(2560), surface(2560)]); +}); + +test("retries a failed revision when a newer surface arrives", async () => { + const sent: NativeRenderSurface[] = []; + const errors: unknown[] = []; + const queue = new NativeSurfaceUpdateQueue(async (value) => { + sent.push(value); + if (sent.length === 1) { + throw new Error("surface timeout"); + } + }, (error) => errors.push(error)); + + queue.update(surface(1280)); + await queue.markReady(); + queue.update(surface(1920)); + await Promise.resolve(); + + assert.equal(errors.length, 1); + assert.deepEqual(sent, [surface(1280), surface(1920)]); +}); diff --git a/opennow-stable/src/main/nativeStreamer/surfaceUpdateQueue.ts b/opennow-stable/src/main/nativeStreamer/surfaceUpdateQueue.ts new file mode 100644 index 000000000..40a860d1c --- /dev/null +++ b/opennow-stable/src/main/nativeStreamer/surfaceUpdateQueue.ts @@ -0,0 +1,99 @@ +import type { NativeRenderSurface } from "@shared/gfn"; + +type SurfaceSender = (surface: NativeRenderSurface) => Promise; +type SurfaceErrorHandler = (error: unknown) => void; + +/** + * Delivers only the newest native render surface after the child handshake is + * complete. Revisions make updates that arrive during an in-flight request + * observable instead of relying on object identity or a lossy queued flag. + */ +export class NativeSurfaceUpdateQueue { + private latestSurface: NativeRenderSurface | null = null; + private latestRevision = 0; + private deliveredRevision = 0; + private processGeneration = 0; + private ready = false; + private inFlight = false; + + constructor( + private readonly send: SurfaceSender, + private readonly onError: SurfaceErrorHandler, + ) {} + + update(surface: NativeRenderSurface): void { + this.latestSurface = surface; + this.latestRevision += 1; + void this.flush(); + } + + markNotReady(): void { + this.ready = false; + this.processGeneration += 1; + + // A replacement process must receive the current surface even when it was + // already delivered successfully to the process that just stopped. + if (this.latestSurface) { + this.deliveredRevision = Math.min(this.deliveredRevision, this.latestRevision - 1); + } + } + + markReady(): Promise { + this.ready = true; + return this.flush(); + } + + private async flush(): Promise { + if ( + this.inFlight + || !this.ready + || !this.latestSurface + || this.deliveredRevision >= this.latestRevision + ) { + return; + } + + this.inFlight = true; + let attemptedRevision = -1; + let attemptedGeneration = -1; + + try { + while ( + this.ready + && this.latestSurface + && this.deliveredRevision < this.latestRevision + ) { + const surface = this.latestSurface; + attemptedRevision = this.latestRevision; + attemptedGeneration = this.processGeneration; + + try { + await this.send(surface); + } catch (error) { + this.onError(error); + break; + } + + if (this.ready && attemptedGeneration === this.processGeneration) { + this.deliveredRevision = attemptedRevision; + } + } + } finally { + this.inFlight = false; + + // Retry only when something changed during the failed/in-flight send. + // A stable failure waits for the next surface update instead of spinning. + if ( + this.ready + && this.latestSurface + && this.deliveredRevision < this.latestRevision + && ( + attemptedRevision !== this.latestRevision + || attemptedGeneration !== this.processGeneration + ) + ) { + void this.flush(); + } + } + } +} diff --git a/opennow-stable/src/main/gfn/accountConnections.ts b/opennow-stable/src/main/platforms/gfn/accountConnections.ts similarity index 100% rename from opennow-stable/src/main/gfn/accountConnections.ts rename to opennow-stable/src/main/platforms/gfn/accountConnections.ts diff --git a/opennow-stable/src/main/platforms/gfn/auth.ts b/opennow-stable/src/main/platforms/gfn/auth.ts new file mode 100644 index 000000000..293cd87bf --- /dev/null +++ b/opennow-stable/src/main/platforms/gfn/auth.ts @@ -0,0 +1,365 @@ +import { randomBytes } from "node:crypto"; + +import { shell } from "electron"; + +import type { + AuthDeviceLoginAttemptRequest, + AuthDeviceLoginChallenge, + AuthDeviceLoginPollRequest, + AuthDeviceLoginPollResult, + AuthDeviceLoginStartRequest, + AuthLoginRequest, + AuthSession, + AuthSessionResult, + AuthTokens, + LoginProvider, + SavedAccount, + StreamRegion, + SubscriptionInfo, +} from "@shared/gfn"; + +import { AccountManager } from "./auth/accountManager"; +import { exchangeDeviceCode, requestDeviceAuthorization } from "./auth/deviceLogin"; +import { SubscriptionVpcEnrichmentCaches } from "./auth/enrichmentCaches"; +import { + buildAuthUrl, + exchangeAuthorizationCode, + findAvailablePort, + generatePkce, + waitForAuthorizationCode, +} from "./auth/oauthFlow"; +import { PersistedAccountState } from "./auth/persistedAccountState"; +import { + normalizeProvider, + ProviderDiscovery, +} from "./auth/providerDiscovery"; +import { SessionValidityCoordinator } from "./auth/sessionValidity"; +import { fetchUserInfo } from "./auth/userInfo"; +import { buildGfnLcarsHeaders } from "./clientHeaders"; + +interface ServerInfoResponse { + metaData?: Array<{ + key: string; + value: string; + }>; +} + +interface DeviceLoginAttempt { + provider: LoginProvider; + deviceCode: string; + expiresAt: number; +} + +export class AuthService { + private readonly state: PersistedAccountState; + private readonly providerDiscovery: ProviderDiscovery; + private readonly enrichmentCaches: SubscriptionVpcEnrichmentCaches; + private readonly sessionValidity: SessionValidityCoordinator; + private readonly accountManager: AccountManager; + private deviceLoginAttempts = new Map(); + private pendingDeviceLoginSessions = new Map(); + + constructor(statePath: string) { + this.state = new PersistedAccountState(statePath); + this.providerDiscovery = new ProviderDiscovery(); + this.enrichmentCaches = new SubscriptionVpcEnrichmentCaches({ + getSession: () => this.getSession(), + getSelectedProvider: () => this.getSelectedProvider(), + ensureValidSession: () => this.ensureValidSession(), + updateSession: (session) => this.state.accounts.updateSession(session), + }); + this.sessionValidity = new SessionValidityCoordinator({ + state: this.state, + enrichmentCaches: this.enrichmentCaches, + logout: () => this.accountManager.logout(), + }); + this.accountManager = new AccountManager( + this.state, + () => this.enrichmentCaches.clearAll(), + ); + } + + async initialize(): Promise { + const restoredSession = await this.state.initialize(); + if (restoredSession) { + this.state.accounts.setSelectedProvider(restoredSession.provider); + await this.enrichmentCaches.enrichUserTier(); + await this.state.persist(); + } + } + + async getProviders(): Promise { + return this.providerDiscovery.getProviders(); + } + + setSession(session: AuthSession | null): void { + this.accountManager.setSession(session); + } + + getSession(): AuthSession | null { + return this.state.accounts.getSession(); + } + + getSavedAccounts(): SavedAccount[] { + return this.accountManager.getSavedAccounts(); + } + + async switchAccount(userId: string): Promise { + return this.accountManager.switchAccount( + userId, + (forceRefresh, expectedUserId) => + this.ensureValidSessionWithStatus(forceRefresh, expectedUserId), + ); + } + + async removeAccount(userId: string): Promise { + await this.accountManager.removeAccount(userId); + } + + async logoutAll(): Promise { + await this.accountManager.logoutAll(); + } + + getSelectedProvider(): LoginProvider { + return this.state.accounts.getSelectedProvider(); + } + + private async selectLoginProvider(providerIdpId?: string): Promise { + const selected = await this.providerDiscovery.selectProvider( + this.state.accounts.getPersistedSelectedProvider(), + providerIdpId, + ); + this.state.accounts.setSelectedProvider(selected); + return this.state.accounts.getPersistedSelectedProvider(); + } + + private async buildLoginSession( + initialTokens: AuthTokens, + provider: LoginProvider, + ): Promise { + const user = await fetchUserInfo(initialTokens); + console.debug("auth: fetched user info during login", { + userId: user.userId, + email: user.email, + avatarUrl: user.avatarUrl, + }); + let tokens = initialTokens; + try { + tokens = await this.sessionValidity.ensureClientToken(initialTokens); + } catch (error) { + console.warn("Unable to fetch client token after login. Falling back to OAuth token only:", error); + } + + return { + provider: normalizeProvider(provider), + tokens, + user, + }; + } + + private async saveLoginSession(session: AuthSession): Promise { + return this.accountManager.saveLoginSession( + session, + () => this.enrichmentCaches.enrichUserTier(), + ); + } + + private pruneExpiredDeviceLogins(now = Date.now(), skipAttemptId?: string): void { + for (const [attemptId, attempt] of this.deviceLoginAttempts) { + if (attemptId === skipAttemptId) { + continue; + } + if (attempt.expiresAt <= now) { + this.deviceLoginAttempts.delete(attemptId); + this.pendingDeviceLoginSessions.delete(attemptId); + } + } + } + + async getRegions(explicitToken?: string): Promise { + const provider = this.getSelectedProvider(); + const base = provider.streamingServiceUrl.endsWith("/") + ? provider.streamingServiceUrl + : `${provider.streamingServiceUrl}/`; + + let token = explicitToken; + if (!token) { + const session = await this.ensureValidSession(); + token = session ? session.tokens.idToken ?? session.tokens.accessToken : undefined; + } + + const headers = buildGfnLcarsHeaders({ + token, + clientType: "BROWSER", + clientStreamer: "WEBRTC", + includeUserAgent: true, + }); + + let response: Response; + try { + response = await fetch(`${base}v2/serverInfo`, { headers }); + } catch { + return []; + } + if (!response.ok) { + return []; + } + + const payload = (await response.json()) as ServerInfoResponse; + return (payload.metaData ?? []) + .filter((entry) => entry.value.startsWith("https://")) + .filter((entry) => entry.key !== "gfn-regions" && !entry.key.startsWith("gfn-")) + .map((entry) => ({ + name: entry.key, + url: entry.value.endsWith("/") ? entry.value : `${entry.value}/`, + })) + .sort((a, b) => a.name.localeCompare(b.name)); + } + + async login(input: AuthLoginRequest): Promise { + const provider = await this.selectLoginProvider(input.providerIdpId); + const { verifier, challenge } = generatePkce(); + const port = await findAvailablePort(); + const authUrl = buildAuthUrl(provider, challenge, port); + const codePromise = waitForAuthorizationCode(port, 120000); + await shell.openExternal(authUrl); + const code = await codePromise; + const initialTokens = await exchangeAuthorizationCode(code, verifier, port); + const session = await this.buildLoginSession(initialTokens, provider); + return this.saveLoginSession(session); + } + + async startDeviceLogin(input: AuthDeviceLoginStartRequest): Promise { + this.pruneExpiredDeviceLogins(); + const provider = await this.selectLoginProvider(input.providerIdpId); + const challenge = await requestDeviceAuthorization(provider); + const attemptId = randomBytes(16).toString("hex"); + this.deviceLoginAttempts.set(attemptId, { + provider, + deviceCode: challenge.deviceCode, + expiresAt: challenge.expiresAt, + }); + return { ...challenge, attemptId }; + } + + async pollDeviceLogin(input: AuthDeviceLoginPollRequest): Promise { + this.pruneExpiredDeviceLogins(); + if (!input.attemptId || !input.deviceCode) { + return { status: "error", error: "Missing device code" }; + } + + const attempt = this.deviceLoginAttempts.get(input.attemptId); + if (!attempt || attempt.deviceCode !== input.deviceCode) { + return { status: "expired", error: "QR login was cancelled or expired" }; + } + if (Date.now() >= attempt.expiresAt) { + this.cancelDeviceLogin(input); + return { status: "expired", error: "QR login expired" }; + } + + const result = await exchangeDeviceCode(input.deviceCode); + if (!this.deviceLoginAttempts.has(input.attemptId)) { + return { status: "expired", error: "QR login was cancelled" }; + } + + if ("accessToken" in result) { + const session = await this.buildLoginSession(result, attempt.provider); + if (!this.deviceLoginAttempts.has(input.attemptId)) { + return { status: "expired", error: "QR login was cancelled" }; + } + this.pendingDeviceLoginSessions.set(input.attemptId, session); + return { status: "authorized" }; + } + + switch (result.error) { + case "authorization_pending": + return { status: "pending", error: result.error_description }; + case "slow_down": + return { status: "slow_down", error: result.error_description }; + case "expired_token": + this.cancelDeviceLogin(input); + return { status: "expired", error: result.error_description ?? "QR login expired" }; + case "access_denied": + this.cancelDeviceLogin(input); + return { status: "access_denied", error: result.error_description ?? "QR login was denied" }; + default: + this.cancelDeviceLogin(input); + return { status: "error", error: result.error_description ?? result.error ?? "QR login failed" }; + } + } + + async completeDeviceLogin(input: AuthDeviceLoginAttemptRequest): Promise { + this.pruneExpiredDeviceLogins(Date.now(), input.attemptId); + const session = this.pendingDeviceLoginSessions.get(input.attemptId); + if (!session || !this.deviceLoginAttempts.has(input.attemptId)) { + throw new Error("QR login is no longer active"); + } + + this.cancelDeviceLogin(input); + return this.saveLoginSession(session); + } + + cancelDeviceLogin(input: AuthDeviceLoginAttemptRequest): void { + this.deviceLoginAttempts.delete(input.attemptId); + this.pendingDeviceLoginSessions.delete(input.attemptId); + } + + async logout(): Promise { + await this.accountManager.logout(); + } + + async getSubscription(): Promise { + return this.enrichmentCaches.getSubscription(); + } + + clearSubscriptionCache(): void { + this.enrichmentCaches.clearSubscription(); + } + + getCachedSubscription(): SubscriptionInfo | null { + return this.enrichmentCaches.getCachedSubscription(); + } + + async getVpcId(explicitToken?: string): Promise { + return this.enrichmentCaches.getVpcId(explicitToken); + } + + clearVpcCache(): void { + this.enrichmentCaches.clearVpc(); + } + + getCachedVpcId(): string | null { + return this.enrichmentCaches.getCachedVpcId(); + } + + async ensureValidSessionWithStatus( + forceRefresh = false, + expectedUserId?: string, + ): Promise { + return this.sessionValidity.ensureValidSessionWithStatus(forceRefresh, expectedUserId); + } + + async ensureValidSession(): Promise { + return this.sessionValidity.ensureValidSession(); + } + + async resolveJwtToken(explicitToken?: string): Promise { + if (this.getSession()) { + const session = await this.ensureValidSession(); + if (!session) { + throw new Error("No authenticated session available"); + } + return session.tokens.idToken ?? session.tokens.accessToken; + } + + if (explicitToken && explicitToken.trim()) { + return explicitToken.trim(); + } + + const session = await this.ensureValidSession(); + if (!session) { + throw new Error("No authenticated session available"); + } + return session.tokens.idToken ?? session.tokens.accessToken; + } +} diff --git a/opennow-stable/src/main/platforms/gfn/auth/accountManager.test.ts b/opennow-stable/src/main/platforms/gfn/auth/accountManager.test.ts new file mode 100644 index 000000000..745ba7e02 --- /dev/null +++ b/opennow-stable/src/main/platforms/gfn/auth/accountManager.test.ts @@ -0,0 +1,75 @@ +/// + +import assert from "node:assert/strict"; +import { mkdtemp, readFile, rm } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import test from "node:test"; + +import type { AuthSession } from "@shared/gfn"; + +import { AccountManager } from "./accountManager"; +import { PersistedAccountState } from "./persistedAccountState"; + +function session(userId: string): AuthSession { + return { + provider: { + idpId: "nvidia", + code: "NVIDIA", + displayName: "NVIDIA", + streamingServiceUrl: "https://provider.example/", + priority: 0, + }, + tokens: { + accessToken: `${userId}-access`, + expiresAt: Date.now() + 60_000, + }, + user: { + userId, + displayName: userId, + membershipTier: "FREE", + }, + }; +} + +test("AccountManager removes an incomplete switched account and restores the prior account", async (t) => { + const directory = await mkdtemp(join(tmpdir(), "opennow-auth-account-")); + t.after(() => rm(directory, { recursive: true, force: true })); + + const statePath = join(directory, "auth.json"); + const state = new PersistedAccountState(statePath); + const first = session("first"); + const second = session("second"); + state.accounts.setSession(first); + state.accounts.setSession(second); + state.accounts.setActiveAccount("first"); + + let cacheClears = 0; + const manager = new AccountManager(state, () => { + cacheClears += 1; + }); + + await assert.rejects( + () => manager.switchAccount("second", async () => ({ + session: second, + refresh: { + attempted: true, + forced: true, + outcome: "missing_refresh_token", + message: "No refresh token available.", + }, + })), + /Saved login for this account is incomplete/, + ); + + assert.equal(state.accounts.getSession()?.user.userId, "first"); + assert.equal(state.accounts.hasAccount("second"), false); + assert.equal(cacheClears, 3); + + const persisted = JSON.parse(await readFile(statePath, "utf8")) as { + activeUserId: string | null; + sessions: AuthSession[]; + }; + assert.equal(persisted.activeUserId, "first"); + assert.deepEqual(persisted.sessions.map((saved) => saved.user.userId), ["first"]); +}); diff --git a/opennow-stable/src/main/platforms/gfn/auth/accountManager.ts b/opennow-stable/src/main/platforms/gfn/auth/accountManager.ts new file mode 100644 index 000000000..43764e099 --- /dev/null +++ b/opennow-stable/src/main/platforms/gfn/auth/accountManager.ts @@ -0,0 +1,111 @@ +import type { AuthSession, AuthSessionResult, SavedAccount } from "@shared/gfn"; + +import type { PersistedAccountState } from "./persistedAccountState"; + +type EnsureSession = (forceRefresh: boolean, expectedUserId?: string) => Promise; + +export class AccountManager { + constructor( + private readonly state: PersistedAccountState, + private readonly clearCaches: () => void, + ) {} + + setSession(session: AuthSession | null): void { + if (!session) { + this.state.accounts.reset(); + this.clearCaches(); + void this.state.persist(); + return; + } + + this.state.accounts.setSession(session); + this.clearCaches(); + void this.state.persist(); + } + + getSavedAccounts(): SavedAccount[] { + return this.state.accounts.getSavedAccounts(); + } + + async saveLoginSession( + session: AuthSession, + enrichUserTier: () => Promise, + ): Promise { + this.state.accounts.setSession(session); + this.clearCaches(); + await enrichUserTier(); + await this.state.persist(); + return this.state.accounts.getSession() as AuthSession; + } + + async switchAccount(userId: string, ensureSession: EnsureSession): Promise { + const target = this.state.accounts.getSessionForUser(userId); + if (!target) { + throw new Error("Saved account not found"); + } + + const previousActiveUserId = this.state.accounts.getActiveUserId(); + const previousSelectedProvider = this.state.accounts.getPersistedSelectedProvider(); + + this.state.accounts.setActiveAccount(userId); + this.clearCaches(); + + const result = await ensureSession(true, userId); + const missingRefreshToken = result.refresh.outcome === "missing_refresh_token"; + const refreshFailed = result.refresh.outcome === "failed"; + const switchedUserMismatch = result.session?.user.userId !== userId; + if (!result.session || refreshFailed || missingRefreshToken || switchedUserMismatch) { + const fallbackMessage = "Failed to switch account due to an invalid or expired session."; + + if (missingRefreshToken) { + await this.removeAccount(userId); + this.state.accounts.setActiveAccount(previousActiveUserId); + this.clearCaches(); + await this.state.persist(); + throw new Error("Saved login for this account is incomplete. Please log in to this account again."); + } + + this.state.accounts.setActiveAccount(previousActiveUserId); + if (previousActiveUserId && this.state.accounts.hasAccount(previousActiveUserId)) { + this.state.accounts.setSelectedProvider(previousSelectedProvider); + } + this.clearCaches(); + await this.state.persist(); + + if (switchedUserMismatch) { + throw new Error("Switched session did not match the selected account."); + } + throw new Error(result.refresh.message || fallbackMessage); + } + return result.session; + } + + async removeAccount(userId: string): Promise { + const removed = this.state.accounts.removeAccount(userId); + if (!removed) { + return; + } + if (this.state.accounts.getActiveUserId() === userId) { + this.state.accounts.setActiveAccount(this.state.accounts.firstUserId()); + } + this.clearCaches(); + await this.state.persist(); + } + + async logout(): Promise { + const activeUserId = this.state.accounts.getActiveUserId(); + if (!activeUserId) { + return; + } + this.state.accounts.removeAccount(activeUserId); + this.state.accounts.setActiveAccount(this.state.accounts.firstUserId()); + this.clearCaches(); + await this.state.persist(); + } + + async logoutAll(): Promise { + this.state.accounts.reset(); + this.clearCaches(); + await this.state.persist(); + } +} diff --git a/opennow-stable/src/main/platforms/gfn/auth/constants.ts b/opennow-stable/src/main/platforms/gfn/auth/constants.ts new file mode 100644 index 000000000..cfce910cb --- /dev/null +++ b/opennow-stable/src/main/platforms/gfn/auth/constants.ts @@ -0,0 +1,17 @@ +export const SERVICE_URLS_ENDPOINT = "https://pcs.geforcenow.com/v1/serviceUrls"; +export const TOKEN_ENDPOINT = "https://login.nvidia.com/token"; +export const CLIENT_TOKEN_ENDPOINT = "https://login.nvidia.com/client_token"; +export const USERINFO_ENDPOINT = "https://login.nvidia.com/userinfo"; +export const AUTH_ENDPOINT = "https://login.nvidia.com/authorize"; +export const DEVICE_AUTHORIZE_ENDPOINT = "https://login.nvidia.com/device/authorize"; + +export const CLIENT_ID = "ZU7sPN-miLujMD95LfOQ453IB0AtjM8sMyvgJ9wCXEQ"; +export const STEAM_DECK_CLIENT_ID = "q61ddeJrVt7O90Nl-P-N7I36yctih4Ml6FyXLrb6j-U"; +export const SCOPES = "openid consent email tk_client age"; +export const DEFAULT_IDP_ID = "PDiAhv2kJTFeQ7WOPqiQ2tRZ7lGhR2X11dXvM4TZSxg"; +export const STEAM_DECK_USER_AGENT = + "Mozilla/5.0 (X11; Linux x86_64; Steam Deck) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36"; + +export const REDIRECT_PORTS = [2259, 6460, 7119, 8870, 9096]; +export const TOKEN_REFRESH_WINDOW_MS = 10 * 60 * 1000; +export const CLIENT_TOKEN_REFRESH_WINDOW_MS = 5 * 60 * 1000; diff --git a/opennow-stable/src/main/platforms/gfn/auth/deviceLogin.ts b/opennow-stable/src/main/platforms/gfn/auth/deviceLogin.ts new file mode 100644 index 000000000..52a1c5334 --- /dev/null +++ b/opennow-stable/src/main/platforms/gfn/auth/deviceLogin.ts @@ -0,0 +1,136 @@ +import type { AuthDeviceLoginChallenge, AuthTokens, LoginProvider } from "@shared/gfn"; + +import { + DEVICE_AUTHORIZE_ENDPOINT, + SCOPES, + STEAM_DECK_CLIENT_ID, + TOKEN_ENDPOINT, +} from "./constants"; +import { buildAuthHeadersForClient, generateDeviceId, toExpiresAt } from "./helpers"; + +interface DeviceAuthorizationResponse { + device_code?: string; + user_code?: string; + verification_uri?: string; + verification_uri_complete?: string; + expires_in?: number; + interval?: number; +} + +interface TokenResponse { + access_token: string; + refresh_token?: string; + id_token?: string; + client_token?: string; + expires_in?: number; +} + +export interface DeviceTokenErrorResponse { + error?: string; + error_description?: string; +} + +export async function requestDeviceAuthorization( + provider: LoginProvider, +): Promise> { + const deviceId = generateDeviceId(); + const body = new URLSearchParams({ + client_id: STEAM_DECK_CLIENT_ID, + scope: SCOPES, + device_id: deviceId, + display_name: "OpenNOW", + idp_id: provider.idpId, + }); + + const response = await fetch(DEVICE_AUTHORIZE_ENDPOINT, { + method: "POST", + headers: { + ...buildAuthHeadersForClient(STEAM_DECK_CLIENT_ID, { + contentType: "application/x-www-form-urlencoded; charset=UTF-8", + }), + "x-device-id": deviceId, + "nv-client-id": STEAM_DECK_CLIENT_ID, + "nv-client-streamer": "WEBRTC", + "nv-client-type": "BROWSER", + "nv-client-platform-name": "browser", + "nv-browser-type": "CHROME", + "nv-device-os": "STEAMOS", + "nv-device-type": "CONSOLE", + "nv-device-model": "STEAMDECK", + "nv-device-make": "VALVE", + }, + body, + }); + + if (!response.ok) { + const text = await response.text(); + throw new Error(`Device authorization failed (${response.status}): ${text.slice(0, 400)}`); + } + + const payload = (await response.json()) as DeviceAuthorizationResponse; + if ( + !payload.device_code || + !payload.user_code || + !payload.verification_uri || + !payload.verification_uri_complete + ) { + throw new Error("Device authorization response did not include QR login data"); + } + + return { + deviceCode: payload.device_code, + userCode: payload.user_code, + verificationUri: payload.verification_uri, + verificationUriComplete: payload.verification_uri_complete, + expiresAt: toExpiresAt(payload.expires_in, 600), + intervalSeconds: Math.max(1, payload.interval ?? 5), + }; +} + +export async function exchangeDeviceCode( + deviceCode: string, +): Promise { + const body = new URLSearchParams({ + grant_type: "urn:ietf:params:oauth:grant-type:device_code", + device_code: deviceCode, + client_id: STEAM_DECK_CLIENT_ID, + }); + + const response = await fetch(TOKEN_ENDPOINT, { + method: "POST", + headers: buildAuthHeadersForClient(STEAM_DECK_CLIENT_ID, { + contentType: "application/x-www-form-urlencoded; charset=UTF-8", + }), + body, + }); + + const payload = (await response.json().catch(() => null)) as + | TokenResponse + | DeviceTokenErrorResponse + | null; + if (!response.ok) { + return payload && typeof payload === "object" + ? (payload as DeviceTokenErrorResponse) + : { + error: "device_token_exchange_failed", + error_description: `Device token exchange failed (${response.status})`, + }; + } + + const tokenPayload = payload as TokenResponse | null; + if (!tokenPayload?.access_token) { + return { + error: "invalid_token_response", + error_description: "Device token response did not include access_token", + }; + } + + return { + accessToken: tokenPayload.access_token, + refreshToken: tokenPayload.refresh_token, + idToken: tokenPayload.id_token, + expiresAt: toExpiresAt(tokenPayload.expires_in), + authClientId: STEAM_DECK_CLIENT_ID, + clientToken: tokenPayload.client_token, + }; +} diff --git a/opennow-stable/src/main/platforms/gfn/auth/enrichmentCaches.ts b/opennow-stable/src/main/platforms/gfn/auth/enrichmentCaches.ts new file mode 100644 index 000000000..6aa0597d9 --- /dev/null +++ b/opennow-stable/src/main/platforms/gfn/auth/enrichmentCaches.ts @@ -0,0 +1,129 @@ +import type { AuthSession, LoginProvider, SubscriptionInfo } from "@shared/gfn"; + +import { buildGfnLcarsHeaders } from "../clientHeaders"; +import { fetchDynamicRegions, fetchSubscription } from "../subscription"; + +interface ServerInfoResponse { + requestStatus?: { + serverId?: string; + }; +} + +interface EnrichmentCacheDependencies { + getSession: () => AuthSession | null; + getSelectedProvider: () => LoginProvider; + ensureValidSession: () => Promise; + updateSession: (session: AuthSession) => void; +} + +export class SubscriptionVpcEnrichmentCaches { + private cachedSubscription: SubscriptionInfo | null = null; + private cachedVpcId: string | null = null; + + constructor(private readonly dependencies: EnrichmentCacheDependencies) {} + + async getSubscription(): Promise { + if (this.cachedSubscription) { + return this.cachedSubscription; + } + + const session = await this.dependencies.ensureValidSession(); + if (!session) { + return null; + } + + const token = session.tokens.idToken ?? session.tokens.accessToken; + const { vpcId } = await fetchDynamicRegions(token, session.provider.streamingServiceUrl); + const subscription = await fetchSubscription( + token, + session.user.userId, + vpcId ?? undefined, + ); + this.cachedSubscription = subscription; + return subscription; + } + + clearSubscription(): void { + this.cachedSubscription = null; + } + + getCachedSubscription(): SubscriptionInfo | null { + return this.cachedSubscription; + } + + async getVpcId(explicitToken?: string): Promise { + if (this.cachedVpcId) { + return this.cachedVpcId; + } + + const provider = this.dependencies.getSelectedProvider(); + const base = provider.streamingServiceUrl.endsWith("/") + ? provider.streamingServiceUrl + : `${provider.streamingServiceUrl}/`; + + let token = explicitToken; + if (!token) { + const session = await this.dependencies.ensureValidSession(); + token = session ? session.tokens.idToken ?? session.tokens.accessToken : undefined; + } + + const headers = buildGfnLcarsHeaders({ + token, + clientType: "BROWSER", + clientStreamer: "WEBRTC", + includeUserAgent: true, + }); + + try { + const response = await fetch(`${base}v2/serverInfo`, { headers }); + if (!response.ok) { + return null; + } + + const payload = (await response.json()) as ServerInfoResponse; + const vpcId = payload.requestStatus?.serverId ?? null; + if (vpcId) { + this.cachedVpcId = vpcId; + } + return vpcId; + } catch { + return null; + } + } + + clearVpc(): void { + this.cachedVpcId = null; + } + + getCachedVpcId(): string | null { + return this.cachedVpcId; + } + + clearAll(): void { + this.clearSubscription(); + this.clearVpc(); + } + + async enrichUserTier(): Promise { + const session = this.dependencies.getSession(); + if (!session) { + return; + } + + try { + const subscription = await this.getSubscription(); + if (subscription?.membershipTier) { + this.dependencies.updateSession({ + ...session, + user: { + ...session.user, + membershipTier: subscription.membershipTier, + }, + }); + console.log(`Resolved membership tier: ${subscription.membershipTier}`); + } + } catch (error) { + console.warn("Failed to fetch subscription tier, keeping fallback:", error); + } + } +} diff --git a/opennow-stable/src/main/platforms/gfn/auth/helpers.ts b/opennow-stable/src/main/platforms/gfn/auth/helpers.ts new file mode 100644 index 000000000..a0e72cc8d --- /dev/null +++ b/opennow-stable/src/main/platforms/gfn/auth/helpers.ts @@ -0,0 +1,63 @@ +import { createHash } from "node:crypto"; +import os from "node:os"; + +import { + buildNvidiaAuthHeaders, + GFN_PLAY_ORIGIN, + GFN_PLAY_REFERER, +} from "../clientHeaders"; +import { CLIENT_ID, STEAM_DECK_CLIENT_ID, STEAM_DECK_USER_AGENT } from "./constants"; + +export function toExpiresAt(expiresInSeconds: number | undefined, defaultSeconds = 86400): number { + return Date.now() + (expiresInSeconds ?? defaultSeconds) * 1000; +} + +export function isExpired(expiresAt: number | undefined): boolean { + if (!expiresAt) { + return true; + } + return expiresAt <= Date.now(); +} + +export function isNearExpiry(expiresAt: number | undefined, windowMs: number): boolean { + if (!expiresAt) { + return true; + } + return expiresAt - Date.now() < windowMs; +} + +export function generateDeviceId(): string { + const host = os.hostname(); + const username = os.userInfo().username; + return createHash("sha256").update(`${host}:${username}:opennow-stable`).digest("hex"); +} + +export function buildAuthHeadersForClient( + authClientId = CLIENT_ID, + options: { + bearerToken?: string; + accept?: string; + contentType?: string; + includeReferer?: boolean; + } = {}, +): Record { + if (authClientId !== STEAM_DECK_CLIENT_ID) { + return buildNvidiaAuthHeaders(options); + } + + const headers: Record = { + Accept: options.accept ?? "application/json, text/plain, */*", + Origin: GFN_PLAY_ORIGIN, + Referer: GFN_PLAY_REFERER, + "User-Agent": STEAM_DECK_USER_AGENT, + }; + + if (options.bearerToken !== undefined) { + headers.Authorization = `Bearer ${options.bearerToken}`; + } + if (options.contentType) { + headers["Content-Type"] = options.contentType; + } + + return headers; +} diff --git a/opennow-stable/src/main/platforms/gfn/auth/oauthFlow.ts b/opennow-stable/src/main/platforms/gfn/auth/oauthFlow.ts new file mode 100644 index 000000000..46677b2fc --- /dev/null +++ b/opennow-stable/src/main/platforms/gfn/auth/oauthFlow.ts @@ -0,0 +1,152 @@ +import { createServer } from "node:http"; +import { createHash, randomBytes } from "node:crypto"; +import net from "node:net"; + +import type { AuthTokens, LoginProvider } from "@shared/gfn"; + +import { + AUTH_ENDPOINT, + CLIENT_ID, + REDIRECT_PORTS, + SCOPES, + TOKEN_ENDPOINT, +} from "./constants"; +import { buildAuthHeadersForClient, generateDeviceId, toExpiresAt } from "./helpers"; + +interface TokenResponse { + access_token: string; + refresh_token?: string; + id_token?: string; + client_token?: string; + expires_in?: number; +} + +export function generatePkce(): { verifier: string; challenge: string } { + const verifier = randomBytes(64) + .toString("base64") + .replace(/\+/g, "-") + .replace(/\//g, "_") + .replace(/=+$/g, "") + .slice(0, 86); + + const challenge = createHash("sha256") + .update(verifier) + .digest("base64") + .replace(/\+/g, "-") + .replace(/\//g, "_") + .replace(/=+$/g, ""); + + return { verifier, challenge }; +} + +export function buildAuthUrl(provider: LoginProvider, challenge: string, port: number): string { + const redirectUri = `http://localhost:${port}`; + const nonce = randomBytes(16).toString("hex"); + const params = new URLSearchParams({ + response_type: "code", + device_id: generateDeviceId(), + scope: SCOPES, + client_id: CLIENT_ID, + redirect_uri: redirectUri, + ui_locales: "en_US", + nonce, + prompt: "select_account", + code_challenge: challenge, + code_challenge_method: "S256", + idp_id: provider.idpId, + }); + return `${AUTH_ENDPOINT}?${params.toString()}`; +} + +async function isPortAvailable(port: number): Promise { + return new Promise((resolve) => { + const server = net.createServer(); + server.once("error", () => resolve(false)); + server.once("listening", () => { + server.close(() => resolve(true)); + }); + server.listen(port, "127.0.0.1"); + }); +} + +export async function findAvailablePort(): Promise { + for (const port of REDIRECT_PORTS) { + if (await isPortAvailable(port)) { + return port; + } + } + + throw new Error("No available OAuth callback ports"); +} + +export async function waitForAuthorizationCode(port: number, timeoutMs: number): Promise { + return new Promise((resolve, reject) => { + const server = createServer((request, response) => { + const url = new URL(request.url ?? "/", `http://localhost:${port}`); + const code = url.searchParams.get("code"); + const error = url.searchParams.get("error"); + + const html = `OpenNOW Login

OpenNOW Login

${ + code + ? "Login complete. You can close this window and return to OpenNOW Stable." + : "Login failed or was cancelled. You can close this window and return to OpenNOW Stable." + }

`; + + response.statusCode = 200; + response.setHeader("Content-Type", "text/html; charset=utf-8"); + response.end(html); + + server.close(() => { + if (code) { + resolve(code); + return; + } + reject(new Error(error ?? "Authorization failed")); + }); + }); + + server.listen(port, "127.0.0.1", () => { + const timer = setTimeout(() => { + server.close(() => reject(new Error("Timed out waiting for OAuth callback"))); + }, timeoutMs); + + server.once("close", () => clearTimeout(timer)); + }); + }); +} + +export async function exchangeAuthorizationCode( + code: string, + verifier: string, + port: number, +): Promise { + const body = new URLSearchParams({ + grant_type: "authorization_code", + code, + redirect_uri: `http://localhost:${port}`, + code_verifier: verifier, + }); + + const response = await fetch(TOKEN_ENDPOINT, { + method: "POST", + headers: buildAuthHeadersForClient(CLIENT_ID, { + contentType: "application/x-www-form-urlencoded; charset=UTF-8", + includeReferer: true, + }), + body, + }); + + if (!response.ok) { + const text = await response.text(); + throw new Error(`Token exchange failed (${response.status}): ${text.slice(0, 400)}`); + } + + const payload = (await response.json()) as TokenResponse; + return { + accessToken: payload.access_token, + refreshToken: payload.refresh_token, + idToken: payload.id_token, + expiresAt: toExpiresAt(payload.expires_in), + authClientId: CLIENT_ID, + }; +} diff --git a/opennow-stable/src/main/platforms/gfn/auth/persistedAccountState.test.ts b/opennow-stable/src/main/platforms/gfn/auth/persistedAccountState.test.ts new file mode 100644 index 000000000..9bdb651bb --- /dev/null +++ b/opennow-stable/src/main/platforms/gfn/auth/persistedAccountState.test.ts @@ -0,0 +1,74 @@ +/// + +import assert from "node:assert/strict"; +import test from "node:test"; + +import type { AuthSession, LoginProvider } from "@shared/gfn"; + +import { AccountState } from "./persistedAccountState"; + +function provider(code: string, streamingServiceUrl = `https://${code.toLowerCase()}.example`): LoginProvider { + return { + idpId: `${code}-idp`, + code, + displayName: code, + streamingServiceUrl, + priority: 0, + }; +} + +function session(userId: string, loginProvider = provider("NVIDIA")): AuthSession { + return { + provider: loginProvider, + tokens: { + accessToken: `${userId}-access`, + refreshToken: `${userId}-refresh`, + expiresAt: Date.now() + 60_000, + }, + user: { + userId, + displayName: userId, + membershipTier: "FREE", + }, + }; +} + +test("AccountState restores the legacy single-session format without changing identity", () => { + const state = new AccountState(); + state.restore({ session: session("legacy-user") }); + + assert.equal(state.getSession()?.user.userId, "legacy-user"); + assert.equal(state.getSession()?.provider.streamingServiceUrl, "https://nvidia.example/"); + assert.equal(state.snapshot().activeUserId, "legacy-user"); + assert.equal(state.snapshot().sessions.length, 1); +}); + +test("AccountState falls back to the first saved account when activeUserId is stale", () => { + const state = new AccountState(); + state.restore({ + sessions: [session("first"), session("second", provider("BPC"))], + activeUserId: "removed-user", + selectedProvider: provider("NVIDIA"), + }); + + assert.equal(state.getSession()?.user.userId, "first"); + state.setActiveAccount("second"); + assert.equal(state.getSession()?.user.userId, "second"); + assert.equal(state.getSelectedProvider().code, "BPC"); +}); + +test("AccountState preserves insertion order while removing and replacing accounts", () => { + const state = new AccountState(); + state.setSession(session("first")); + state.setSession(session("second", provider("BPC"))); + + assert.equal(state.firstUserId(), "first"); + assert.equal(state.removeAccount("second"), true); + state.setActiveAccount(state.firstUserId()); + + assert.equal(state.getSession()?.user.userId, "first"); + assert.deepEqual( + state.getSavedAccounts().map((account) => account.userId), + ["first"], + ); +}); diff --git a/opennow-stable/src/main/platforms/gfn/auth/persistedAccountState.ts b/opennow-stable/src/main/platforms/gfn/auth/persistedAccountState.ts new file mode 100644 index 000000000..af3ecc295 --- /dev/null +++ b/opennow-stable/src/main/platforms/gfn/auth/persistedAccountState.ts @@ -0,0 +1,169 @@ +import { access, mkdir, readFile, writeFile } from "node:fs/promises"; +import { dirname } from "node:path"; + +import type { AuthSession, LoginProvider, SavedAccount } from "@shared/gfn"; + +import { defaultProvider, normalizeProvider } from "./providerDiscovery"; + +export interface PersistedAuthState { + sessions: AuthSession[]; + activeUserId: string | null; + selectedProvider: LoginProvider | null; +} + +export type RestorableAuthState = Partial & { + session?: AuthSession | null; +}; + +export class AccountState { + private sessions = new Map(); + private activeUserId: string | null = null; + private selectedProvider: LoginProvider = defaultProvider(); + + restore(parsed: RestorableAuthState): void { + if (parsed.selectedProvider) { + this.selectedProvider = normalizeProvider(parsed.selectedProvider); + } + + this.sessions.clear(); + if (Array.isArray(parsed.sessions)) { + for (const persistedSession of parsed.sessions) { + if (!persistedSession?.user?.userId) { + continue; + } + this.sessions.set(persistedSession.user.userId, { + ...persistedSession, + provider: normalizeProvider(persistedSession.provider), + }); + } + } else if (parsed.session?.user?.userId) { + this.sessions.set(parsed.session.user.userId, { + ...parsed.session, + provider: normalizeProvider(parsed.session.provider), + }); + } + + this.activeUserId = + typeof parsed.activeUserId === "string" && this.sessions.has(parsed.activeUserId) + ? parsed.activeUserId + : this.sessions.keys().next().value ?? null; + } + + snapshot(): PersistedAuthState { + return { + sessions: Array.from(this.sessions.values()), + activeUserId: this.activeUserId, + selectedProvider: this.selectedProvider, + }; + } + + reset(): void { + this.sessions.clear(); + this.activeUserId = null; + this.selectedProvider = defaultProvider(); + } + + getSession(): AuthSession | null { + if (!this.activeUserId) { + return null; + } + return this.sessions.get(this.activeUserId) ?? null; + } + + getSessionForUser(userId: string): AuthSession | null { + return this.sessions.get(userId) ?? null; + } + + hasAccount(userId: string): boolean { + return this.sessions.has(userId); + } + + firstUserId(): string | null { + return this.sessions.keys().next().value ?? null; + } + + getActiveUserId(): string | null { + return this.activeUserId; + } + + getSelectedProvider(): LoginProvider { + return this.getSession()?.provider ?? this.selectedProvider; + } + + getPersistedSelectedProvider(): LoginProvider { + return this.selectedProvider; + } + + setSelectedProvider(provider: LoginProvider): void { + this.selectedProvider = normalizeProvider(provider); + } + + setSession(session: AuthSession): AuthSession { + const normalized: AuthSession = { + ...session, + provider: normalizeProvider(session.provider), + }; + this.sessions.set(normalized.user.userId, normalized); + this.activeUserId = normalized.user.userId; + this.selectedProvider = normalized.provider; + return normalized; + } + + updateSession(session: AuthSession): void { + this.sessions.set(session.user.userId, session); + } + + setActiveAccount(userId: string | null): void { + this.activeUserId = userId && this.sessions.has(userId) ? userId : null; + this.selectedProvider = this.getSession()?.provider ?? defaultProvider(); + } + + removeAccount(userId: string): boolean { + return this.sessions.delete(userId); + } + + getSavedAccounts(): SavedAccount[] { + return Array.from(this.sessions.values()).map((session) => ({ + userId: session.user.userId, + displayName: session.user.displayName, + email: session.user.email, + avatarUrl: session.user.avatarUrl, + membershipTier: session.user.membershipTier, + providerCode: session.provider.code, + })); + } +} + +export class PersistedAccountState { + readonly accounts = new AccountState(); + + constructor(private readonly statePath: string) {} + + async initialize(): Promise { + try { + await access(this.statePath); + } catch { + await this.persist(); + return null; + } + + try { + const raw = await readFile(this.statePath, "utf8"); + this.accounts.restore(JSON.parse(raw) as RestorableAuthState); + return this.accounts.getSession(); + } catch { + this.accounts.reset(); + await this.persist(); + return null; + } + } + + async persist(): Promise { + await mkdir(dirname(this.statePath), { recursive: true }); + await writeFile( + this.statePath, + JSON.stringify(this.accounts.snapshot(), null, 2), + "utf8", + ); + } +} diff --git a/opennow-stable/src/main/platforms/gfn/auth/providerDiscovery.ts b/opennow-stable/src/main/platforms/gfn/auth/providerDiscovery.ts new file mode 100644 index 000000000..daefd6cee --- /dev/null +++ b/opennow-stable/src/main/platforms/gfn/auth/providerDiscovery.ts @@ -0,0 +1,102 @@ +import type { LoginProvider } from "@shared/gfn"; + +import { GFN_USER_AGENT } from "../clientHeaders"; +import { DEFAULT_IDP_ID, SERVICE_URLS_ENDPOINT } from "./constants"; + +interface ServiceUrlsResponse { + gfnServiceInfo?: { + gfnServiceEndpoints?: Array<{ + idpId: string; + loginProviderCode: string; + loginProviderDisplayName: string; + streamingServiceUrl: string; + loginProviderPriority?: number; + }>; + }; +} + +export function defaultProvider(): LoginProvider { + return { + idpId: DEFAULT_IDP_ID, + code: "NVIDIA", + displayName: "NVIDIA", + streamingServiceUrl: "https://prod.cloudmatchbeta.nvidiagrid.net/", + priority: 0, + }; +} + +export function normalizeProvider(provider: LoginProvider): LoginProvider { + return { + ...provider, + streamingServiceUrl: provider.streamingServiceUrl.endsWith("/") + ? provider.streamingServiceUrl + : `${provider.streamingServiceUrl}/`, + }; +} + +export class ProviderDiscovery { + private providers: LoginProvider[] = []; + + async getProviders(): Promise { + if (this.providers.length > 0) { + return this.providers; + } + + let response: Response; + try { + response = await fetch(SERVICE_URLS_ENDPOINT, { + headers: { + Accept: "application/json", + "User-Agent": GFN_USER_AGENT, + }, + }); + } catch (error) { + console.warn("Failed to fetch providers, using default:", error); + this.providers = [defaultProvider()]; + return this.providers; + } + + if (!response.ok) { + console.warn(`Providers fetch failed with status ${response.status}, using default`); + this.providers = [defaultProvider()]; + return this.providers; + } + + try { + const payload = (await response.json()) as ServiceUrlsResponse; + const endpoints = payload.gfnServiceInfo?.gfnServiceEndpoints ?? []; + const providers = endpoints + .map((entry) => ({ + idpId: entry.idpId, + code: entry.loginProviderCode, + displayName: + entry.loginProviderCode === "BPC" ? "bro.game" : entry.loginProviderDisplayName, + streamingServiceUrl: entry.streamingServiceUrl, + priority: entry.loginProviderPriority ?? 0, + })) + .sort((a, b) => a.priority - b.priority) + .map(normalizeProvider); + + this.providers = providers.length > 0 ? providers : [defaultProvider()]; + console.log(`Loaded ${this.providers.length} providers`); + return this.providers; + } catch (error) { + console.warn("Failed to parse providers response, using default:", error); + this.providers = [defaultProvider()]; + return this.providers; + } + } + + async selectProvider( + selectedProvider: LoginProvider, + providerIdpId?: string, + ): Promise { + const providers = await this.getProviders(); + const selected = + providers.find((provider) => provider.idpId === providerIdpId) ?? + selectedProvider ?? + providers[0] ?? + defaultProvider(); + return normalizeProvider(selected); + } +} diff --git a/opennow-stable/src/main/platforms/gfn/auth/sessionValidity.test.ts b/opennow-stable/src/main/platforms/gfn/auth/sessionValidity.test.ts new file mode 100644 index 000000000..de856b620 --- /dev/null +++ b/opennow-stable/src/main/platforms/gfn/auth/sessionValidity.test.ts @@ -0,0 +1,30 @@ +/// + +import assert from "node:assert/strict"; +import test from "node:test"; + +import { shouldRefreshSession } from "./sessionValidity"; + +test("shouldRefreshSession only coordinates refresh inside the token refresh window", () => { + assert.equal( + shouldRefreshSession({ + accessToken: "valid", + expiresAt: Date.now() + 60 * 60 * 1000, + }), + false, + ); + assert.equal( + shouldRefreshSession({ + accessToken: "near-expiry", + expiresAt: Date.now() + 30_000, + }), + true, + ); + assert.equal( + shouldRefreshSession({ + accessToken: "expired", + expiresAt: Date.now() - 1, + }), + true, + ); +}); diff --git a/opennow-stable/src/main/platforms/gfn/auth/sessionValidity.ts b/opennow-stable/src/main/platforms/gfn/auth/sessionValidity.ts new file mode 100644 index 000000000..128b8295b --- /dev/null +++ b/opennow-stable/src/main/platforms/gfn/auth/sessionValidity.ts @@ -0,0 +1,259 @@ +import type { + AuthSession, + AuthSessionResult, + AuthTokens, + AuthUser, +} from "@shared/gfn"; + +import { CLIENT_TOKEN_REFRESH_WINDOW_MS, TOKEN_REFRESH_WINDOW_MS } from "./constants"; +import type { SubscriptionVpcEnrichmentCaches } from "./enrichmentCaches"; +import { isExpired, isNearExpiry } from "./helpers"; +import type { PersistedAccountState } from "./persistedAccountState"; +import { + mergeTokenSnapshot, + refreshAuthTokens, + refreshWithClientToken, + requestClientToken, +} from "./tokenRefresh"; +import { fetchUserInfo } from "./userInfo"; + +interface SessionValidityDependencies { + state: PersistedAccountState; + enrichmentCaches: SubscriptionVpcEnrichmentCaches; + logout: () => Promise; +} + +export function shouldRefreshSession(tokens: AuthTokens): boolean { + return isNearExpiry(tokens.expiresAt, TOKEN_REFRESH_WINDOW_MS); +} + +export class SessionValidityCoordinator { + constructor(private readonly dependencies: SessionValidityDependencies) {} + + async ensureClientToken(tokens: AuthTokens): Promise { + const hasUsableClientToken = + Boolean(tokens.clientToken) && + !isNearExpiry(tokens.clientTokenExpiresAt, CLIENT_TOKEN_REFRESH_WINDOW_MS); + if (hasUsableClientToken || isExpired(tokens.expiresAt)) { + return tokens; + } + + const clientToken = await requestClientToken(tokens.accessToken, tokens.authClientId); + return { + ...tokens, + clientToken: clientToken.token, + clientTokenExpiresAt: clientToken.expiresAt, + clientTokenLifetimeMs: clientToken.lifetimeMs, + }; + } + + async ensureValidSessionWithStatus( + forceRefresh = false, + expectedUserId?: string, + ): Promise { + const { accounts } = this.dependencies.state; + const currentSession = accounts.getSession(); + if (!currentSession) { + return { + session: null, + refresh: { + attempted: false, + forced: forceRefresh, + outcome: "not_attempted", + message: "No saved session found.", + }, + }; + } + + const userId = currentSession.user.userId; + let tokens = currentSession.tokens; + + if (!tokens.clientToken && !isExpired(tokens.expiresAt)) { + try { + const withClientToken = await this.ensureClientToken(tokens); + if (withClientToken.clientToken && withClientToken.clientToken !== tokens.clientToken) { + accounts.updateSession({ + ...currentSession, + tokens: withClientToken, + }); + tokens = withClientToken; + await this.dependencies.state.persist(); + } + } catch (error) { + console.warn("Unable to bootstrap client token from saved session:", error); + } + } + + if (!forceRefresh && !shouldRefreshSession(tokens)) { + return { + session: accounts.getSession(), + refresh: { + attempted: false, + forced: forceRefresh, + outcome: "not_attempted", + message: "Session token is still valid.", + }, + }; + } + + const applyRefreshedTokens = async ( + refreshedTokens: AuthTokens, + source: "client_token" | "refresh_token", + ): Promise => { + const latestSession = accounts.getSession() ?? currentSession; + const baseSession = latestSession.user.userId === userId ? latestSession : currentSession; + const expectedRefreshUserId = expectedUserId ?? userId; + let refreshedUser: AuthUser | null = null; + let userInfoError: string | undefined; + try { + refreshedUser = await fetchUserInfo(refreshedTokens); + console.debug("auth: fetched user info on token refresh", { + userId: refreshedUser.userId, + email: refreshedUser.email, + avatarUrl: refreshedUser.avatarUrl, + }); + } catch (error) { + console.warn("Token refresh succeeded but user info refresh failed. Keeping cached user:", error); + userInfoError = error instanceof Error ? error.message : "Unknown error while fetching user info"; + } + + const resolvedUser = refreshedUser ?? baseSession.user; + if (resolvedUser.userId !== expectedRefreshUserId) { + return { + session: baseSession, + refresh: { + attempted: true, + forced: forceRefresh, + outcome: "failed", + message: refreshedUser + ? "Token refresh returned a different account than expected." + : "Token refresh kept a cached account identity that did not match the expected account.", + error: refreshedUser + ? `expected_user_id:${expectedRefreshUserId} actual_user_id:${refreshedUser.userId}` + : userInfoError + ? `expected_user_id:${expectedRefreshUserId} cached_user_id:${resolvedUser.userId} user_info_error:${userInfoError}` + : `expected_user_id:${expectedRefreshUserId} cached_user_id:${resolvedUser.userId}`, + }, + }; + } + + accounts.updateSession({ + provider: baseSession.provider, + tokens: refreshedTokens, + user: resolvedUser, + }); + this.dependencies.enrichmentCaches.clearSubscription(); + await this.dependencies.enrichmentCaches.enrichUserTier(); + await this.dependencies.state.persist(); + + const sourceText = source === "client_token" ? "client token" : "refresh token"; + return { + session: accounts.getSession(), + refresh: { + attempted: true, + forced: forceRefresh, + outcome: "refreshed", + message: forceRefresh + ? `Saved session token refreshed via ${sourceText}.` + : `Session token refreshed via ${sourceText} because it was near expiry.`, + }, + }; + }; + + const refreshErrors: string[] = []; + if (tokens.clientToken) { + try { + const refreshedFromClientToken = await refreshWithClientToken( + tokens.clientToken, + userId, + tokens.authClientId, + ); + let refreshedTokens = mergeTokenSnapshot(tokens, refreshedFromClientToken); + refreshedTokens = await this.ensureClientToken(refreshedTokens); + return applyRefreshedTokens(refreshedTokens, "client_token"); + } catch (error) { + const message = + error instanceof Error ? error.message : "Unknown error while refreshing with client token"; + refreshErrors.push(`client_token: ${message}`); + } + } + + if (tokens.refreshToken) { + try { + const refreshedOAuth = await refreshAuthTokens(tokens.refreshToken, tokens.authClientId); + let refreshedTokens: AuthTokens = { + ...tokens, + ...refreshedOAuth, + idToken: refreshedOAuth.idToken ?? tokens.idToken, + clientToken: tokens.clientToken, + clientTokenExpiresAt: tokens.clientTokenExpiresAt, + clientTokenLifetimeMs: tokens.clientTokenLifetimeMs, + authClientId: refreshedOAuth.authClientId ?? tokens.authClientId, + }; + refreshedTokens = await this.ensureClientToken(refreshedTokens); + return applyRefreshedTokens(refreshedTokens, "refresh_token"); + } catch (error) { + const message = + error instanceof Error ? error.message : "Unknown error while refreshing token"; + refreshErrors.push(`refresh_token: ${message}`); + } + } + + const errorText = refreshErrors.length > 0 ? refreshErrors.join(" | ") : undefined; + const expired = isExpired(tokens.expiresAt); + if (!tokens.clientToken && !tokens.refreshToken) { + if (expired) { + await this.dependencies.logout(); + return { + session: null, + refresh: { + attempted: true, + forced: forceRefresh, + outcome: "missing_refresh_token", + message: "Saved session expired and has no refresh mechanism. Please log in again.", + }, + }; + } + + return { + session: accounts.getSession(), + refresh: { + attempted: true, + forced: forceRefresh, + outcome: "missing_refresh_token", + message: "No refresh token available. Using saved session token.", + }, + }; + } + + if (expired) { + await this.dependencies.logout(); + return { + session: null, + refresh: { + attempted: true, + forced: forceRefresh, + outcome: "failed", + message: "Token refresh failed and the saved session expired. Please log in again.", + error: errorText, + }, + }; + } + + return { + session: accounts.getSession(), + refresh: { + attempted: true, + forced: forceRefresh, + outcome: "failed", + message: "Token refresh failed. Using saved session token.", + error: errorText, + }, + }; + } + + async ensureValidSession(): Promise { + const result = await this.ensureValidSessionWithStatus(false); + return result.session; + } +} diff --git a/opennow-stable/src/main/platforms/gfn/auth/tokenRefresh.test.ts b/opennow-stable/src/main/platforms/gfn/auth/tokenRefresh.test.ts new file mode 100644 index 000000000..692732102 --- /dev/null +++ b/opennow-stable/src/main/platforms/gfn/auth/tokenRefresh.test.ts @@ -0,0 +1,81 @@ +/// + +import test from "node:test"; +import assert from "node:assert/strict"; + +import { mergeTokenSnapshot } from "./tokenRefresh"; + +test("mergeTokenSnapshot keeps the prior id_token when refresh omits it", () => { + const merged = mergeTokenSnapshot( + { + accessToken: "old-access", + refreshToken: "refresh", + idToken: "prior-id-token", + expiresAt: Date.now() + 60_000, + clientToken: "client", + clientTokenExpiresAt: Date.now() + 120_000, + clientTokenLifetimeMs: 120_000, + authClientId: "client-id", + }, + { + access_token: "new-access", + refresh_token: "refresh", + expires_in: 3600, + }, + ); + + assert.equal(merged.accessToken, "new-access"); + assert.equal(merged.idToken, "prior-id-token"); + assert.equal(merged.clientToken, "client"); + assert.equal(typeof merged.clientTokenExpiresAt, "number"); +}); + +test("mergeTokenSnapshot clears client-token expiry when client_token rotates", () => { + const merged = mergeTokenSnapshot( + { + accessToken: "old-access", + refreshToken: "refresh", + idToken: "prior-id-token", + expiresAt: Date.now() + 60_000, + clientToken: "old-client", + clientTokenExpiresAt: Date.now() + 120_000, + clientTokenLifetimeMs: 120_000, + }, + { + access_token: "new-access", + id_token: "new-id-token", + client_token: "new-client", + expires_in: 3600, + }, + ); + + assert.equal(merged.idToken, "new-id-token"); + assert.equal(merged.clientToken, "new-client"); + assert.equal(merged.clientTokenExpiresAt, undefined); + assert.equal(merged.clientTokenLifetimeMs, undefined); +}); + +test("mergeTokenSnapshot keeps client-token expiry when client_token is unchanged", () => { + const clientTokenExpiresAt = Date.now() + 120_000; + const merged = mergeTokenSnapshot( + { + accessToken: "old-access", + refreshToken: "refresh", + idToken: "prior-id-token", + expiresAt: Date.now() + 60_000, + clientToken: "same-client", + clientTokenExpiresAt, + clientTokenLifetimeMs: 120_000, + }, + { + access_token: "new-access", + client_token: "same-client", + expires_in: 3600, + }, + ); + + assert.equal(merged.clientToken, "same-client"); + assert.equal(merged.clientTokenExpiresAt, clientTokenExpiresAt); + assert.equal(merged.clientTokenLifetimeMs, 120_000); + assert.equal(merged.idToken, "prior-id-token"); +}); diff --git a/opennow-stable/src/main/platforms/gfn/auth/tokenRefresh.ts b/opennow-stable/src/main/platforms/gfn/auth/tokenRefresh.ts new file mode 100644 index 000000000..5ddd6fe93 --- /dev/null +++ b/opennow-stable/src/main/platforms/gfn/auth/tokenRefresh.ts @@ -0,0 +1,127 @@ +import type { AuthTokens } from "@shared/gfn"; + +import { CLIENT_ID, CLIENT_TOKEN_ENDPOINT, TOKEN_ENDPOINT } from "./constants"; +import { buildAuthHeadersForClient, toExpiresAt } from "./helpers"; + +export interface TokenResponse { + access_token: string; + refresh_token?: string; + id_token?: string; + client_token?: string; + expires_in?: number; +} + +interface ClientTokenResponse { + client_token: string; + expires_in?: number; +} + +export async function refreshAuthTokens( + refreshToken: string, + authClientId = CLIENT_ID, +): Promise { + const body = new URLSearchParams({ + grant_type: "refresh_token", + refresh_token: refreshToken, + client_id: authClientId, + }); + + const response = await fetch(TOKEN_ENDPOINT, { + method: "POST", + headers: buildAuthHeadersForClient(authClientId, { + contentType: "application/x-www-form-urlencoded; charset=UTF-8", + }), + body, + }); + + if (!response.ok) { + const text = await response.text(); + throw new Error(`Token refresh failed (${response.status}): ${text.slice(0, 400)}`); + } + + const payload = (await response.json()) as TokenResponse; + return { + accessToken: payload.access_token, + refreshToken: payload.refresh_token ?? refreshToken, + // Omit rather than set undefined so callers that spread onto prior tokens + // do not wipe a still-valid id_token when the refresh response excludes it. + ...(payload.id_token ? { idToken: payload.id_token } : {}), + expiresAt: toExpiresAt(payload.expires_in), + authClientId, + }; +} + +export async function requestClientToken( + accessToken: string, + authClientId = CLIENT_ID, +): Promise<{ + token: string; + expiresAt: number; + lifetimeMs: number; +}> { + const response = await fetch(CLIENT_TOKEN_ENDPOINT, { + headers: buildAuthHeadersForClient(authClientId, { bearerToken: accessToken }), + }); + + if (!response.ok) { + const text = await response.text(); + throw new Error(`Client token request failed (${response.status}): ${text.slice(0, 400)}`); + } + + const payload = (await response.json()) as ClientTokenResponse; + const expiresAt = toExpiresAt(payload.expires_in); + return { + token: payload.client_token, + expiresAt, + lifetimeMs: Math.max(0, expiresAt - Date.now()), + }; +} + +export async function refreshWithClientToken( + clientToken: string, + userId: string, + authClientId = CLIENT_ID, +): Promise { + const body = new URLSearchParams({ + grant_type: "urn:ietf:params:oauth:grant-type:client_token", + client_token: clientToken, + client_id: authClientId, + sub: userId, + }); + + const response = await fetch(TOKEN_ENDPOINT, { + method: "POST", + headers: buildAuthHeadersForClient(authClientId, { + contentType: "application/x-www-form-urlencoded; charset=UTF-8", + }), + body, + }); + + if (!response.ok) { + const text = await response.text(); + throw new Error(`Client-token refresh failed (${response.status}): ${text.slice(0, 400)}`); + } + + return (await response.json()) as TokenResponse; +} + +export function mergeTokenSnapshot(base: AuthTokens, refreshed: TokenResponse): AuthTokens { + const nextClientToken = refreshed.client_token ?? base.clientToken; + const clientTokenRotated = + typeof refreshed.client_token === "string" && + refreshed.client_token.length > 0 && + refreshed.client_token !== base.clientToken; + + return { + accessToken: refreshed.access_token, + refreshToken: refreshed.refresh_token ?? base.refreshToken, + // Refresh responses often omit id_token; keep the prior JWT for LCARS callers. + idToken: refreshed.id_token ?? base.idToken, + expiresAt: toExpiresAt(refreshed.expires_in), + authClientId: base.authClientId ?? CLIENT_ID, + clientToken: nextClientToken, + // Rotated client tokens must not inherit stale expiry metadata. + clientTokenExpiresAt: clientTokenRotated ? undefined : base.clientTokenExpiresAt, + clientTokenLifetimeMs: clientTokenRotated ? undefined : base.clientTokenLifetimeMs, + }; +} diff --git a/opennow-stable/src/main/platforms/gfn/auth/userInfo.ts b/opennow-stable/src/main/platforms/gfn/auth/userInfo.ts new file mode 100644 index 000000000..c75e66ba5 --- /dev/null +++ b/opennow-stable/src/main/platforms/gfn/auth/userInfo.ts @@ -0,0 +1,87 @@ +import { createHash } from "node:crypto"; + +import type { AuthTokens, AuthUser } from "@shared/gfn"; + +import { USERINFO_ENDPOINT } from "./constants"; +import { buildAuthHeadersForClient } from "./helpers"; + +function decodeBase64Url(value: string): string { + const normalized = value.replace(/-/g, "+").replace(/_/g, "/"); + const padding = normalized.length % 4; + const padded = padding === 0 ? normalized : `${normalized}${"=".repeat(4 - padding)}`; + return Buffer.from(padded, "base64").toString("utf8"); +} + +function parseJwtPayload(token: string): T | null { + const parts = token.split("."); + if (parts.length !== 3) { + return null; + } + try { + const payload = decodeBase64Url(parts[1]); + return JSON.parse(payload) as T; + } catch { + return null; + } +} + +export function gravatarUrl(email: string, size = 80): string { + const normalized = email.trim().toLowerCase(); + const hash = createHash("md5").update(normalized).digest("hex"); + return `https://www.gravatar.com/avatar/${hash}?s=${size}&d=identicon`; +} + +export async function fetchUserInfo(tokens: AuthTokens): Promise { + const jwtToken = tokens.idToken ?? tokens.accessToken; + const parsed = parseJwtPayload<{ + sub?: string; + email?: string; + preferred_username?: string; + gfn_tier?: string; + picture?: string; + }>(jwtToken); + + if (parsed?.sub) { + const emailFromToken = parsed.email; + const pictureFromToken = parsed.picture; + if (emailFromToken || pictureFromToken) { + const avatar = pictureFromToken ?? (emailFromToken ? gravatarUrl(emailFromToken) : undefined); + return { + userId: parsed.sub, + displayName: parsed.preferred_username ?? emailFromToken?.split("@")[0] ?? "User", + email: emailFromToken, + avatarUrl: avatar, + membershipTier: parsed.gfn_tier ?? "FREE", + }; + } + } + + const response = await fetch(USERINFO_ENDPOINT, { + headers: buildAuthHeadersForClient(tokens.authClientId, { + bearerToken: tokens.accessToken, + accept: "application/json", + }), + }); + + if (!response.ok) { + throw new Error(`User info failed (${response.status})`); + } + + const payload = (await response.json()) as { + sub: string; + preferred_username?: string; + email?: string; + picture?: string; + }; + + const email = payload.email; + const avatar = payload.picture ?? (email ? gravatarUrl(email) : undefined); + + return { + userId: payload.sub, + displayName: payload.preferred_username ?? email?.split("@")[0] ?? "User", + email, + avatarUrl: avatar, + membershipTier: "FREE", + }; +} diff --git a/opennow-stable/src/main/platforms/gfn/catalogBrowse.ts b/opennow-stable/src/main/platforms/gfn/catalogBrowse.ts new file mode 100644 index 000000000..2b80a9144 --- /dev/null +++ b/opennow-stable/src/main/platforms/gfn/catalogBrowse.ts @@ -0,0 +1,537 @@ +import type { + CatalogBrowseRequest, + CatalogBrowseResult, + CatalogFilterGroup, + CatalogSortOption, + GameInfo, + GamePanelResult, +} from "@shared/gfn"; +import { cacheManager } from "../../services/cacheManager"; +import { appendPublicGameSearchMatches, mergePublicGameVariants } from "./publicGames"; +import { fetchLcarsGraphQl } from "./lcarsGraphql"; +import { + accountScopedGamesCacheKey, + catalogBrowseCacheKey, + fetchPublicGames, + loadAccountScopedFromCache, + resolveAccountCacheId, + shouldBypassGamesCache, +} from "./gamesCache"; +import { + appToGame, + type AppData, + DEFAULT_LOCALE, + dedupeGames, + enrichGamesWithMetadata, + GFN_FEATURE_FIELDS, + type GraphQlResponse, + getVpcId, +} from "./gameAppMapper"; + +const DEFAULT_CATALOG_FETCH_COUNT = 120; +const MAX_CATALOG_PAGES = 3; +const DEFAULT_SORT_ID = "relevance"; + +interface FilterSortDefinitionsResponse { + data?: { + filterGroupDefinitions?: GraphQlFilterGroup[]; + sortOrderDefinitions?: Array<{ + id: string; + label: string; + orderBy: string; + }>; + }; + errors?: Array<{ message: string }>; +} + +interface AppsSearchResponse { + data?: { + apps?: { + numberReturned?: number; + numberSupported?: number; + pageInfo?: { + hasNextPage?: boolean; + endCursor?: string; + totalCount?: number; + }; + items?: AppData[]; + }; + }; + errors?: Array<{ message: string }>; +} + +interface GraphQlFilterGroup { + id: string; + label: string; + filters?: Array<{ + id: string; + label: string; + filters?: string[]; + }>; +} + +interface CatalogDefinitions { + filterGroups: CatalogFilterGroup[]; + sortOptions: CatalogSortOption[]; + filterPayloadById: Record; +} + +async function fetchPanels( + token: string, + panelNames: string[], + vpcId: string, + options?: { withLibraryTime?: boolean }, + proxyUrl?: string, +): Promise { + const queryName = panelNames.includes("MARQUEE") + ? "Marquee" + : panelNames.includes("LIBRARY") + ? options?.withLibraryTime === true ? "LibrarySectionWithTime" : "LibrarySection" + : "Main"; + + return await fetchLcarsGraphQl( + queryName, + { + vpcId, + locale: DEFAULT_LOCALE, + panelNames, + }, + token, + proxyUrl, + { context: "Games GraphQL failed" }, + ); +} + +function panelTextMatchesFeatured(value: string | undefined): boolean { + return value?.toLowerCase().includes("featured") ?? false; +} + +function getFeaturedGameIdentity(game: GameInfo): string { + return game.id || game.uuid || game.launchAppId || game.title; +} + +function featuredGamesFromPanels(payload: GraphQlResponse): GameInfo[] { + if (payload.errors?.length) { + throw new Error(payload.errors.map((error) => error.message).join(", ")); + } + + const explicitGames: GameInfo[] = []; + const explicitIds = new Set(); + const curatedGames: GameInfo[] = []; + const curatedIds = new Set(); + + const appendUnique = (target: GameInfo[], seen: Set, game: GameInfo): void => { + const identity = getFeaturedGameIdentity(game); + if (!identity || seen.has(identity)) return; + seen.add(identity); + target.push(game); + }; + + for (const panel of payload.data?.panels ?? []) { + const panelFeatured = panelTextMatchesFeatured(panel.name) || panelTextMatchesFeatured(panel.id); + for (const section of panel.sections ?? []) { + const sectionFeatured = panelFeatured || panelTextMatchesFeatured(section.title) || panelTextMatchesFeatured(section.id); + for (const item of section.items ?? []) { + if (item.__typename !== "GameItem" || !item.app) continue; + const game = appToGame(item.app); + if (!game.id || !game.title || game.variants.length === 0) continue; + appendUnique(curatedGames, curatedIds, game); + if (sectionFeatured) appendUnique(explicitGames, explicitIds, game); + } + } + } + + return explicitGames.length > 0 ? explicitGames : curatedGames; +} + +export function flattenPanels(payload: GraphQlResponse): GameInfo[] { + if (payload.errors?.length) { + throw new Error(payload.errors.map((error) => error.message).join(", ")); + } + + const games: GameInfo[] = []; + + for (const panel of payload.data?.panels ?? []) { + for (const section of panel.sections ?? []) { + for (const item of section.items ?? []) { + if (item.__typename === "GameItem" && item.app) { + games.push(appToGame(item.app)); + } + } + } + } + + return dedupeGames(games); +} + +function parsePanelResults(payload: GraphQlResponse): GamePanelResult[] { + if (payload.errors?.length) { + throw new Error(payload.errors.map((error) => error.message).join(", ")); + } + + const panels: GamePanelResult[] = []; + for (const panel of payload.data?.panels ?? []) { + const sections = (panel.sections ?? []) + .map((section) => ({ + id: section.id ?? section.title ?? "", + title: section.title ?? "", + games: (section.items ?? []) + .filter((item) => item.__typename === "GameItem" && item.app) + .map((item) => appToGame(item.app as AppData)) + .filter((game) => game.id && game.title && game.variants.length > 0), + })) + .filter((section) => section.games.length > 0); + + if (sections.length > 0) { + panels.push({ + id: panel.id ?? panel.name, + title: panel.name, + sections, + }); + } + } + return panels; +} + +async function fetchFilterAndSortDefinitions(token?: string, proxyUrl?: string): Promise { + const payload = await fetchLcarsGraphQl( + "FilterGroupAndSortOrderDefinitions", + { locale: DEFAULT_LOCALE }, + token, + proxyUrl, + ); + if (payload.errors?.length) { + throw new Error(payload.errors.map((error) => error.message).join(", ")); + } + + const filterPayloadById: Record = {}; + const filterGroups: CatalogFilterGroup[] = []; + + for (const group of payload.data?.filterGroupDefinitions ?? []) { + const options = (group.filters ?? []).flatMap((entry) => { + const filterJson = entry.filters?.[0]; + if (!filterJson) { + return []; + } + try { + filterPayloadById[entry.id] = JSON.parse(filterJson); + return [{ + id: entry.id, + rawId: entry.id, + label: entry.label, + groupId: group.id, + groupLabel: group.label, + }]; + } catch { + return []; + } + }); + + if (options.length > 0) { + filterGroups.push({ id: group.id, label: group.label, options }); + } + } + + const sortOptions = (payload.data?.sortOrderDefinitions ?? []).map((sort) => ({ + id: sort.id, + label: sort.label, + orderBy: sort.orderBy, + })); + + return { + filterGroups, + sortOptions, + filterPayloadById, + }; +} + +function mergeFilterPayloads(filterIds: string[], filterPayloadById: Record): Record { + const merged: Record = {}; + + for (const filterId of filterIds) { + const payload = filterPayloadById[filterId]; + if (!payload || typeof payload !== "object") { + continue; + } + Object.assign(merged, payload as Record); + } + + return merged; +} + +export async function browseCatalogUncached(input: CatalogBrowseRequest): Promise { + const token = input.token; + if (!token) { + throw new Error("Catalog browsing requires an authenticated token"); + } + + const vpcId = await getVpcId(token, input.providerStreamingBaseUrl, input.proxyUrl); + const definitions = await fetchFilterAndSortDefinitions(token, input.proxyUrl); + const normalizedFilterIds = (input.filterIds ?? []).filter((id) => id in definitions.filterPayloadById); + const selectedSort = definitions.sortOptions.find((option) => option.id === input.sortId) + ?? definitions.sortOptions.find((option) => option.id === DEFAULT_SORT_ID) + ?? definitions.sortOptions[0] + ?? { id: DEFAULT_SORT_ID, label: "Relevance", orderBy: "itemMetadata.relevance:DESC,sortName:ASC" }; + const searchQuery = input.searchQuery?.trim() ?? ""; + const fetchCount = Math.max(24, Math.min(input.fetchCount ?? DEFAULT_CATALOG_FETCH_COUNT, 200)); + const filters = mergeFilterPayloads(normalizedFilterIds, definitions.filterPayloadById); + + const appFields = ` + numberReturned + numberSupported + pageInfo { hasNextPage endCursor totalCount } + items { + id + title + images { KEY_ART KEY_IMAGE GAME_BOX_ART TV_BANNER HERO_IMAGE MARQUEE_HERO_IMAGE FEATURE_IMAGE GAME_LOGO SCREENSHOTS } + variants { + id + appStore + storeUrl + supportedControls + gfn { + status + features { +${GFN_FEATURE_FIELDS} + } + library { status selected } + } + } + gfn { + playabilityState + minimumMembershipTierLabel + catalogSkuStrings { + SKU_BASED_TAG + SKU_BASED_PLAYABILITY_TEXT + SKU_BASED_UNPLAYABLE_DIALOG_HEADER + SKU_BASED_UNPLAYABLE_DIALOG_BODY_UPGRADE + SKU_BASED_UNPLAYABLE_DIALOG_BODY_UPGRADE_ECOMM_RESTRICTED + } + } + itemMetadata { campaignIds } + } + `; + + const query = searchQuery.length > 0 + ? `query GetSearchFilterResults( + $vpcId: String!, + $locale: String!, + $sortString: String!, + $fetchCount: Int!, + $cursor: String!, + $searchString: String!, + $filters: AppFilterFields! + ) { + apps( + vpcId: $vpcId, + language: $locale, + orderBy: $sortString, + first: $fetchCount, + after: $cursor, + searchQuery: $searchString, + filters: $filters + ) { +${appFields} + } + }` + : `query GetFilterBrowseResults( + $vpcId: String!, + $locale: String!, + $sortString: String!, + $fetchCount: Int!, + $cursor: String!, + $filters: AppFilterFields! + ) { + apps( + vpcId: $vpcId, + language: $locale, + orderBy: $sortString, + first: $fetchCount, + after: $cursor, + filters: $filters + ) { +${appFields} + } + }`; + + const collectedApps: AppData[] = []; + let numberReturned = 0; + let numberSupported = 0; + let totalCount = 0; + let hasNextPage = false; + let endCursor = ""; + let cursor = ""; + + for (let page = 0; page < MAX_CATALOG_PAGES; page += 1) { + const variables = searchQuery.length > 0 + ? { + vpcId, + locale: DEFAULT_LOCALE, + sortString: selectedSort.orderBy, + fetchCount, + cursor, + searchString: searchQuery, + filters, + } + : { + vpcId, + locale: DEFAULT_LOCALE, + sortString: selectedSort.orderBy, + fetchCount, + cursor, + filters, + }; + const payload = await fetchLcarsGraphQl( + searchQuery.length > 0 ? "AppsWithSearch" : "AppsWithoutSearch", + variables, + token, + input.proxyUrl, + { + context: "GFN catalog query failed", + fallbackQuery: query, + }, + ); + + if (payload.errors?.length) { + throw new Error(payload.errors.map((error) => error.message).join(", ")); + } + + const apps = payload.data?.apps; + const items = apps?.items ?? []; + collectedApps.push(...items); + numberReturned += apps?.numberReturned ?? items.length; + numberSupported = apps?.numberSupported ?? numberSupported; + hasNextPage = apps?.pageInfo?.hasNextPage ?? false; + endCursor = apps?.pageInfo?.endCursor ?? ""; + totalCount = apps?.pageInfo?.totalCount ?? totalCount; + + if (!hasNextPage || !endCursor) { + break; + } + + cursor = endCursor; + } + + const games = dedupeGames(await enrichGamesWithMetadata(token, vpcId, collectedApps.map(appToGame), input.proxyUrl)); + const publicGames = await fetchPublicGames(input.proxyUrl); + const gamesWithPublicVariants = appendPublicGameSearchMatches( + mergePublicGameVariants(games, publicGames), + publicGames, + searchQuery, + ); + + return { + games: gamesWithPublicVariants, + numberReturned, + numberSupported: Math.max(numberSupported, gamesWithPublicVariants.length), + totalCount: Math.max(totalCount, gamesWithPublicVariants.length), + hasNextPage, + endCursor: endCursor || undefined, + searchQuery, + selectedSortId: selectedSort.id, + selectedFilterIds: normalizedFilterIds, + filterGroups: definitions.filterGroups, + sortOptions: definitions.sortOptions, + }; +} + +export async function browseCatalog(input: CatalogBrowseRequest): Promise { + const token = input.token; + if (!token) { + throw new Error("Catalog browsing requires an authenticated token"); + } + + const cached = await peekCachedBrowseCatalog(input); + if (cached) { + return cached; + } + + const result = await browseCatalogUncached(input); + const accountId = resolveAccountCacheId(input.userId, token); + if (!shouldBypassGamesCache(input.proxyUrl)) { + const cacheKey = catalogBrowseCacheKey(input, accountId); + await cacheManager.saveToCache(cacheKey, result); + } + return result; +} + +export async function peekCachedBrowseCatalog(input: CatalogBrowseRequest): Promise { + const token = input.token; + if (!token) { + return null; + } + if (shouldBypassGamesCache(input.proxyUrl)) { + return null; + } + + const accountId = resolveAccountCacheId(input.userId, token); + const cacheKey = catalogBrowseCacheKey(input, accountId); + const cached = await cacheManager.loadFromCache(cacheKey); + return cached?.data ?? null; +} + +export async function fetchMainGames( + token: string, + providerStreamingBaseUrl?: string, + accountId?: string, + proxyUrl?: string, +): Promise { + const cached = await loadAccountScopedFromCache("main", accountId, token, providerStreamingBaseUrl, proxyUrl); + if (cached) { + return mergePublicGameVariants(cached.data, await fetchPublicGames(proxyUrl)); + } + + const games = await fetchMainGamesUncached(token, providerStreamingBaseUrl, proxyUrl); + if (!shouldBypassGamesCache(proxyUrl)) { + const cacheKey = accountScopedGamesCacheKey("main", resolveAccountCacheId(accountId, token), providerStreamingBaseUrl, proxyUrl); + await cacheManager.saveToCache(cacheKey, games); + } + return games; +} + +export async function fetchFeaturedGames( + token: string, + providerStreamingBaseUrl?: string, + accountId?: string, + proxyUrl?: string, +): Promise { + const cached = await loadAccountScopedFromCache("featured", accountId, token, providerStreamingBaseUrl, proxyUrl); + if (cached) return cached.data; + + const vpcId = await getVpcId(token, providerStreamingBaseUrl, proxyUrl); + const games = featuredGamesFromPanels(await fetchPanels(token, ["MARQUEE"], vpcId, undefined, proxyUrl)).slice(0, 6); + + if (!shouldBypassGamesCache(proxyUrl)) { + const cacheKey = accountScopedGamesCacheKey("featured", resolveAccountCacheId(accountId, token), providerStreamingBaseUrl, proxyUrl); + await cacheManager.saveToCache(cacheKey, games); + } + return games; +} + +export async function fetchStorePanels( + token: string, + providerStreamingBaseUrl?: string, + accountId?: string, + proxyUrl?: string, +): Promise { + const cached = await loadAccountScopedFromCache("store-panels", accountId, token, providerStreamingBaseUrl, proxyUrl); + if (cached) return cached.data; + + const vpcId = await getVpcId(token, providerStreamingBaseUrl, proxyUrl); + const panels = parsePanelResults(await fetchPanels(token, ["MAIN"], vpcId, undefined, proxyUrl)); + if (!shouldBypassGamesCache(proxyUrl)) { + const cacheKey = accountScopedGamesCacheKey("store-panels", resolveAccountCacheId(accountId, token), providerStreamingBaseUrl, proxyUrl); + await cacheManager.saveToCache(cacheKey, panels); + } + return panels; +} + +export async function fetchMainGamesUncached(token: string, providerStreamingBaseUrl?: string, proxyUrl?: string): Promise { + const vpcId = await getVpcId(token, providerStreamingBaseUrl, proxyUrl); + const payload = await fetchPanels(token, ["MAIN"], vpcId, undefined, proxyUrl); + const games = flattenPanels(payload); + return mergePublicGameVariants(await enrichGamesWithMetadata(token, vpcId, games, proxyUrl), await fetchPublicGames(proxyUrl)); +} + +/** Shared by library fallback path. */ +export { fetchPanels }; diff --git a/opennow-stable/src/main/gfn/clientHeaders.ts b/opennow-stable/src/main/platforms/gfn/clientHeaders.ts similarity index 73% rename from opennow-stable/src/main/gfn/clientHeaders.ts rename to opennow-stable/src/main/platforms/gfn/clientHeaders.ts index c5a55a03e..4314e588e 100644 --- a/opennow-stable/src/main/gfn/clientHeaders.ts +++ b/opennow-stable/src/main/platforms/gfn/clientHeaders.ts @@ -1,5 +1,13 @@ import crypto from "node:crypto"; +import { GFN_PLAY_ORIGIN as SHARED_GFN_PLAY_ORIGIN, GFN_PLAY_REFERER as SHARED_GFN_PLAY_REFERER } from "@shared/gfn/endpoints"; + +import { + resolveGfnDeviceIdentity, + type GfnDeviceIdentity, + type GfnDeviceOs, +} from "./deviceIdentity"; + const GFN_WINDOWS_USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36 NVIDIACEFClient/HEAD/debb5919f6 GFN-PC/2.0.80.173"; const GFN_MACOS_USER_AGENT = @@ -9,14 +17,14 @@ export const GFN_USER_AGENT = process.platform === "darwin" ? GFN_MACOS_USER_AGE export const GFN_CLIENT_VERSION = "2.0.80.173"; export const LCARS_CLIENT_ID = "ec7e38d4-03af-4b58-b131-cfb0495903ab"; -export const GFN_PLAY_ORIGIN = "https://play.geforcenow.com"; -export const GFN_PLAY_REFERER = "https://play.geforcenow.com/"; +export const GFN_PLAY_ORIGIN = SHARED_GFN_PLAY_ORIGIN; +export const GFN_PLAY_REFERER = SHARED_GFN_PLAY_REFERER; export const NVIDIA_FILE_ORIGIN = "https://nvfile"; export const NVIDIA_FILE_REFERER = "https://nvfile/"; export type GfnClientStreamer = "NVIDIA-CLASSIC" | "WEBRTC"; export type GfnClientType = "NATIVE" | "BROWSER"; -export type GfnDeviceOs = "WINDOWS" | "MACOS" | "LINUX"; +export type { GfnDeviceOs }; export function gfnJwtAuthorization(token: string): string { return `GFNJWT ${token}`; @@ -27,13 +35,20 @@ export function bearerAuthorization(token: string): string { } export function platformToGfnDeviceOs(platform: NodeJS.Platform = process.platform): GfnDeviceOs { - if (platform === "win32") { - return "WINDOWS"; - } - if (platform === "darwin") { - return "MACOS"; + return resolveGfnDeviceIdentity({ identifyAsSteamDeck: false, platform }).deviceOs; +} + +function applyDeviceIdentityHeaders( + headers: Record, + identity: GfnDeviceIdentity, + options?: { includeMakeModel?: boolean }, +): void { + headers["nv-device-os"] = identity.deviceOs; + headers["nv-device-type"] = identity.deviceType; + if (options?.includeMakeModel !== false) { + headers["nv-device-make"] = identity.deviceMake; + headers["nv-device-model"] = identity.deviceModel; } - return "LINUX"; } export interface NvidiaAuthHeadersOptions { @@ -70,11 +85,13 @@ export interface GfnLcarsHeadersOptions { clientStreamer: GfnClientStreamer; accept?: string; deviceOs?: GfnDeviceOs; + identifyAsSteamDeck?: boolean; includeUserAgent?: boolean; includeEmptyTokenAuthorization?: boolean; } export function buildGfnLcarsHeaders(options: GfnLcarsHeadersOptions): Record { + const identity = resolveGfnDeviceIdentity({ identifyAsSteamDeck: options.identifyAsSteamDeck }); const headers: Record = { Accept: options.accept ?? "application/json", }; @@ -87,8 +104,10 @@ export function buildGfnLcarsHeaders(options: GfnLcarsHeadersOptions): Record { - return { +export function buildGfnGraphQlHeaders( + token?: string, + options?: { identifyAsSteamDeck?: boolean }, +): Record { + const identity = resolveGfnDeviceIdentity(options); + const headers: Record = { Accept: "application/json, text/plain, */*", "Content-Type": "application/json", Origin: GFN_PLAY_ORIGIN, @@ -108,13 +131,11 @@ export function buildGfnGraphQlHeaders(token?: string): Record { "nv-client-type": "NATIVE", "nv-client-version": GFN_CLIENT_VERSION, "nv-client-streamer": "NVIDIA-CLASSIC", - "nv-device-os": platformToGfnDeviceOs(), - "nv-device-type": "DESKTOP", - "nv-device-make": "UNKNOWN", - "nv-device-model": "UNKNOWN", "nv-browser-type": "CHROME", "User-Agent": GFN_USER_AGENT, }; + applyDeviceIdentityHeaders(headers, identity); + return headers; } export interface GfnCloudMatchHeadersOptions { @@ -122,6 +143,7 @@ export interface GfnCloudMatchHeadersOptions { clientId?: string; deviceId?: string; includeOrigin?: boolean; + identifyAsSteamDeck?: boolean; } function resolveCloudMatchIdentity(options: GfnCloudMatchHeadersOptions): { clientId: string; deviceId: string } { @@ -133,6 +155,7 @@ function resolveCloudMatchIdentity(options: GfnCloudMatchHeadersOptions): { clie export function buildGfnCloudMatchHeaders(options: GfnCloudMatchHeadersOptions): Record { const { clientId, deviceId } = resolveCloudMatchIdentity(options); + const identity = resolveGfnDeviceIdentity({ identifyAsSteamDeck: options.identifyAsSteamDeck }); const headers: Record = { "User-Agent": GFN_USER_AGENT, Authorization: gfnJwtAuthorization(options.token), @@ -142,12 +165,9 @@ export function buildGfnCloudMatchHeaders(options: GfnCloudMatchHeadersOptions): "nv-client-streamer": "NVIDIA-CLASSIC", "nv-client-type": "NATIVE", "nv-client-version": GFN_CLIENT_VERSION, - "nv-device-make": "UNKNOWN", - "nv-device-model": "UNKNOWN", - "nv-device-os": platformToGfnDeviceOs(), - "nv-device-type": "DESKTOP", "x-device-id": deviceId, }; + applyDeviceIdentityHeaders(headers, identity); if (options.includeOrigin !== false) { headers.Origin = GFN_PLAY_ORIGIN; @@ -159,8 +179,8 @@ export function buildGfnCloudMatchHeaders(options: GfnCloudMatchHeadersOptions): export function buildGfnCloudMatchClaimHeaders(options: GfnCloudMatchHeadersOptions): Record { const { clientId, deviceId } = resolveCloudMatchIdentity(options); - - return { + const identity = resolveGfnDeviceIdentity({ identifyAsSteamDeck: options.identifyAsSteamDeck }); + const headers: Record = { "User-Agent": GFN_USER_AGENT, Authorization: gfnJwtAuthorization(options.token), "Content-Type": "application/json", @@ -170,8 +190,8 @@ export function buildGfnCloudMatchClaimHeaders(options: GfnCloudMatchHeadersOpti "nv-client-streamer": "NVIDIA-CLASSIC", "nv-client-type": "NATIVE", "nv-client-version": GFN_CLIENT_VERSION, - "nv-device-os": platformToGfnDeviceOs(), - "nv-device-type": "DESKTOP", "x-device-id": deviceId, }; + applyDeviceIdentityHeaders(headers, identity); + return headers; } diff --git a/opennow-stable/src/main/gfn/cloudmatch.test.ts b/opennow-stable/src/main/platforms/gfn/cloudmatch.test.ts similarity index 95% rename from opennow-stable/src/main/gfn/cloudmatch.test.ts rename to opennow-stable/src/main/platforms/gfn/cloudmatch.test.ts index f1e75b5fa..4bf306035 100644 --- a/opennow-stable/src/main/gfn/cloudmatch.test.ts +++ b/opennow-stable/src/main/platforms/gfn/cloudmatch.test.ts @@ -143,17 +143,28 @@ test("CloudMatch resolves default prod endpoint to serverInfo local region befor const originalFetch = globalThis.fetch; const originalWarn = console.warn; const calls: string[] = []; + type CapturedNetworkTestRequestBody = { + netTestRequestData: { + netTestProfile: { + framesPerSecond: number; + }; + }; + }; type CapturedSessionRequestBody = { sessionRequestData: { networkTestSessionId?: string | null; appLaunchMode?: number; enablePersistingInGameSettings?: boolean; + clientRequestMonitorSettings: Array<{ + framesPerSecond: number; + }>; requestedStreamingFeatures: { bitDepth?: number; chromaFormat?: number; }; }; }; + let networkTestRequestBody: CapturedNetworkTestRequestBody | null = null; let requestBody: CapturedSessionRequestBody | null = null; const expectedSessionUrl = `https://np-lax-01.cloudmatchbeta.nvidiagrid.net/v2/session?${new URLSearchParams({ keyboardLayout: resolveGfnKeyboardLayout(DEFAULT_KEYBOARD_LAYOUT, process.platform), @@ -178,6 +189,7 @@ test("CloudMatch resolves default prod endpoint to serverInfo local region befor } if (url === "https://np-lax-01.cloudmatchbeta.nvidiagrid.net/v2/nettestsession") { + networkTestRequestBody = JSON.parse(String(init?.body)); return new Response(JSON.stringify({ requestStatus: { statusCode: 1, statusDescription: "SUCCESS_STATUS", serverId: "NP-LAX-01" }, netTestSession: { @@ -206,7 +218,7 @@ test("CloudMatch resolves default prod endpoint to serverInfo local region befor iceServers: [{ urls: "stun:127.0.0.1:19302" }], }, sessionRequestData: { - clientRequestMonitorSettings: [{ widthInPixels: 2560, heightInPixels: 1440, framesPerSecond: 240 }], + clientRequestMonitorSettings: [{ widthInPixels: 2560, heightInPixels: 1440, framesPerSecond: 90 }], requestedStreamingFeatures: createdRequestBody.sessionRequestData.requestedStreamingFeatures, enablePersistingInGameSettings: createdRequestBody.sessionRequestData.enablePersistingInGameSettings, }, @@ -225,7 +237,7 @@ test("CloudMatch resolves default prod endpoint to serverInfo local region befor internalTitle: "Test Game", accountLinked: true, zone: "prod", - settings: makeSettings({ colorQuality: "10bit_444", enableL4S: true, appLaunchMode: "gamepadFriendly" }), + settings: makeSettings({ fps: 90, colorQuality: "10bit_444", enableL4S: true, appLaunchMode: "gamepadFriendly" }), }); assert.equal(session.streamingBaseUrl, "https://np-lax-01.cloudmatchbeta.nvidiagrid.net"); @@ -235,8 +247,12 @@ test("CloudMatch resolves default prod endpoint to serverInfo local region befor "https://np-lax-01.cloudmatchbeta.nvidiagrid.net/v2/nettestsession", expectedSessionUrl, ]); + const capturedNetworkTestRequestBody = networkTestRequestBody as CapturedNetworkTestRequestBody | null; + assert.ok(capturedNetworkTestRequestBody); + assert.equal(capturedNetworkTestRequestBody.netTestRequestData.netTestProfile.framesPerSecond, 90); const capturedRequestBody = requestBody as CapturedSessionRequestBody | null; assert.ok(capturedRequestBody); + assert.equal(capturedRequestBody.sessionRequestData.clientRequestMonitorSettings[0]?.framesPerSecond, 90); assert.equal(capturedRequestBody.sessionRequestData.requestedStreamingFeatures.bitDepth, 1); assert.equal(capturedRequestBody.sessionRequestData.requestedStreamingFeatures.chromaFormat, 1); assert.equal(capturedRequestBody.sessionRequestData.appLaunchMode, 2); diff --git a/opennow-stable/src/main/platforms/gfn/cloudmatch.ts b/opennow-stable/src/main/platforms/gfn/cloudmatch.ts new file mode 100644 index 000000000..b1a2dd6f0 --- /dev/null +++ b/opennow-stable/src/main/platforms/gfn/cloudmatch.ts @@ -0,0 +1,667 @@ +import crypto from "node:crypto"; + +import type { + ActiveSessionInfo, + SessionAdAction, + SessionAdReportRequest, + SessionClaimRequest, + SessionCreateRequest, + SessionInfo, + SessionPollRequest, + SessionStopRequest, +} from "@shared/gfn"; + +import { + DEFAULT_KEYBOARD_LAYOUT, + resolveGfnKeyboardLayout, +} from "@shared/gfn"; + +import type { CloudMatchResponse, GetSessionsResponse } from "./types"; +import { SessionError } from "./errorCodes"; +import { + buildGfnCloudMatchClaimHeaders, + buildGfnCloudMatchHeaders, +} from "./clientHeaders"; +import { getStableDeviceId } from "./deviceId"; +import { + readCloudMatchJson, + throwIfCloudMatchResponseError, +} from "./request"; +import { appLaunchModeWireValue } from "./cloudmatchFeatures"; +import { + extractServerInfoRegionBases, + fetchCloudMatch, + formatErrorForLog, + isZoneHostname, + normalizeCloudMatchBaseUrl, + resolveCreateSessionBase, + resolvePollStopBase, + resolveStreamingBaseUrl, + type CloudMatchServerInfoResponse, +} from "./cloudmatchTransport"; +import { + isReadySessionStatus, + normalizeIceServers, + resolveSignaling, + streamingServerIp, +} from "./cloudmatchSignaling"; +import { + buildClaimRequestBody, + buildSessionRequestBody, + createNetworkTestSession, +} from "./cloudmatchSessionRequest"; +import { + echoedSessionAppLaunchMode, + extractAdState, + extractNegotiatedStreamProfile, + extractQueuePosition, + extractSessionQueuePosition, + extractSessionSeatSetupStep, + normalizeStreamingFeatures, + toSessionInfo, +} from "./cloudmatchSessionParsing"; + +export { + appLaunchModeWireValue, + buildRequestedStreamingFeatures, + shouldEnableInGameSettingsPersistence, + shouldRequestReflex, +} from "./cloudmatchFeatures"; +export { extractServerInfoRegionBases } from "./cloudmatchTransport"; + +const SESSION_MODIFY_ACTION_AD_UPDATE = 6; + +const AD_ACTION_CODES: Record = { + start: 1, + pause: 2, + resume: 3, + finish: 4, + cancel: 5, +}; + +export async function createSession(input: SessionCreateRequest): Promise { + if (!input.token) { + throw new Error("Missing token for session creation"); + } + + if (!/^\d+$/.test(input.appId)) { + throw new Error(`Invalid launch appId '${input.appId}' (must be numeric)`); + } + + // Generate client/device IDs once for the entire session lifecycle + const clientId = crypto.randomUUID(); + const deviceId = getStableDeviceId(); + + const requestedBase = resolveStreamingBaseUrl(input.zone, input.streamingBaseUrl); + const base = await resolveCreateSessionBase( + requestedBase, + input.token, + clientId, + deviceId, + input.proxyUrl, + ); + const networkTestSessionId = await createNetworkTestSession({ + base, + token: input.token, + clientId, + deviceId, + settings: input.settings, + proxyUrl: input.proxyUrl, + }); + const body = buildSessionRequestBody(input, deviceId, networkTestSessionId); + console.log( + `[CloudMatch] createSession in-game settings persistence: user=${input.enablePersistingInGameSettings === true}, ` + + `gameSupport=${input.supportsInGameSettingsPersistence === true}, ` + + `sent=${body.sessionRequestData.enablePersistingInGameSettings}, ` + + `networkTestSessionId=${networkTestSessionId ?? "none"}`, + ); + + const keyboardLayout = resolveGfnKeyboardLayout(input.settings.keyboardLayout ?? DEFAULT_KEYBOARD_LAYOUT, process.platform); + const languageCode = input.settings.gameLanguage ?? "en_US"; + const url = `${base}/v2/session?${new URLSearchParams({ keyboardLayout, languageCode }).toString()}`; + const response = await fetchCloudMatch(url, { + method: "POST", + headers: buildGfnCloudMatchHeaders({ token: input.token, clientId, deviceId, includeOrigin: true }), + body: JSON.stringify(body), + }, { proxyUrl: input.proxyUrl }); + + const { payload } = await readCloudMatchJson(response); + return await toSessionInfo({ + zone: input.zone, + streamingBaseUrl: base, + payload, + clientId, + deviceId, + fallbackAppId: input.appId, + fallbackAppLaunchMode: appLaunchModeWireValue(input.settings.appLaunchMode), + }); +} + +export async function pollSession(input: SessionPollRequest): Promise { + if (!input.token) { + throw new Error("Missing token for session polling"); + } + + // Use provided client/device IDs if available (should match session creation) + const clientId = input.clientId ?? crypto.randomUUID(); + const deviceId = input.deviceId ?? crypto.randomUUID(); + + const base = resolvePollStopBase(input.zone, input.streamingBaseUrl, input.serverIp); + const baseHost = new URL(base).hostname; + const pollProxyUrl = isZoneHostname(baseHost) ? input.proxyUrl : undefined; + const url = `${base}/v2/session/${input.sessionId}`; + // Polling should NOT include Origin/Referer headers (matches claimSession polling pattern) + const headers = buildGfnCloudMatchHeaders({ token: input.token, clientId, deviceId, includeOrigin: false }); + const response = await fetchCloudMatch(url, { + method: "GET", + headers, + }, { proxyUrl: pollProxyUrl }); + + const { payload } = await readCloudMatchJson(response); + + // Match Rust behavior: if the poll was routed through the zone load balancer + // and the response now contains a real server IP in connectionInfo, re-poll + // directly via the real server IP. This ensures the signaling data and + // connection info are correct (the zone LB may return different data than + // a direct server poll). + const realServerIp = streamingServerIp(payload); + const polledViaZone = isZoneHostname(baseHost); + const realIpDiffers = + realServerIp && + realServerIp.length > 0 && + !isZoneHostname(realServerIp) && + realServerIp !== input.serverIp; + + if (polledViaZone && realIpDiffers && isReadySessionStatus(payload.session.status)) { + // Session is ready and we now know the real server IP — re-poll directly + console.log( + `[CloudMatch] Session ready: re-polling via real server IP ${realServerIp} (was: ${baseHost})`, + ); + const directBase = `https://${realServerIp}`; + const directUrl = `${directBase}/v2/session/${input.sessionId}`; + try { + // The ready-session direct real-IP re-poll intentionally bypasses the session proxy. + const directResponse = await fetchCloudMatch(directUrl, { + method: "GET", + headers, + }); + if (directResponse.ok) { + const directText = await directResponse.text(); + const directPayload = JSON.parse(directText) as CloudMatchResponse; + if (directPayload.requestStatus.statusCode === 1) { + console.log("[CloudMatch] Direct re-poll succeeded, using direct response for signaling info"); + return await toSessionInfo({ zone: input.zone, streamingBaseUrl: directBase, payload: directPayload, clientId, deviceId }); + } + } + } catch (e) { + // Direct poll failed — fall through to use the original zone LB response + console.warn("[CloudMatch] Direct re-poll failed, using zone LB response:", e); + } + } + + return await toSessionInfo({ zone: input.zone, streamingBaseUrl: base, payload, clientId, deviceId }); +} + +export async function reportSessionAd(input: SessionAdReportRequest): Promise { + if (!input.token) { + throw new Error("Missing token for ad update"); + } + + const clientId = input.clientId ?? crypto.randomUUID(); + const deviceId = input.deviceId ?? crypto.randomUUID(); + const base = resolvePollStopBase(input.zone, input.streamingBaseUrl, input.serverIp); + const url = `${base}/v2/session/${input.sessionId}`; + const clientTimestamp = input.clientTimestamp ?? Math.floor(Date.now() / 1000); + const adUpdate = { + adId: input.adId, + adAction: AD_ACTION_CODES[input.action], + clientTimestamp, + ...(typeof input.watchedTimeInMs === "number" + ? { watchedTimeInMs: Math.max(0, Math.round(input.watchedTimeInMs)) } + : {}), + ...(typeof input.pausedTimeInMs === "number" + ? { pausedTimeInMs: Math.max(0, Math.round(input.pausedTimeInMs)) } + : {}), + ...(input.cancelReason ? { cancelReason: input.cancelReason } : {}), + }; + const requestBody = { + action: SESSION_MODIFY_ACTION_AD_UPDATE, + adUpdates: [adUpdate], + }; + + console.log( + `[CloudMatch] reportSessionAd: sending action=${input.action}(${requestBody.adUpdates[0].adAction}), adId=${input.adId}, ` + + `sessionId=${input.sessionId}, zone=${input.zone}, url=${url}, ` + + `cancelReason=${input.cancelReason ?? "n/a"}, errorInfo=${input.errorInfo ?? "n/a"}`, + ); + + const response = await fetchCloudMatch(url, { + method: "PUT", + // Official browser requests include Origin/Referer on cross-origin ad updates. + headers: buildGfnCloudMatchHeaders({ token: input.token, clientId, deviceId, includeOrigin: true }), + body: JSON.stringify(requestBody), + }); + + const { text, payload } = await readCloudMatchJson(response, { + onErrorText: (text) => { + console.warn( + `[CloudMatch] reportSessionAd: backend error status=${response.status}, sessionId=${input.sessionId}, ` + + `adId=${input.adId}, action=${input.action}, body=${text.slice(0, 500)}`, + ); + }, + }); + if (payload.requestStatus.statusCode !== 1) { + console.warn( + `[CloudMatch] reportSessionAd: API error requestStatus=${payload.requestStatus.statusCode}, ` + + `description=${payload.requestStatus.statusDescription ?? "unknown"}, sessionId=${input.sessionId}, ` + + `adId=${input.adId}, action=${input.action}`, + ); + throw SessionError.fromResponse(200, text); + } + + console.log( + `[CloudMatch] reportSessionAd: success sessionId=${input.sessionId}, adId=${input.adId}, action=${input.action}, ` + + `status=${payload.session.status}, queuePosition=${extractQueuePosition(payload) ?? "n/a"}, ` + + `adsRequired=${extractAdState(payload)?.isAdsRequired ?? false}`, + ); + + return await toSessionInfo({ zone: input.zone, streamingBaseUrl: base, payload, clientId, deviceId }); +} + +export async function stopSession(input: SessionStopRequest): Promise { + if (!input.token) { + throw new Error("Missing token for session stop"); + } + + // Use provided client/device IDs if available (should match session creation) + const clientId = input.clientId ?? crypto.randomUUID(); + const deviceId = input.deviceId ?? crypto.randomUUID(); + + const base = resolvePollStopBase(input.zone, input.streamingBaseUrl, input.serverIp); + const url = `${base}/v2/session/${input.sessionId}`; + const response = await fetchCloudMatch(url, { + method: "DELETE", + headers: buildGfnCloudMatchHeaders({ token: input.token, clientId, deviceId, includeOrigin: false }), + }); + + await throwIfCloudMatchResponseError(response); +} + +/** + * Get list of active sessions (status 2 or 3) + * Returns sessions that are Ready or Streaming + */ +export async function getActiveSessions( + token: string, + streamingBaseUrl: string, +): Promise { + if (!token) { + throw new Error("Missing token for getting active sessions"); + } + + const base = normalizeCloudMatchBaseUrl(streamingBaseUrl); + const headers = buildGfnCloudMatchHeaders({ + token, + deviceId: getStableDeviceId(), + includeOrigin: false, + }); + const primary = await fetchActiveSessionsFromBase(base, headers); + if (primary) { + return primary; + } + + for (const fallbackBase of await discoverActiveSessionFallbackBases(base, headers)) { + if (fallbackBase === base) { + continue; + } + const fallback = await fetchActiveSessionsFromBase(fallbackBase, headers); + if (fallback) { + return fallback; + } + } + + return []; +} + +async function discoverActiveSessionFallbackBases( + base: string, + headers: Record, +): Promise { + try { + const response = await fetchCloudMatch(`${base}/v2/serverInfo`, { + method: "GET", + headers, + }); + if (!response.ok) { + return []; + } + return extractServerInfoRegionBases((await response.json()) as CloudMatchServerInfoResponse); + } catch (error) { + console.warn(`[CloudMatch] getActiveSessions fallback discovery failed: ${formatErrorForLog(error)}`); + return []; + } +} + +async function fetchActiveSessionsFromBase( + base: string, + headers: Record, +): Promise { + const url = `${base}/v2/session`; + + let response: Response; + try { + response = await fetchCloudMatch(url, { + method: "GET", + headers, + }, { retries: 0 }); + } catch (error) { + console.warn(`[CloudMatch] getActiveSessions fetch failed for ${base}: ${formatErrorForLog(error)}`); + return null; + } + + const text = await response.text(); + + if (!response.ok) { + console.warn(`Get sessions failed: ${response.status} - ${text.slice(0, 200)}`); + return null; + } + + let sessionsResponse: GetSessionsResponse; + try { + sessionsResponse = JSON.parse(text) as GetSessionsResponse; + } catch { + return []; + } + + if (sessionsResponse.requestStatus.statusCode !== 1) { + console.warn(`Get sessions API error: ${sessionsResponse.requestStatus.statusDescription}`); + return []; + } + + // Filter active sessions: + // 1 = Setup/Queuing (counts against SESSION_LIMIT — must be included for resume logic) + // 2 = Ready + // 3 = Streaming + const activeSessions: ActiveSessionInfo[] = sessionsResponse.sessions + .filter((s) => s.status === 1 || s.status === 2 || s.status === 3) + .map((s) => { + // Extract appId from sessionRequestData + const appId = s.sessionRequestData?.appId ? Number(s.sessionRequestData.appId) : 0; + + // The server echoes the appLaunchMode the session was created with; keep it + // so claim/resume requests can stay session-stable. + const rawAppLaunchMode = s.sessionRequestData?.appLaunchMode; + const appLaunchMode = + typeof rawAppLaunchMode === "number" && Number.isFinite(rawAppLaunchMode) + ? rawAppLaunchMode + : undefined; + const enablePersistingInGameSettings = + typeof s.sessionRequestData?.enablePersistingInGameSettings === "boolean" + ? s.sessionRequestData.enablePersistingInGameSettings + : undefined; + + // Prefer the real server IP from connectionInfo[usage=14] — this is the actual game server, + // not the zone load balancer. sessionControlInfo.ip is the zone LB hostname and cannot + // accept claim (PUT) requests, which causes HTTP 400. + const connInfo = s.connectionInfo?.find((conn) => conn.usage === 14 && conn.ip); + const rawConnIp = connInfo?.ip as string | string[] | undefined; + const connIp = Array.isArray(rawConnIp) ? rawConnIp[0] : rawConnIp; + + const rawControlIp = s.sessionControlInfo?.ip as string | string[] | undefined; + const controlIp = Array.isArray(rawControlIp) ? rawControlIp[0] : rawControlIp; + + const serverIp = connIp ?? controlIp; + + const signalingUrl = connIp + ? `wss://${connIp}:443/nvst/` + : controlIp + ? `wss://${controlIp}:443/nvst/` + : undefined; + + // Extract resolution and fps from monitor settings + const monitorSettings = s.monitorSettings?.[0]; + const resolution = monitorSettings + ? `${monitorSettings.widthInPixels ?? 0}x${monitorSettings.heightInPixels ?? 0}` + : undefined; + const fps = monitorSettings?.framesPerSecond ?? undefined; + + return { + sessionId: s.sessionId, + appId, + appLaunchMode, + enablePersistingInGameSettings, + gpuType: s.gpuType, + status: s.status, + queuePosition: extractSessionQueuePosition(s), + seatSetupStep: extractSessionSeatSetupStep(s), + streamingBaseUrl: base, + serverIp, + signalingUrl, + resolution, + fps, + }; + }); + + return activeSessions; +} + +/** + * Claim/Resume an existing session + * Required before connecting to an existing session + */ +export async function claimSession(input: SessionClaimRequest): Promise { + if (!input.token) { + throw new Error("Missing token for session claim"); + } + + const deviceId = input.deviceId ?? getStableDeviceId(); + const clientId = input.clientId ?? crypto.randomUUID(); + + // Provide default values for optional parameters + const appId = input.appId ?? "0"; + const settings = input.settings ?? { + resolution: "1920x1080", + fps: 60, + maxBitrateMbps: 75, + codec: "H264", + colorQuality: "8bit_420", + keyboardLayout: DEFAULT_KEYBOARD_LAYOUT, + gameLanguage: "en_US", + enableL4S: false, + enableCloudGsync: false, + }; + const keyboardLayout = resolveGfnKeyboardLayout(settings.keyboardLayout ?? DEFAULT_KEYBOARD_LAYOUT, process.platform); + const languageCode = settings.gameLanguage ?? "en_US"; + + // The session list endpoint returns the zone LB hostname in sessionControlInfo.ip. + // A claim PUT sent to the zone LB returns HTTP 400 because it does not handle + // session-level mutations. The real game server IP is only reliably available from + // the individual session endpoint (GET /v2/session/{id}). Resolve it here before + // building the claim URL. + // IMPORTANT: We must query the SAME zone LB where the session is hosted (use serverIp), + // not the provider's generic streamingBaseUrl (which may route to a different zone LB). + let effectiveServerIp = input.serverIp; + console.log(`[CloudMatch] claimSession: input serverIp=${input.serverIp}, isZone=${isZoneHostname(input.serverIp)}`); + if (isZoneHostname(effectiveServerIp)) { + const zoneBase = `https://${effectiveServerIp}`; + const prefetchUrl = `${zoneBase}/v2/session/${input.sessionId}`; + console.log(`[CloudMatch] claimSession: pre-flight query ${prefetchUrl}`); + const prefetchHeaders = buildGfnCloudMatchHeaders({ token: input.token, clientId, deviceId, includeOrigin: false }); + try { + const prefetchResp = await fetchCloudMatch(prefetchUrl, { method: "GET", headers: prefetchHeaders }); + console.log(`[CloudMatch] claimSession: pre-flight response status=${prefetchResp.status}`); + if (prefetchResp.ok) { + const prefetchPayload = JSON.parse(await prefetchResp.text()) as CloudMatchResponse; + const realIp = streamingServerIp(prefetchPayload); + console.log(`[CloudMatch] claimSession: extracted realIp=${realIp}, isZone=${realIp ? isZoneHostname(realIp) : 'N/A'}`); + if (realIp) { + effectiveServerIp = realIp; + const ipType = isZoneHostname(realIp) ? 'zone LB' : 'direct IP'; + console.log(`[CloudMatch] claimSession: using extracted ${ipType}: ${realIp}`); + } + } else { + console.warn(`[CloudMatch] claimSession: pre-flight returned HTTP ${prefetchResp.status}, text=${await prefetchResp.text()}`); + } + } catch (e) { + console.warn("[CloudMatch] claimSession: pre-flight poll failed, proceeding with zone hostname:", e); + } + } + + const claimUrl = `https://${effectiveServerIp}/v2/session/${input.sessionId}?${new URLSearchParams({ keyboardLayout, languageCode }).toString()}`; + + // Pre-claim validation: check session status before deciding whether to send a RESUME claim. + // Status 1 (setup/launching/queuing) sessions cannot be RESUME'd — the server will reject + // with SESSION_NOT_PAUSED. For these sessions we skip the claim PUT and poll directly. + // Status 2/3 (ready/streaming) sessions are paused and can be RESUME'd normally. + let preClaimStatus: number | null = null; + let shouldSendResumeClaim = true; + try { + const validationUrl = `https://${effectiveServerIp}/v2/session/${input.sessionId}`; + const validationHeaders = buildGfnCloudMatchHeaders({ token: input.token, clientId, deviceId, includeOrigin: false }); + const validationResp = await fetchCloudMatch(validationUrl, { method: "GET", headers: validationHeaders }); + if (validationResp.ok) { + const validationText = await validationResp.text(); + const validationPayload = JSON.parse(validationText) as CloudMatchResponse; + preClaimStatus = validationPayload.session?.status ?? 0; + const errorCode = validationPayload.session?.errorCode ?? 0; + console.log(`[CloudMatch] claimSession: pre-claim validation status=${preClaimStatus}, errorCode=${errorCode}`); + console.log(`[CloudMatch] claimSession: validation response (first 1000 chars): ${validationText.slice(0, 1000)}`); + if (preClaimStatus === 1) { + console.log(`[CloudMatch] claimSession: session is still launching (status=1), skipping RESUME claim — polling directly to ready state`); + } else if ( + input.recoveryMode === true && + (preClaimStatus === 2 || preClaimStatus === 3) + ) { + // Recovery parity: if the session is already ready/streaming, avoid sending + // another RESUME mutation. Repeated RESUME PUTs can rotate signaling hosts + // and push the session back into transient setup/cleanup states. + shouldSendResumeClaim = false; + console.log( + `[CloudMatch] claimSession: recoveryMode and session already ready (status=${preClaimStatus}); skipping redundant RESUME claim`, + ); + } else if (preClaimStatus !== 2 && preClaimStatus !== 3) { + console.warn(`[CloudMatch] claimSession: session not in ready state (status=${preClaimStatus}), claim may fail`); + } + } else { + console.warn(`[CloudMatch] claimSession: pre-claim validation returned HTTP ${validationResp.status}`); + } + } catch (e) { + console.warn("[CloudMatch] claimSession: pre-claim validation failed:", e); + } + + // Only send the RESUME claim PUT if the session is in a paused state (status 2 or 3). + // For status=1 (still launching) we bypass the claim and fall through to the polling loop. + if (preClaimStatus !== 1 && shouldSendResumeClaim) { + const payload = buildClaimRequestBody( + input.sessionId, + appId, + settings, + input.appLaunchMode, + input.enablePersistingInGameSettings === true, + ); + + const headers = buildGfnCloudMatchClaimHeaders({ token: input.token, clientId, deviceId }); + + console.log(`[CloudMatch] claimSession PUT ${claimUrl}`); + console.log(`[CloudMatch] claimSession body: ${JSON.stringify(payload)}`); + const response = await fetchCloudMatch(claimUrl, { + method: "PUT", + headers, + body: JSON.stringify(payload), + }); + + const { text, payload: apiResponse } = await readCloudMatchJson(response, { + onText: (text) => { + console.log(`[CloudMatch] claimSession response: HTTP ${response.status}`); + console.log(`[CloudMatch] claimSession response body FULL: ${text}`); + }, + }); + + if (apiResponse.requestStatus.statusCode !== 1) { + throw SessionError.fromResponse(200, text); + } + } + + // Poll until session is ready (status 2 or 3) + const getUrl = `https://${effectiveServerIp}/v2/session/${input.sessionId}`; + const maxAttempts = 60; + + for (let attempt = 1; attempt <= maxAttempts; attempt++) { + if (attempt > 1) { + await new Promise((resolve) => setTimeout(resolve, 1000)); + } + + const pollHeaders = buildGfnCloudMatchHeaders({ token: input.token, clientId, deviceId, includeOrigin: false }); + + const pollResponse = await fetchCloudMatch(getUrl, { + method: "GET", + headers: pollHeaders, + }); + + if (!pollResponse.ok) { + continue; + } + + const pollText = await pollResponse.text(); + let pollApiResponse: CloudMatchResponse; + + try { + pollApiResponse = JSON.parse(pollText) as CloudMatchResponse; + } catch { + continue; + } + + const sessionData = pollApiResponse.session; + + if (sessionData.status === 2 || sessionData.status === 3) { + // Session is ready + const signaling = resolveSignaling(pollApiResponse); + const queuePosition = extractQueuePosition(pollApiResponse); + const negotiatedStreamProfile = extractNegotiatedStreamProfile(pollApiResponse); + const requestedStreamingFeatures = normalizeStreamingFeatures( + pollApiResponse.session.sessionRequestData?.requestedStreamingFeatures, + ); + const finalizedStreamingFeatures = normalizeStreamingFeatures( + pollApiResponse.session.finalizedStreamingFeatures, + ); + const enablePersistingInGameSettings = + typeof pollApiResponse.session.sessionRequestData?.enablePersistingInGameSettings === "boolean" + ? pollApiResponse.session.sessionRequestData.enablePersistingInGameSettings + : undefined; + console.log( + `[CloudMatch] claimed negotiated streaming features: requested=${JSON.stringify(requestedStreamingFeatures ?? {})} finalized=${JSON.stringify(finalizedStreamingFeatures ?? {})} cloudGsync=${negotiatedStreamProfile?.enableCloudGsync ?? "n/a"}, reflex=${negotiatedStreamProfile?.enableReflex ?? "n/a"}, l4s=${negotiatedStreamProfile?.enableL4S ?? "n/a"}`, + ); + + return { + sessionId: sessionData.sessionId, + appId: input.appId, + status: sessionData.status, + queuePosition, + zone: "", // Zone not applicable for claimed sessions + streamingBaseUrl: `https://${effectiveServerIp}`, + serverIp: signaling.serverIp, + signalingServer: signaling.signalingServer, + signalingUrl: signaling.signalingUrl, + gpuType: sessionData.gpuType, + appLaunchMode: echoedSessionAppLaunchMode(pollApiResponse) ?? input.appLaunchMode, + enablePersistingInGameSettings, + rtspsEndpoints: signaling.rtspsEndpoints.length > 0 ? signaling.rtspsEndpoints : undefined, + iceServers: await normalizeIceServers(pollApiResponse), + mediaConnectionInfo: signaling.mediaConnectionInfo, + negotiatedStreamProfile: negotiatedStreamProfile ?? extractNegotiatedStreamProfile(pollApiResponse), + requestedStreamingFeatures, + finalizedStreamingFeatures, + clientId, + deviceId, + }; + } + + // Status 1 (setup/launching), 6 (cleaning up), etc. — continue polling for ready state (2 or 3) + // Only break if we encounter a terminal error state (status 4, 5, etc.) + if (sessionData.status > 3 && sessionData.status !== 6) { + break; + } + } + + throw new Error("Session did not become ready after claiming"); +} diff --git a/opennow-stable/src/main/platforms/gfn/cloudmatchFeatures.ts b/opennow-stable/src/main/platforms/gfn/cloudmatchFeatures.ts new file mode 100644 index 000000000..79a99c15a --- /dev/null +++ b/opennow-stable/src/main/platforms/gfn/cloudmatchFeatures.ts @@ -0,0 +1,60 @@ +import type { AppLaunchMode, SessionCreateRequest, StreamSettings } from "@shared/gfn"; +import { DEFAULT_MINIMUM_FPS_FOR_REFLEX_WITHOUT_VRR } from "@shared/cloudGsync"; + +import type { CloudMatchRequest } from "./types"; + +// Wire values used by cloudmatch session requests. Matches the official +// client's mapping: Default -> 1, GamepadFriendly -> 2, TouchFriendly -> 3. +const APP_LAUNCH_MODE_WIRE_VALUES: Record = { + default: 1, + gamepadFriendly: 2, + touchFriendly: 3, +}; + +export function appLaunchModeWireValue(mode: AppLaunchMode | undefined): number { + return APP_LAUNCH_MODE_WIRE_VALUES[mode ?? "default"]; +} + +export function buildRequestedStreamingFeatures( + settings: StreamSettings, + bitDepth: number, + chromaFormat: number, + _hdrEnabled: boolean, +): CloudMatchRequest["sessionRequestData"]["requestedStreamingFeatures"] { + const cloudGsync = settings.enableCloudGsync; + + return { + reflex: shouldRequestReflex(settings), + bitDepth, + cloudGsync, + enabledL4S: settings.enableL4S, + supportedHidDevices: 0, + profile: 0, + fallbackToLogicalResolution: false, + chromaFormat, + prefilterMode: 0, + prefilterSharpness: 0, + prefilterNoiseReduction: 0, + hudStreamingMode: 0, + }; +} + +export function shouldRequestReflex(settings: StreamSettings): boolean { + if (typeof settings.cloudGsyncResolution?.reflexEnabled === "boolean") { + return settings.cloudGsyncResolution.reflexEnabled; + } + + const reflexMinimum = + settings.cloudGsyncResolution?.capabilities.minimumFpsForReflexWithoutVrr + ?? DEFAULT_MINIMUM_FPS_FOR_REFLEX_WITHOUT_VRR; + return settings.enableCloudGsync || settings.fps >= reflexMinimum; +} + +export function shouldEnableInGameSettingsPersistence( + input: Pick, +): boolean { + return ( + input.enablePersistingInGameSettings === true && + input.supportsInGameSettingsPersistence === true + ); +} diff --git a/opennow-stable/src/main/platforms/gfn/cloudmatchSessionParsing.ts b/opennow-stable/src/main/platforms/gfn/cloudmatchSessionParsing.ts new file mode 100644 index 000000000..3fe162f2a --- /dev/null +++ b/opennow-stable/src/main/platforms/gfn/cloudmatchSessionParsing.ts @@ -0,0 +1,433 @@ +import type { + ColorQuality, + NegotiatedStreamProfile, + SessionAdInfo, + SessionAdState, + SessionInfo, + StreamingFeatures, +} from "@shared/gfn"; + +import type { CloudMatchResponse, GetSessionsResponse } from "./types"; +import { SessionError } from "./errorCodes"; +import { + normalizeIceServers, + resolveSignaling, +} from "./cloudmatchSignaling"; + +const GFN_AD_MEDIA_PROFILE_ORDER = new Map([ + ["mp4deinterlaced720p", 0], + ["webm", 1], + ["hlsadaptive", 2], +]); + +/** Wire appLaunchMode the server echoes back for an existing session, if present. */ +export function echoedSessionAppLaunchMode(payload: CloudMatchResponse): number | undefined { + const raw = payload.session?.sessionRequestData?.appLaunchMode; + return typeof raw === "number" && Number.isFinite(raw) ? raw : undefined; +} + +export function toPositiveInt(value: unknown): number | undefined { + if (typeof value === "number" && Number.isFinite(value)) { + const normalized = Math.trunc(value); + return normalized > 0 ? normalized : undefined; + } + if (typeof value === "string" && value.trim().length > 0) { + const parsed = Number.parseInt(value, 10); + return Number.isFinite(parsed) && parsed > 0 ? parsed : undefined; + } + return undefined; +} + +export function toBoolean(value: unknown): boolean | undefined { + if (typeof value === "boolean") { + return value; + } + if (typeof value === "number" && Number.isFinite(value)) { + return value !== 0; + } + if (typeof value === "string") { + const normalized = value.trim().toLowerCase(); + if (normalized === "true" || normalized === "1") { + return true; + } + if (normalized === "false" || normalized === "0") { + return false; + } + } + return undefined; +} + +export function toOptionalString(value: unknown): string | undefined { + if (typeof value !== "string") { + return undefined; + } + const trimmed = value.trim(); + return trimmed.length > 0 ? trimmed : undefined; +} + +export function extractSessionQueuePosition(session: CloudMatchResponse["session"] | GetSessionsResponse["sessions"][number]): number | undefined { + const direct = toPositiveInt(session.queuePosition); + if (direct !== undefined) { + return direct; + } + + const seatSetup = session.seatSetupInfo; + if (seatSetup) { + const nested = toPositiveInt(seatSetup.queuePosition); + if (nested !== undefined) { + return nested; + } + } + + const nestedSessionProgress = session.sessionProgress; + if (nestedSessionProgress) { + const nested = toPositiveInt(nestedSessionProgress.queuePosition); + if (nested !== undefined) { + return nested; + } + } + + const nestedProgressInfo = session.progressInfo; + if (nestedProgressInfo) { + const nested = toPositiveInt(nestedProgressInfo.queuePosition); + if (nested !== undefined) { + return nested; + } + } + + return undefined; +} + +export function extractQueuePosition(payload: CloudMatchResponse): number | undefined { + return extractSessionQueuePosition(payload.session); +} + +export function extractSessionSeatSetupStep(session: CloudMatchResponse["session"] | GetSessionsResponse["sessions"][number]): number | undefined { + const raw = session.seatSetupInfo?.seatSetupStep; + if (typeof raw === "number" && Number.isFinite(raw)) { + return Math.trunc(raw); + } + return undefined; +} + +export function extractSeatSetupStep(payload: CloudMatchResponse): number | undefined { + return extractSessionSeatSetupStep(payload.session); +} + +export function normalizeSessionAdInfo(ad: NonNullable[number], index: number): SessionAdInfo | null { + const adId = toOptionalString(ad.adId); + const adMediaFiles = (ad.adMediaFiles ?? []) + .map((file) => ({ + mediaFileUrl: toOptionalString(file.mediaFileUrl), + encodingProfile: toOptionalString(file.encodingProfile), + })) + .filter((file) => file.mediaFileUrl || file.encodingProfile) + .sort((left, right) => { + const leftRank = left.encodingProfile ? GFN_AD_MEDIA_PROFILE_ORDER.get(left.encodingProfile) ?? Number.MAX_SAFE_INTEGER : Number.MAX_SAFE_INTEGER; + const rightRank = right.encodingProfile ? GFN_AD_MEDIA_PROFILE_ORDER.get(right.encodingProfile) ?? Number.MAX_SAFE_INTEGER : Number.MAX_SAFE_INTEGER; + return leftRank - rightRank; + }); + + // Match the official browser config preference order: MP4, WebM, then HLS. + const preferredMediaFile = adMediaFiles.find((file) => file.mediaFileUrl); + const mediaUrl = + preferredMediaFile?.mediaFileUrl ?? + toOptionalString(ad.adUrl) ?? + toOptionalString(ad.mediaUrl) ?? + toOptionalString(ad.videoUrl) ?? + toOptionalString(ad.url); + + const adUrl = toOptionalString(ad.adUrl); + const clickThroughUrl = toOptionalString(ad.clickThroughUrl); + const title = toOptionalString(ad.title); + const description = toOptionalString(ad.description); + const adLengthInSeconds = + typeof ad.adLengthInSeconds === "number" && Number.isFinite(ad.adLengthInSeconds) && ad.adLengthInSeconds > 0 + ? ad.adLengthInSeconds + : undefined; + + // adLengthInSeconds is the confirmed live field (value is in seconds, convert to ms). + // Fall back to legacy durationMs / durationInMs which are already in ms. + const durationMs = + (adLengthInSeconds !== undefined + ? Math.round(adLengthInSeconds * 1000) + : undefined) ?? + toPositiveInt(ad.durationMs) ?? + toPositiveInt(ad.durationInMs); + + const adState = typeof ad.adState === "number" && Number.isFinite(ad.adState) ? Math.trunc(ad.adState) : undefined; + + if (!adId && !mediaUrl && !adUrl && adMediaFiles.length === 0 && !title && !description) { + return null; + } + + return { + adId: adId ?? `ad-${index + 1}`, + state: adState, + adState, + adUrl, + mediaUrl, + adMediaFiles, + clickThroughUrl, + adLengthInSeconds, + durationMs, + title, + description, + }; +} + +export function extractAdState(payload: CloudMatchResponse): SessionAdState | undefined { + const sessionAdsRequired = + toBoolean(payload.session.sessionAdsRequired) ?? + toBoolean(payload.session.isAdsRequired) ?? + toBoolean(payload.session.sessionProgress?.isAdsRequired) ?? + toBoolean(payload.session.progressInfo?.isAdsRequired); + + // Log raw sessionAds whenever the server signals ads are required so field names + // can be verified when creative URLs are expected but the ads[] array stays empty. + if (sessionAdsRequired) { + console.log( + `[CloudMatch] extractAdState: sessionAdsRequired=${payload.session.sessionAdsRequired}, ` + + `isAdsRequired=${payload.session.isAdsRequired}, ` + + `sessionAds=${JSON.stringify(payload.session.sessionAds ?? null)}, ` + + `opportunity=${JSON.stringify(payload.session.opportunity ?? null)}`, + ); + } + + const ads = (payload.session.sessionAds ?? []) + .map((ad, index) => normalizeSessionAdInfo(ad, index)) + .filter((ad): ad is SessionAdInfo => ad !== null); + + const opportunity = payload.session.opportunity; + const normalizedOpportunity = opportunity + ? { + state: toOptionalString(opportunity.state), + queuePaused: toBoolean(opportunity.queuePaused), + gracePeriodSeconds: toPositiveInt(opportunity.gracePeriodSeconds), + message: toOptionalString(opportunity.message), + title: toOptionalString(opportunity.title), + description: toOptionalString(opportunity.description), + } + : undefined; + const queuePaused = + normalizedOpportunity?.queuePaused ?? + (typeof normalizedOpportunity?.state === "string" ? normalizedOpportunity.state.toLowerCase() === "graceperiodstart" : undefined); + const gracePeriodSeconds = normalizedOpportunity?.gracePeriodSeconds; + const effectiveIsAdsRequired = sessionAdsRequired ?? ads.length > 0; + const message = + normalizedOpportunity?.message ?? + normalizedOpportunity?.description ?? + (queuePaused + ? "Resume ads to stay in queue." + : effectiveIsAdsRequired + ? "Finish ads to stay in queue." + : undefined); + + if (!effectiveIsAdsRequired && ads.length === 0 && !queuePaused && !message) { + return undefined; + } + + return { + isAdsRequired: effectiveIsAdsRequired, + sessionAdsRequired, + isQueuePaused: queuePaused, + gracePeriodSeconds, + message, + sessionAds: ads, + ads, + opportunity: normalizedOpportunity, + // Mark whether the server sent sessionAds=null (transient gap) so the + // renderer's mergeAdState can safely restore the previous ad list for the + // ad player, while NOT restoring it after an explicit client-side clear + // that follows a rejected finish action. + serverSentEmptyAds: payload.session.sessionAds == null, + }; +} + +export function toColorQuality(bitDepth?: number, chromaFormat?: number): ColorQuality | undefined { + const normalizedBitDepth = bitDepth === 10 ? 1 : bitDepth; + const normalizedChromaFormat = chromaFormat === 2 ? 1 : chromaFormat; + + if (normalizedBitDepth !== 0 && normalizedBitDepth !== 1) { + return undefined; + } + if (normalizedChromaFormat !== 0 && normalizedChromaFormat !== 1) { + return undefined; + } + + if (normalizedBitDepth === 1) { + return normalizedChromaFormat === 1 ? "10bit_444" : "10bit_420"; + } + + return normalizedChromaFormat === 1 ? "8bit_444" : "8bit_420"; +} + +export function normalizeStreamingFeatures( + features: + | NonNullable["requestedStreamingFeatures"] + | CloudMatchResponse["session"]["finalizedStreamingFeatures"] + | undefined, +): StreamingFeatures | undefined { + if (!features) { + return undefined; + } + + const normalized: StreamingFeatures = {}; + + if (typeof features.reflex === "boolean") { + normalized.reflex = features.reflex; + } + if (typeof features.bitDepth === "number" && Number.isFinite(features.bitDepth)) { + normalized.bitDepth = Math.trunc(features.bitDepth); + } + if (typeof features.cloudGsync === "boolean") { + normalized.cloudGsync = features.cloudGsync; + } + if (typeof features.chromaFormat === "number" && Number.isFinite(features.chromaFormat)) { + normalized.chromaFormat = Math.trunc(features.chromaFormat); + } + if (typeof features.enabledL4S === "boolean") { + normalized.enabledL4S = features.enabledL4S; + } + if ("trueHdr" in features && typeof features.trueHdr === "boolean") { + normalized.trueHdr = features.trueHdr; + } + + return Object.keys(normalized).length > 0 ? normalized : undefined; +} + +export function extractNegotiatedStreamProfile(payload: CloudMatchResponse): NegotiatedStreamProfile | undefined { + const monitor = payload.session.sessionRequestData?.clientRequestMonitorSettings?.[0]; + const finalizedFeatures = payload.session.finalizedStreamingFeatures; + const requestedFeatures = payload.session.sessionRequestData?.requestedStreamingFeatures; + + const width = monitor?.widthInPixels; + const height = monitor?.heightInPixels; + const fps = monitor?.framesPerSecond; + const colorQuality = toColorQuality( + finalizedFeatures?.bitDepth ?? requestedFeatures?.bitDepth, + finalizedFeatures?.chromaFormat ?? requestedFeatures?.chromaFormat, + ); + const enabledL4S = finalizedFeatures?.enabledL4S ?? requestedFeatures?.enabledL4S; + const enabledCloudGsync = finalizedFeatures?.cloudGsync ?? requestedFeatures?.cloudGsync; + const enabledReflex = finalizedFeatures?.reflex ?? requestedFeatures?.reflex; + + const profile: NegotiatedStreamProfile = {}; + + if ( + typeof width === "number" && + Number.isFinite(width) && + width > 0 && + typeof height === "number" && + Number.isFinite(height) && + height > 0 + ) { + profile.resolution = `${Math.trunc(width)}x${Math.trunc(height)}`; + } + + if (typeof fps === "number" && Number.isFinite(fps) && fps > 0) { + profile.fps = Math.trunc(fps); + } + + if (colorQuality) { + profile.colorQuality = colorQuality; + } + + if (typeof enabledL4S === "boolean") { + profile.enableL4S = enabledL4S; + } + + if (typeof enabledCloudGsync === "boolean") { + profile.enableCloudGsync = enabledCloudGsync; + } + + if (typeof enabledReflex === "boolean") { + profile.enableReflex = enabledReflex; + } + + return Object.keys(profile).length > 0 ? profile : undefined; +} + +export interface ToSessionInfoOptions { + zone: string; + streamingBaseUrl: string; + payload: CloudMatchResponse; + clientId?: string; + deviceId?: string; + fallbackAppId?: string; + /** Wire appLaunchMode sent with the request, used when the server does not echo it */ + fallbackAppLaunchMode?: number; +} + +export async function toSessionInfo(options: ToSessionInfoOptions): Promise { + const { zone, streamingBaseUrl, payload, clientId, deviceId } = options; + if (payload.requestStatus.statusCode !== 1) { + // Use SessionError for parsing error responses + const errorJson = JSON.stringify(payload); + throw SessionError.fromResponse(200, errorJson); + } + + const signaling = resolveSignaling(payload); + const queuePosition = extractQueuePosition(payload); + const seatSetupStep = extractSeatSetupStep(payload); + const adState = extractAdState(payload); + const negotiatedStreamProfile = extractNegotiatedStreamProfile(payload); + const requestedStreamingFeatures = normalizeStreamingFeatures( + payload.session.sessionRequestData?.requestedStreamingFeatures, + ); + const finalizedStreamingFeatures = normalizeStreamingFeatures( + payload.session.finalizedStreamingFeatures, + ); + const enablePersistingInGameSettings = + typeof payload.session.sessionRequestData?.enablePersistingInGameSettings === "boolean" + ? payload.session.sessionRequestData.enablePersistingInGameSettings + : undefined; + + // Debug logging to trace signaling resolution + const connections = payload.session.connectionInfo ?? []; + const connectionSummary = connections + .map((conn) => { + const rawIp = Array.isArray(conn.ip) ? conn.ip[0] : conn.ip; + return `{usage=${conn.usage},ip=${rawIp ?? "null"},port=${conn.port},resourcePath=${conn.resourcePath ?? "null"}}`; + }) + .join(", "); + console.log( + `[CloudMatch] toSessionInfo: status=${payload.session.status}, ` + + `seatSetupStep=${seatSetupStep ?? "n/a"}, ` + + `queuePosition=${queuePosition ?? "n/a"}, ` + + `connectionInfo=${connections.length} entries, ` + + `serverIp=${signaling.serverIp}, ` + + `signalingServer=${signaling.signalingServer}, ` + + `signalingUrl=${signaling.signalingUrl}, ` + + `rtspsEndpoints=${JSON.stringify(signaling.rtspsEndpoints)}, ` + + `connections=[${connectionSummary}]`, + ); + console.log( + `[CloudMatch] negotiated streaming features: requested=${JSON.stringify(requestedStreamingFeatures ?? {})} finalized=${JSON.stringify(finalizedStreamingFeatures ?? {})} cloudGsync=${negotiatedStreamProfile?.enableCloudGsync ?? "n/a"}, reflex=${negotiatedStreamProfile?.enableReflex ?? "n/a"}, l4s=${negotiatedStreamProfile?.enableL4S ?? "n/a"}`, + ); + + return { + sessionId: payload.session.sessionId, + appId: payload.session.sessionRequestData?.appId ?? options.fallbackAppId, + status: payload.session.status, + seatSetupStep, + queuePosition, + adState, + zone, + streamingBaseUrl, + serverIp: signaling.serverIp, + signalingServer: signaling.signalingServer, + signalingUrl: signaling.signalingUrl, + gpuType: payload.session.gpuType, + appLaunchMode: echoedSessionAppLaunchMode(payload) ?? options.fallbackAppLaunchMode, + enablePersistingInGameSettings, + rtspsEndpoints: signaling.rtspsEndpoints.length > 0 ? signaling.rtspsEndpoints : undefined, + iceServers: await normalizeIceServers(payload), + mediaConnectionInfo: signaling.mediaConnectionInfo, + negotiatedStreamProfile, + requestedStreamingFeatures, + finalizedStreamingFeatures, + clientId, + deviceId, + }; +} diff --git a/opennow-stable/src/main/platforms/gfn/cloudmatchSessionRequest.ts b/opennow-stable/src/main/platforms/gfn/cloudmatchSessionRequest.ts new file mode 100644 index 000000000..a00be970e --- /dev/null +++ b/opennow-stable/src/main/platforms/gfn/cloudmatchSessionRequest.ts @@ -0,0 +1,338 @@ +import crypto from "node:crypto"; +import { createHash } from "node:crypto"; + +import type { SessionCreateRequest, StreamSettings } from "@shared/gfn"; +import { + colorQualityBitDepth, + colorQualityChromaFormat, +} from "@shared/gfn"; + +import type { CloudMatchRequest } from "./types"; +import { buildGfnCloudMatchHeaders } from "./clientHeaders"; +import { resolveGfnDeviceIdentity } from "./deviceIdentity"; +import { getStableDeviceId } from "./deviceId"; +import { + appLaunchModeWireValue, + buildRequestedStreamingFeatures, + shouldEnableInGameSettingsPersistence, +} from "./cloudmatchFeatures"; +import { + fetchCloudMatch, + formatErrorForLog, +} from "./cloudmatchTransport"; + +const NETWORK_TEST_SESSION_TIMEOUT_MS = 8_000; +const NETWORK_TEST_SESSION_CACHE_TTL_MS = 30 * 60 * 1000; + +const networkTestSessionCache = new Map(); + +interface NetworkTestSessionResponse { + requestStatus?: { + statusCode?: number; + statusDescription?: string; + serverId?: string; + }; + netTestSession?: { + sessionId?: string; + connectionInfo?: Array<{ + ip?: string; + port?: number; + appLevelProtocol?: number; + }>; + netTestThresholds?: { + recommendedBandwidthMBPS?: number; + requiredBandwidthMBPS?: number; + recommendedLatencyMS?: number; + requiredLatencyMS?: number; + recommendedPacketLossPct?: number; + requiredPacketLossPct?: number; + }; + serverId?: string; + }; +} + +export function parseResolution(input: string): { width: number; height: number } { + const [rawWidth, rawHeight] = input.split("x"); + const width = Number.parseInt(rawWidth ?? "", 10); + const height = Number.parseInt(rawHeight ?? "", 10); + + if (!Number.isFinite(width) || !Number.isFinite(height) || width <= 0 || height <= 0) { + return { width: 1920, height: 1080 }; + } + + return { width, height }; +} + +function networkTestSessionCacheKey(base: string, settings: StreamSettings, token: string, proxyUrl?: string): string { + const { width, height } = parseResolution(settings.resolution); + const identityHash = createHash("sha256") + .update(token) + .update("\0") + .update(proxyUrl ?? "") + .digest("hex") + .slice(0, 16); + return `${base}\0${width}x${height}@${settings.fps}\0${identityHash}`; +} + +export function getCachedNetworkTestSessionId(base: string, settings: StreamSettings, token: string, proxyUrl?: string): string | null { + const cacheKey = networkTestSessionCacheKey(base, settings, token, proxyUrl); + const cached = networkTestSessionCache.get(cacheKey); + if (!cached) { + return null; + } + + if (cached.expiresAt <= Date.now()) { + networkTestSessionCache.delete(cacheKey); + return null; + } + + return cached.sessionId; +} + +export function cacheNetworkTestSessionId( + base: string, + settings: StreamSettings, + token: string, + sessionId: string, + proxyUrl?: string, +): void { + networkTestSessionCache.set(networkTestSessionCacheKey(base, settings, token, proxyUrl), { + sessionId, + expiresAt: Date.now() + NETWORK_TEST_SESSION_CACHE_TTL_MS, + }); +} + +export async function createNetworkTestSession(input: { + base: string; + token: string; + clientId: string; + deviceId: string; + settings: StreamSettings; + proxyUrl?: string; +}): Promise { + const cached = getCachedNetworkTestSessionId(input.base, input.settings, input.token, input.proxyUrl); + if (cached) { + return cached; + } + + const { width, height } = parseResolution(input.settings.resolution); + const { clientPlatformName } = resolveGfnDeviceIdentity(); + const body = { + netTestRequestData: { + clientPlatformName, + netTestProfile: { + widthInPixels: width, + heightInPixels: height, + framesPerSecond: input.settings.fps, + }, + }, + }; + + try { + const response = await fetchCloudMatch(`${input.base}/v2/nettestsession`, { + method: "POST", + headers: buildGfnCloudMatchHeaders({ + token: input.token, + clientId: input.clientId, + deviceId: input.deviceId, + includeOrigin: true, + }), + body: JSON.stringify(body), + }, { + proxyUrl: input.proxyUrl, + timeoutMs: NETWORK_TEST_SESSION_TIMEOUT_MS, + retries: 0, + }); + + if (!response.ok) { + console.warn(`[CloudMatch] nettestsession failed HTTP ${response.status}: ${(await response.text()).slice(0, 200)}`); + return null; + } + + const payload = (await response.json()) as NetworkTestSessionResponse; + if (payload.requestStatus?.statusCode !== 1) { + console.warn( + `[CloudMatch] nettestsession API error: ${payload.requestStatus?.statusCode ?? "unknown"} ` + + `${payload.requestStatus?.statusDescription ?? ""}`.trim(), + ); + return null; + } + + const sessionId = payload.netTestSession?.sessionId?.trim(); + if (!sessionId) { + console.warn("[CloudMatch] nettestsession response did not include a sessionId"); + return null; + } + + cacheNetworkTestSessionId(input.base, input.settings, input.token, sessionId, input.proxyUrl); + return sessionId; + } catch (error) { + console.warn(`[CloudMatch] nettestsession creation failed: ${formatErrorForLog(error)}`); + return null; + } +} + +export function timezoneOffsetMs(): number { + return -new Date().getTimezoneOffset() * 60 * 1000; +} + +export function webRtcSessionMetadata(width: number, height: number): Array<{ key: string; value: string }> { + return [ + { key: "SubSessionId", value: crypto.randomUUID() }, + { key: "wssignaling", value: "1" }, + { key: "GSStreamerType", value: "WebRTC" }, + { key: "networkType", value: "Unknown" }, + { key: "ClientImeSupport", value: "0" }, + { + key: "clientPhysicalResolution", + value: JSON.stringify({ horizontalPixels: width, verticalPixels: height }), + }, + { key: "surroundAudioInfo", value: "2" }, + ]; +} + +export function buildSessionRequestBody( + input: SessionCreateRequest, + deviceHashId: string, + networkTestSessionId: string | null = null, +): CloudMatchRequest { + const { width, height } = parseResolution(input.settings.resolution); + const cq = input.settings.colorQuality; + // IMPORTANT: hdrEnabled is a SEPARATE toggle from color quality. + // The Rust reference (cloudmatch.rs) uses settings.hdr_enabled independently. + // 10-bit color depth does NOT mean HDR — you can have 10-bit SDR. + // Conflating them caused the server to set up an HDR pipeline, which + // dynamically downscaled resolution to ~540p. + const hdrEnabled = false; // No HDR toggle implemented yet; hardcode off like claim body + const bitDepth = colorQualityBitDepth(cq); + const chromaFormat = colorQualityChromaFormat(cq); + const accountLinked = input.accountLinked ?? true; + + return { + sessionRequestData: { + appId: input.appId, + internalTitle: input.internalTitle || null, + availableSupportedControllers: [], + networkTestSessionId, + parentSessionId: null, + clientIdentification: "GFN-PC", + // Keep device identity stable across create -> reconnect/resume flows. + // The official client preserves this identity, and resume reliability depends on it. + deviceHashId, + clientVersion: "30.0", + sdkVersion: "1.0", + streamerVersion: 1, + clientPlatformName: resolveGfnDeviceIdentity().clientPlatformName, + clientRequestMonitorSettings: [ + { + monitorId: 0, + positionX: 0, + positionY: 0, + widthInPixels: width, + heightInPixels: height, + framesPerSecond: input.settings.fps, + sdrHdrMode: hdrEnabled ? 1 : 0, + displayData: hdrEnabled + ? { + desiredContentMaxLuminance: 1000, + desiredContentMinLuminance: 0, + desiredContentMaxFrameAverageLuminance: 500, + } + : {}, + hdr10PlusGamingData: null, + dpi: 0, + }, + ], + useOps: true, + audioMode: 2, + metaData: webRtcSessionMetadata(width, height), + sdrHdrMode: hdrEnabled ? 1 : 0, + clientDisplayHdrCapabilities: hdrEnabled + ? { + version: 1, + hdrEdrSupportedFlagsInUint32: 1, + staticMetadataDescriptorId: 0, + } + : null, + surroundAudioInfo: 0, + remoteControllersBitmap: 0, + clientTimezoneOffset: timezoneOffsetMs(), + enhancedStreamMode: 1, + appLaunchMode: appLaunchModeWireValue(input.settings.appLaunchMode), + secureRTSPSupported: false, + partnerCustomData: "", + accountLinked, + enablePersistingInGameSettings: shouldEnableInGameSettingsPersistence(input), + userAge: 26, + requestedStreamingFeatures: buildRequestedStreamingFeatures( + input.settings, + bitDepth, + chromaFormat, + hdrEnabled, + ), + }, + }; +} + +/** + * Build claim/resume request payload + */ +export function buildClaimRequestBody( + sessionId: string, + appId: string, + settings: StreamSettings, + sessionAppLaunchMode?: number, + enablePersistingInGameSettings = false, +): unknown { + // For RESUME claims, we must NOT attempt to renegotiate streaming parameters. + // The session is already configured on the server side. Sending different fps, resolution, + // codec, etc. causes HTTP 400 from the server because those parameters are immutable for + // an already-streaming session. Only send the action and minimal required fields. + const deviceId = getStableDeviceId(); + const subSessionId = crypto.randomUUID(); + const timezoneMs = timezoneOffsetMs(); + + return { + action: 2, + data: "RESUME", + sessionRequestData: { + // Minimal fields required for resume - NO streaming parameter renegotiation + audioMode: 2, + remoteControllersBitmap: 0, + sdrHdrMode: 0, + networkTestSessionId: null, + availableSupportedControllers: [], + clientVersion: "30.0", + deviceHashId: deviceId, + internalTitle: null, + clientPlatformName: resolveGfnDeviceIdentity().clientPlatformName, + metaData: [ + { key: "SubSessionId", value: subSessionId }, + { key: "wssignaling", value: "1" }, + { key: "GSStreamerType", value: "WebRTC" }, + { key: "networkType", value: "Unknown" }, + { key: "ClientImeSupport", value: "0" }, + { key: "surroundAudioInfo", value: "2" }, + ], + surroundAudioInfo: 0, + clientTimezoneOffset: timezoneMs, + clientIdentification: "GFN-PC", + parentSessionId: null, + appId: parseInt(appId, 10), + streamerVersion: 1, + // Resume must not renegotiate session parameters: prefer the wire value the + // session was created with over whatever the UI toggles currently say. + appLaunchMode: sessionAppLaunchMode ?? appLaunchModeWireValue(settings.appLaunchMode), + sdkVersion: "1.0", + enhancedStreamMode: 1, + useOps: true, + clientDisplayHdrCapabilities: null, + accountLinked: true, + partnerCustomData: "", + enablePersistingInGameSettings, + secureRTSPSupported: false, + userAge: 26, + }, + metaData: [], + }; +} diff --git a/opennow-stable/src/main/platforms/gfn/cloudmatchSignaling.ts b/opennow-stable/src/main/platforms/gfn/cloudmatchSignaling.ts new file mode 100644 index 000000000..aaa27593e --- /dev/null +++ b/opennow-stable/src/main/platforms/gfn/cloudmatchSignaling.ts @@ -0,0 +1,363 @@ +import dns from "node:dns"; + +import type { IceServer, MediaConnectionInfo } from "@shared/gfn"; + +import type { CloudMatchResponse } from "./types"; +import { collectRtspsEndpoints } from "./nvstRtspProbe"; +import { isZoneHostname } from "./cloudmatchTransport"; + +const READY_SESSION_STATUSES = new Set([2, 3]); + +export function isReadySessionStatus(status: number): boolean { + return READY_SESSION_STATUSES.has(status); +} + +async function resolveHostnameWithFallback(hostname: string): Promise { + // Try system resolver first, then fall back to Cloudflare (1.1.1.1) and Google (8.8.8.8) + try { + const r = await dns.promises.lookup(hostname); + if (r && (r as any).address) return (r as any).address; + } catch { + // ignore and try custom resolvers + } + + const fallbackServers = ["1.1.1.1", "8.8.8.8"]; + for (const server of fallbackServers) { + try { + const resolver = new dns.Resolver(); + resolver.setServers([server]); + const addrs: string[] = await new Promise((resolve, reject) => { + resolver.resolve4(hostname, (err, addresses) => { + if (err) reject(err); + else resolve(addresses); + }); + }); + if (addrs && addrs.length > 0) return addrs[0]; + } catch { + // try next fallback + } + } + + return null; +} + +export async function normalizeIceServers(response: CloudMatchResponse): Promise { + const raw = response.session.iceServerConfiguration?.iceServers ?? []; + const servers = raw + .map((entry) => { + const urls = Array.isArray(entry.urls) ? entry.urls : [entry.urls]; + return { + urls, + username: entry.username, + credential: entry.credential, + }; + }) + .filter((entry) => entry.urls.length > 0); + + if (servers.length > 0) { + // Attempt to resolve any hostnames in STUN/TURN URLs to IPs to avoid relying on the + // renderer's DNS resolution. This makes it possible to try alternate DNS servers + // when the system resolver fails. + const resolvedServers: IceServer[] = []; + for (const s of servers) { + const resolvedUrls: string[] = []; + for (const u of s.urls) { + try { + const m = u.match(/^([a-zA-Z0-9+.-]+):([^/]+)/); + if (m) { + const scheme = m[1]; + const hostPort = m[2]; + const host = hostPort.split(":")[0]; + const portPart = hostPort.includes(":") ? ":" + hostPort.split(":").slice(1).join(":") : ""; + + // Helper to bracket IPv6 literals when necessary + const bracketIfIpv6 = (h: string) => { + if (h.startsWith("[") && h.endsWith("]")) return h; + // Heuristic: contains ':' and is not an IPv4 dotted-quad + if (h.includes(":") && !/^\d{1,3}(?:\.\d{1,3}){3}$/.test(h)) { + return `[${h}]`; + } + return h; + }; + + // If host already looks like an IPv4 or bracketed IPv6, keep original URL + if (/^\d{1,3}(?:\.\d{1,3}){3}$/.test(host) || /^\[[0-9a-fA-F:]+\]$/.test(host)) { + resolvedUrls.push(u); + } else { + const ip = await resolveHostnameWithFallback(host); + const finalHost = ip ?? host; + const maybeBracketted = bracketIfIpv6(finalHost); + resolvedUrls.push(`${scheme}:${maybeBracketted}${portPart}`); + } + } else { + resolvedUrls.push(u); + } + } catch { + resolvedUrls.push(u); + } + } + resolvedServers.push({ urls: resolvedUrls, username: s.username, credential: s.credential }); + } + + return resolvedServers; + } + + // Default fallbacks — try to resolve known STUN hostnames to IPs as well + const defaults = ["s1.stun.gamestream.nvidia.com:19308", "stun.l.google.com:19302", "stun1.l.google.com:19302"]; + const out: IceServer[] = []; + for (const d of defaults) { + const parts = d.split(":"); + const host = parts[0]; + const port = parts.length > 1 ? `:${parts.slice(1).join(":")}` : ""; + const ip = await resolveHostnameWithFallback(host); + const bracketIfIpv6 = (h: string) => (h.includes(":") && !h.startsWith("[") ? `[${h}]` : h); + if (ip) out.push({ urls: [`stun:${bracketIfIpv6(ip)}${port}`] }); + else out.push({ urls: [`stun:${bracketIfIpv6(host)}${port}`] }); + } + + return out; +} + +/** + * Extract the streaming server IP from the CloudMatch response, matching Rust's + * `streaming_server_ip()` priority chain: + * 1. connectionInfo[usage==14].ip (direct IP) + * 2. Host extracted from connectionInfo[usage==14].resourcePath (for rtsps:// URLs) + * 3. sessionControlInfo.ip (fallback) + */ +export function streamingServerIp(response: CloudMatchResponse): string | null { + const connections = response.session.connectionInfo ?? []; + const sigConn = connections.find((conn) => conn.usage === 14); + + if (sigConn) { + // Priority 1: Direct IP field + const rawIp = sigConn.ip; + const directIp = Array.isArray(rawIp) ? rawIp[0] : rawIp; + if (directIp && directIp.length > 0) { + return directIp; + } + + // Priority 2: Extract host from resourcePath (Alliance format: rtsps://host:port) + if (sigConn.resourcePath) { + const host = extractHostFromUrl(sigConn.resourcePath); + if (host) return host; + } + } + + // Priority 3: sessionControlInfo.ip + const controlIp = response.session.sessionControlInfo?.ip; + if (controlIp && controlIp.length > 0) { + return Array.isArray(controlIp) ? controlIp[0] : controlIp; + } + + return null; +} + +/** + * Extract host from a URL string (handles rtsps://, rtsp://, wss://, https://). + * Matches Rust's extract_host_from_url(). + */ +export function extractHostFromUrl(url: string): string | null { + const prefixes = ["rtsps://", "rtsp://", "wss://", "https://"]; + let afterProto: string | null = null; + for (const prefix of prefixes) { + if (url.startsWith(prefix)) { + afterProto = url.slice(prefix.length); + break; + } + } + if (!afterProto) return null; + + // Get host (before port or path) + const host = afterProto.split(":")[0]?.split("/")[0]; + if (!host || host.length === 0 || host.startsWith(".")) return null; + return host; +} + +export function resolveSignaling(response: CloudMatchResponse): { + serverIp: string; + signalingServer: string; + signalingUrl: string; + mediaConnectionInfo?: MediaConnectionInfo; + rtspsEndpoints: string[]; +} { + const connections = response.session.connectionInfo ?? []; + const signalingConnection = + connections.find((conn) => conn.usage === 14 && conn.ip) ?? connections.find((conn) => conn.ip); + + // Use the Rust-matching priority chain for server IP + const serverIp = streamingServerIp(response); + if (!serverIp) { + throw new Error("CloudMatch response did not include a signaling host"); + } + + const resourcePath = signalingConnection?.resourcePath ?? "/nvst/"; + + // Build signaling URL matching Rust's build_signaling_url() behavior: + // - rtsps://host:port -> extract host, convert to wss://host/nvst/ + // - wss://... -> use as-is + // - /path -> wss://serverIp:443/path + // - fallback -> wss://serverIp:443/nvst/ + const { signalingUrl, signalingHost } = buildSignalingUrl(resourcePath, serverIp); + + // Use the resolved signaling host (which may differ from serverIp if extracted from rtsps:// URL) + const effectiveHost = signalingHost ?? serverIp; + const signalingServer = effectiveHost.includes(":") + ? effectiveHost + : `${effectiveHost}:443`; + + const rtspsHost = + connections + .map((connection) => typeof connection.resourcePath === "string" + ? extractHostFromUrl(connection.resourcePath) + : null) + .find((host): host is string => Boolean(host)) ?? + signalingHost ?? + (isZoneHostname(serverIp) ? null : serverIp); + + return { + serverIp, + signalingServer, + signalingUrl, + mediaConnectionInfo: resolveMediaConnectionInfo(connections, serverIp, { + logMissing: isReadySessionStatus(response.session.status), + }), + rtspsEndpoints: collectRtspsEndpoints(connections, rtspsHost), + }; +} + +/** + * Resolve the media connection endpoint (IP + port) from the session's connectionInfo array. + * Matches Rust's media_connection_info() priority chain: + * 1. usage=2 (Primary media path, UDP) + * 2. usage=17 (Alternative media path) + * 3. usage=14 with highest port (Alliance fallback — distinguishes media port from signaling port) + * 4. Fallback: use serverIp with the highest port from any usage=14 entry + * + * For each entry, IP is extracted from: + * a. The .ip field directly + * b. The hostname in .resourcePath (e.g. rtsps://80-250-97-40.server.net:48322) + * c. Fallback to serverIp (only for usage=14 Alliance fallback) + */ +export function resolveMediaConnectionInfo( + connections: Array<{ ip?: string; port: number; usage: number; protocol?: number; resourcePath?: string }>, + serverIp: string, + options?: { logMissing?: boolean }, +): { ip: string; port: number; usage: number } | undefined { + // Helper: extract IP from a connection entry + const extractIp = (conn: { ip?: string; resourcePath?: string }): string | null => { + // Try direct IP field + const rawIp = conn.ip; + const directIp = Array.isArray(rawIp) ? rawIp[0] : rawIp; + if (directIp && directIp.length > 0) return directIp; + + // Try hostname from resourcePath + if (conn.resourcePath) { + const host = extractHostFromUrl(conn.resourcePath); + if (host) return host; + } + + return null; + }; + + // Helper: extract port from a connection entry (fallback to resourcePath URL port) + const extractPort = (conn: { port: number; resourcePath?: string }): number => { + if (conn.port > 0) return conn.port; + + // Try extracting port from resourcePath URL + if (conn.resourcePath) { + try { + const url = new URL(conn.resourcePath.replace("rtsps://", "https://").replace("rtsp://", "http://")); + const portStr = url.port; + if (portStr) return parseInt(portStr, 10); + } catch { + // Ignore + } + } + + return 0; + }; + + // Priority 1: usage=2 (Primary media path, UDP) + const primary = connections.find((c) => c.usage === 2); + if (primary) { + const ip = extractIp(primary); + const port = extractPort(primary); + console.log(`[CloudMatch] resolveMediaConnectionInfo: usage=2 candidate: ip=${ip}, port=${port}`); + if (ip && port > 0) return { ip, port, usage: primary.usage }; + } + + // Priority 2: usage=17 (Alternative media path) + const alt = connections.find((c) => c.usage === 17); + if (alt) { + const ip = extractIp(alt); + const port = extractPort(alt); + console.log(`[CloudMatch] resolveMediaConnectionInfo: usage=17 candidate: ip=${ip}, port=${port}`); + if (ip && port > 0) return { ip, port, usage: alt.usage }; + } + + // Priority 3: usage=14 with highest port (Alliance fallback) + const alliance = connections + .filter((c) => c.usage === 14) + .sort((a, b) => b.port - a.port); + + for (const conn of alliance) { + const ip = extractIp(conn) ?? serverIp; + const port = extractPort(conn); + console.log(`[CloudMatch] resolveMediaConnectionInfo: usage=14 candidate: ip=${ip}, port=${port} (serverIp fallback=${serverIp})`); + if (ip && port > 0) return { ip, port, usage: conn.usage }; + } + + if (options?.logMissing ?? true) { + console.log("[CloudMatch] resolveMediaConnectionInfo: NO valid media connection info found"); + } + return undefined; +} + +/** + * Build signaling WSS URL from the resourcePath, matching Rust implementation. + * Returns the URL and optionally the extracted host (if different from serverIp). + */ +export function buildSignalingUrl( + raw: string, + serverIp: string, +): { signalingUrl: string; signalingHost: string | null } { + if (raw.startsWith("rtsps://") || raw.startsWith("rtsp://")) { + // Extract hostname from RTSP URL, convert to wss:// + const withoutScheme = raw.startsWith("rtsps://") + ? raw.slice("rtsps://".length) + : raw.slice("rtsp://".length); + const host = withoutScheme.split(":")[0]?.split("/")[0]; + if (host && host.length > 0 && !host.startsWith(".")) { + return { + signalingUrl: `wss://${host}/nvst/`, + signalingHost: host, + }; + } + return { + signalingUrl: `wss://${serverIp}:443/nvst/`, + signalingHost: null, + }; + } + + if (raw.startsWith("wss://")) { + // Already a full WSS URL, use as-is; extract host + const withoutScheme = raw.slice("wss://".length); + const host = withoutScheme.split("/")[0] ?? null; + return { signalingUrl: raw, signalingHost: host }; + } + + if (raw.startsWith("/")) { + // Relative path + return { + signalingUrl: `wss://${serverIp}:443${raw}`, + signalingHost: null, + }; + } + + // Fallback + return { + signalingUrl: `wss://${serverIp}:443/nvst/`, + signalingHost: null, + }; +} diff --git a/opennow-stable/src/main/platforms/gfn/cloudmatchTransport.test.ts b/opennow-stable/src/main/platforms/gfn/cloudmatchTransport.test.ts new file mode 100644 index 000000000..acefa299f --- /dev/null +++ b/opennow-stable/src/main/platforms/gfn/cloudmatchTransport.test.ts @@ -0,0 +1,16 @@ +import assert from "node:assert/strict"; +import test from "node:test"; + +import { isZoneHostname } from "./cloudmatchTransport"; + +test("isZoneHostname accepts NVIDIA CloudMatch domains and their subdomains", () => { + assert.equal(isZoneHostname("cloudmatch.nvidiagrid.net"), true); + assert.equal(isZoneHostname("np-ams-06.cloudmatchbeta.nvidiagrid.net"), true); + assert.equal(isZoneHostname("NP-AMS-06.CLOUDMATCHBETA.NVIDIAGRID.NET."), true); +}); + +test("isZoneHostname rejects hostnames that only contain a CloudMatch domain substring", () => { + assert.equal(isZoneHostname("cloudmatch.nvidiagrid.net.attacker.example"), false); + assert.equal(isZoneHostname("attacker-cloudmatchbeta.nvidiagrid.net"), false); + assert.equal(isZoneHostname("cloudmatchbeta.nvidiagrid.net.evil.test"), false); +}); diff --git a/opennow-stable/src/main/platforms/gfn/cloudmatchTransport.ts b/opennow-stable/src/main/platforms/gfn/cloudmatchTransport.ts new file mode 100644 index 000000000..9eadb3457 --- /dev/null +++ b/opennow-stable/src/main/platforms/gfn/cloudmatchTransport.ts @@ -0,0 +1,195 @@ +import { buildGfnCloudMatchHeaders } from "./clientHeaders"; +import { fetchWithOptionalProxy } from "./proxyFetch"; + +export const CLOUDMATCH_REQUEST_TIMEOUT_MS = 30_000; +export const CLOUDMATCH_GET_RETRIES = 2; +export const CLOUDMATCH_RETRY_DELAYS_MS = [250, 750]; +export const CLOUDMATCH_RETRY_STATUSES = new Set([408, 425, 429, 500, 502, 503, 504]); + +export interface CloudMatchServerInfoResponse { + metaData?: Array<{ + key: string; + value: string; + }>; +} + +export interface CloudMatchFetchOptions { + proxyUrl?: string; + timeoutMs?: number; + retries?: number; +} + +export function sleep(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + +export function formatErrorForLog(error: unknown): string { + if (error instanceof Error) { + const cause = error.cause instanceof Error ? `: ${error.cause.message}` : ""; + return `${error.message}${cause}`; + } + return String(error); +} + +export async function fetchCloudMatch( + input: string, + init: RequestInit, + options: CloudMatchFetchOptions = {}, +): Promise { + const method = (init.method ?? "GET").toUpperCase(); + const retries = options.retries ?? (method === "GET" ? CLOUDMATCH_GET_RETRIES : 0); + const timeoutMs = options.timeoutMs ?? CLOUDMATCH_REQUEST_TIMEOUT_MS; + + let lastError: unknown; + for (let attempt = 0; attempt <= retries; attempt += 1) { + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), timeoutMs); + + try { + const response = await fetchWithOptionalProxy(input, { + ...init, + signal: controller.signal, + }, options.proxyUrl); + clearTimeout(timeout); + + if (attempt < retries && CLOUDMATCH_RETRY_STATUSES.has(response.status)) { + await sleep(CLOUDMATCH_RETRY_DELAYS_MS[Math.min(attempt, CLOUDMATCH_RETRY_DELAYS_MS.length - 1)] ?? 0); + continue; + } + + return response; + } catch (error) { + clearTimeout(timeout); + lastError = error; + if (attempt >= retries) { + throw error; + } + + const retryDelay = CLOUDMATCH_RETRY_DELAYS_MS[Math.min(attempt, CLOUDMATCH_RETRY_DELAYS_MS.length - 1)]; + await sleep(retryDelay ?? 0); + } + } + + throw lastError instanceof Error ? lastError : new Error(String(lastError)); +} + +export function normalizeCloudMatchBaseUrl(url: string): string { + const trimmed = url.trim(); + const withProtocol = /^https?:\/\//i.test(trimmed) ? trimmed : `https://${trimmed}`; + return withProtocol.endsWith("/") ? withProtocol.slice(0, -1) : withProtocol; +} + +export function extractServerInfoRegionBases(payload: CloudMatchServerInfoResponse): string[] { + const metadata = payload.metaData ?? []; + const byKey = new Map(metadata.map((entry) => [entry.key, entry.value])); + const regionNames = byKey.get("gfn-regions") + ?.split(",") + .map((entry) => entry.trim()) + .filter(Boolean) ?? []; + const localRegionName = byKey.get("local-region")?.trim(); + const orderedRegionNames = [ + ...(localRegionName ? [localRegionName] : []), + ...regionNames, + ]; + const bases: string[] = []; + const seen = new Set(); + + for (const regionName of orderedRegionNames) { + const regionUrl = byKey.get(regionName); + if (!regionUrl?.startsWith("http")) { + continue; + } + const normalized = normalizeCloudMatchBaseUrl(regionUrl); + if (!seen.has(normalized)) { + seen.add(normalized); + bases.push(normalized); + } + } + + return bases; +} + +export function isDefaultStreamingServiceBase(baseUrl: string): boolean { + try { + const hostname = new URL(baseUrl).hostname.toLowerCase(); + return hostname === "prod.cloudmatchbeta.nvidiagrid.net" || + (hostname.startsWith("prod.") && hostname.endsWith(".nvidiagrid.net")); + } catch { + return false; + } +} + +export async function resolveCreateSessionBase( + base: string, + token: string, + clientId: string, + deviceId: string, + proxyUrl?: string, +): Promise { + if (!isDefaultStreamingServiceBase(base)) { + return base; + } + + try { + const response = await fetchCloudMatch(`${base}/v2/serverInfo`, { + method: "GET", + headers: buildGfnCloudMatchHeaders({ token, clientId, deviceId, includeOrigin: false }), + }, { proxyUrl }); + if (!response.ok) { + return base; + } + + const [localRegionBase] = extractServerInfoRegionBases( + (await response.json()) as CloudMatchServerInfoResponse, + ); + if (!localRegionBase || localRegionBase === base) { + return base; + } + + console.log(`[CloudMatch] createSession resolved ${base} to local region ${localRegionBase}`); + return localRegionBase; + } catch (error) { + console.warn(`[CloudMatch] createSession local-region discovery failed: ${formatErrorForLog(error)}`); + return base; + } +} + +export function cloudmatchUrl(zone: string): string { + return `https://${zone}.cloudmatchbeta.nvidiagrid.net`; +} + +export function resolveStreamingBaseUrl(zone: string, provided?: string): string { + if (provided && provided.trim()) { + const trimmed = provided.trim(); + return trimmed.endsWith("/") ? trimmed.slice(0, -1) : trimmed; + } + return cloudmatchUrl(zone); +} + +export function shouldUseServerIp(baseUrl: string): boolean { + return baseUrl.includes("cloudmatchbeta.nvidiagrid.net"); +} + +/** + * Check if a given IP/hostname is a CloudMatch zone load balancer hostname + * (not a real game server IP). Zone hostnames look like: + * np-ams-06.cloudmatchbeta.nvidiagrid.net + */ +export function isZoneHostname(ip: string): boolean { + const hostname = ip.trim().toLowerCase().replace(/\.$/, ""); + return ["cloudmatchbeta.nvidiagrid.net", "cloudmatch.nvidiagrid.net"].some( + (domain) => hostname === domain || hostname.endsWith(`.${domain}`), + ); +} + +export function resolvePollStopBase(zone: string, provided?: string, serverIp?: string): string { + const base = resolveStreamingBaseUrl(zone, provided); + // Only use serverIp if it's a real server IP (not a zone hostname). + // The Rust version checks: if we're NOT an alliance partner AND we have a server_ip, use it. + // But if the "serverIp" is actually the zone hostname (from an early poll when connectionInfo + // was empty), using it is circular and doesn't help. + if (serverIp && shouldUseServerIp(base) && !isZoneHostname(serverIp)) { + return `https://${serverIp}`; + } + return base; +} diff --git a/opennow-stable/src/main/gfn/deviceId.ts b/opennow-stable/src/main/platforms/gfn/deviceId.ts similarity index 100% rename from opennow-stable/src/main/gfn/deviceId.ts rename to opennow-stable/src/main/platforms/gfn/deviceId.ts diff --git a/opennow-stable/src/main/platforms/gfn/deviceIdentity.test.ts b/opennow-stable/src/main/platforms/gfn/deviceIdentity.test.ts new file mode 100644 index 000000000..c4d0994d5 --- /dev/null +++ b/opennow-stable/src/main/platforms/gfn/deviceIdentity.test.ts @@ -0,0 +1,79 @@ +/// + +import test from "node:test"; +import assert from "node:assert/strict"; + +import { + buildGfnCloudMatchHeaders, + buildGfnGraphQlHeaders, + buildGfnLcarsHeaders, +} from "./clientHeaders"; +import { + STEAM_DECK_DEVICE_IDENTITY, + configureIdentifyAsSteamDeck, + resolveGfnDeviceIdentity, +} from "./deviceIdentity"; + +test("resolveGfnDeviceIdentity defaults to host desktop DESKTOP/UNKNOWN", () => { + configureIdentifyAsSteamDeck(() => false); + const identity = resolveGfnDeviceIdentity({ identifyAsSteamDeck: false, platform: "win32" }); + assert.equal(identity.deviceOs, "WINDOWS"); + assert.equal(identity.deviceType, "DESKTOP"); + assert.equal(identity.deviceMake, "UNKNOWN"); + assert.equal(identity.deviceModel, "UNKNOWN"); + assert.equal(identity.clientPlatformName, "windows"); +}); + +test("resolveGfnDeviceIdentity Steam Deck profile matches official headers", () => { + const identity = resolveGfnDeviceIdentity({ identifyAsSteamDeck: true }); + assert.deepEqual(identity, STEAM_DECK_DEVICE_IDENTITY); + assert.equal(identity.deviceOs, "STEAMOS"); + assert.equal(identity.deviceType, "CONSOLE"); + assert.equal(identity.deviceMake, "VALVE"); + assert.equal(identity.deviceModel, "STEAMDECK"); + assert.equal(identity.clientPlatformName, "SteamOS"); +}); + +test("CloudMatch/LCARS/GraphQL headers honor Steam Deck identity", () => { + configureIdentifyAsSteamDeck(() => true); + + const cloudMatch = buildGfnCloudMatchHeaders({ + token: "token", + clientId: "client", + deviceId: "device", + }); + assert.equal(cloudMatch["nv-device-os"], "STEAMOS"); + assert.equal(cloudMatch["nv-device-type"], "CONSOLE"); + assert.equal(cloudMatch["nv-device-make"], "VALVE"); + assert.equal(cloudMatch["nv-device-model"], "STEAMDECK"); + + const lcars = buildGfnLcarsHeaders({ + token: "token", + clientType: "NATIVE", + clientStreamer: "NVIDIA-CLASSIC", + }); + assert.equal(lcars["nv-device-os"], "STEAMOS"); + assert.equal(lcars["nv-device-type"], "CONSOLE"); + assert.equal(lcars["nv-device-make"], "VALVE"); + assert.equal(lcars["nv-device-model"], "STEAMDECK"); + + const graphQl = buildGfnGraphQlHeaders("token"); + assert.equal(graphQl["nv-device-os"], "STEAMOS"); + assert.equal(graphQl["nv-device-type"], "CONSOLE"); + assert.equal(graphQl["nv-device-make"], "VALVE"); + assert.equal(graphQl["nv-device-model"], "STEAMDECK"); + + configureIdentifyAsSteamDeck(() => false); +}); + +test("explicit identifyAsSteamDeck option overrides settings reader", () => { + configureIdentifyAsSteamDeck(() => false); + const headers = buildGfnCloudMatchHeaders({ + token: "token", + clientId: "client", + deviceId: "device", + identifyAsSteamDeck: true, + }); + assert.equal(headers["nv-device-os"], "STEAMOS"); + assert.equal(headers["nv-device-type"], "CONSOLE"); +}); diff --git a/opennow-stable/src/main/platforms/gfn/deviceIdentity.ts b/opennow-stable/src/main/platforms/gfn/deviceIdentity.ts new file mode 100644 index 000000000..32ad34bed --- /dev/null +++ b/opennow-stable/src/main/platforms/gfn/deviceIdentity.ts @@ -0,0 +1,85 @@ +/** + * GFN device identity profiles. + * + * Official Steam Deck CEF still uses clientIdentification/MES serviceName `gfn_pc`, + * but advertises itself via nv-device-* headers and clientPlatformName. OpenNOW only + * used Steam Deck headers for QR device-login; this module owns the stream/MES profile. + */ + +export type GfnDeviceOs = "WINDOWS" | "MACOS" | "LINUX" | "STEAMOS"; +export type GfnDeviceType = "DESKTOP" | "CONSOLE"; + +export interface GfnDeviceIdentity { + deviceOs: GfnDeviceOs; + deviceType: GfnDeviceType; + deviceMake: string; + deviceModel: string; + /** CloudMatch session / nettest `clientPlatformName` */ + clientPlatformName: string; +} + +// OpenNOW historically always sent clientPlatformName "windows" on the desktop +// CloudMatch path (even on macOS/Linux). Keep that unless Steam Deck spoof is on. +const DESKTOP_IDENTITY_BY_PLATFORM: Record<"win32" | "darwin" | "linux", GfnDeviceIdentity> = { + win32: { + deviceOs: "WINDOWS", + deviceType: "DESKTOP", + deviceMake: "UNKNOWN", + deviceModel: "UNKNOWN", + clientPlatformName: "windows", + }, + darwin: { + deviceOs: "MACOS", + deviceType: "DESKTOP", + deviceMake: "UNKNOWN", + deviceModel: "UNKNOWN", + clientPlatformName: "windows", + }, + linux: { + deviceOs: "LINUX", + deviceType: "DESKTOP", + deviceMake: "UNKNOWN", + deviceModel: "UNKNOWN", + clientPlatformName: "windows", + }, +}; + +/** Matches official Steam Deck / SteamOS mall device headers (VALVE + STEAMDECK + CONSOLE). */ +export const STEAM_DECK_DEVICE_IDENTITY: Readonly = Object.freeze({ + deviceOs: "STEAMOS", + deviceType: "CONSOLE", + deviceMake: "VALVE", + deviceModel: "STEAMDECK", + clientPlatformName: "SteamOS", +}); + +let identifyAsSteamDeckReader: (() => boolean) | null = null; + +/** Wire from main after settings load so header builders stay free of settings imports. */ +export function configureIdentifyAsSteamDeck(reader: () => boolean): void { + identifyAsSteamDeckReader = reader; +} + +export function isIdentifyAsSteamDeckEnabled(): boolean { + return identifyAsSteamDeckReader?.() ?? false; +} + +export function resolveHostDesktopIdentity( + platform: NodeJS.Platform = process.platform, +): GfnDeviceIdentity { + if (platform === "win32" || platform === "darwin" || platform === "linux") { + return DESKTOP_IDENTITY_BY_PLATFORM[platform]; + } + return DESKTOP_IDENTITY_BY_PLATFORM.linux; +} + +export function resolveGfnDeviceIdentity(options?: { + identifyAsSteamDeck?: boolean; + platform?: NodeJS.Platform; +}): GfnDeviceIdentity { + const identifyAsSteamDeck = options?.identifyAsSteamDeck ?? isIdentifyAsSteamDeckEnabled(); + if (identifyAsSteamDeck) { + return STEAM_DECK_DEVICE_IDENTITY; + } + return resolveHostDesktopIdentity(options?.platform); +} diff --git a/opennow-stable/src/main/gfn/errorCodes.test.ts b/opennow-stable/src/main/platforms/gfn/errorCodes.test.ts similarity index 100% rename from opennow-stable/src/main/gfn/errorCodes.test.ts rename to opennow-stable/src/main/platforms/gfn/errorCodes.test.ts diff --git a/opennow-stable/src/main/platforms/gfn/errorCodes.ts b/opennow-stable/src/main/platforms/gfn/errorCodes.ts new file mode 100644 index 000000000..0bc665baa --- /dev/null +++ b/opennow-stable/src/main/platforms/gfn/errorCodes.ts @@ -0,0 +1,331 @@ +import type { SessionErrorInfo } from "@shared/sessionError"; +import { GfnErrorCode } from "./gfnErrorCodeEnum"; +import { ERROR_MESSAGES } from "./gfnErrorMessages"; + +export { GfnErrorCode } from "./gfnErrorCodeEnum"; +export { ERROR_MESSAGES } from "./gfnErrorMessages"; +export type { ErrorMessageEntry } from "./gfnErrorMessages"; + +/** + * CloudMatch error codes. + * + * These mappings provide user-friendly messages for session failures. + */ + +/** CloudMatch error response structure */ +interface CloudMatchErrorResponse { + requestStatus?: { + statusCode?: number; + statusDescription?: string; + unifiedErrorCode?: number; + }; + session?: { + sessionId?: string; + errorCode?: number; + }; +} + +/** Session error class for parsing and handling CloudMatch errors */ +export class SessionError extends Error { + /** HTTP status code */ + public readonly httpStatus: number; + /** CloudMatch status code from requestStatus.statusCode */ + public readonly statusCode: number; + /** Status description from requestStatus.statusDescription */ + public readonly statusDescription?: string; + /** Unified error code from requestStatus.unifiedErrorCode */ + public readonly unifiedErrorCode?: number; + /** Session error code from session.errorCode */ + public readonly sessionErrorCode?: number; + /** Computed service error code */ + public readonly gfnErrorCode: number; + /** User-friendly title */ + public readonly title: string; + + constructor(info: SessionErrorInfo) { + super(info.description); + this.name = "SessionError"; + this.httpStatus = info.httpStatus; + this.statusCode = info.statusCode; + this.statusDescription = info.statusDescription; + this.unifiedErrorCode = info.unifiedErrorCode; + this.sessionErrorCode = info.sessionErrorCode; + this.gfnErrorCode = info.gfnErrorCode; + this.title = info.title; + } + + /** Get error type as a string (e.g., "SessionLimitExceeded") */ + get errorType(): string { + // Try to find the enum name from the error code + const entry = Object.entries(GfnErrorCode).find(([, value]) => value === this.gfnErrorCode); + if (entry) { + return entry[0]; + } + // Fallback to status code based naming + if (this.statusCode > 0) { + return `StatusCode${this.statusCode}`; + } + return "UnknownError"; + } + + /** Get user-friendly error message */ + get errorDescription(): string { + return this.message; + } + + /** + * Parse error from CloudMatch response JSON + */ + static fromResponse(httpStatus: number, responseBody: string): SessionError { + let json: CloudMatchErrorResponse = {}; + + try { + json = JSON.parse(responseBody) as CloudMatchErrorResponse; + } catch { + // Parsing failed, use empty object + } + + // Extract fields + const statusCode = json.requestStatus?.statusCode ?? 0; + const statusDescription = json.requestStatus?.statusDescription; + const unifiedErrorCode = json.requestStatus?.unifiedErrorCode; + const sessionErrorCode = json.session?.errorCode; + + // Compute normalized service error code + const gfnErrorCode = SessionError.computeErrorCode(statusCode, unifiedErrorCode); + + // Get user-friendly message + const { title, description } = SessionError.getErrorMessage( + gfnErrorCode, + statusDescription, + httpStatus, + ); + + return new SessionError({ + httpStatus, + statusCode, + statusDescription, + unifiedErrorCode, + sessionErrorCode, + gfnErrorCode, + title, + description, + }); + } + + /** + * Compute service error code from CloudMatch response + */ + private static computeErrorCode(statusCode: number, unifiedErrorCode?: number): number { + // Base error code + let errorCode: number = 3237093632; // SessionServerErrorBegin + + // Convert statusCode to error code + if (statusCode === 1) { + errorCode = 15859712; // Success + } else if (statusCode > 0 && statusCode < 255) { + errorCode = 3237093632 + statusCode; + } + + // Use unifiedErrorCode if available and error_code is generic + if (unifiedErrorCode !== undefined) { + switch (errorCode) { + case 3237093632: // SessionServerErrorBegin + case 3237093636: // ServerInternalError + case 3237093381: // InvalidServerResponse + errorCode = unifiedErrorCode; + break; + } + } + + return errorCode; + } + + /** + * Get user-friendly error message + */ + private static getErrorMessage( + errorCode: number, + statusDescription: string | undefined, + httpStatus: number, + ): { title: string; description: string } { + // Check for known error code + const knownError = ERROR_MESSAGES.get(errorCode); + if (knownError) { + return knownError; + } + + // Parse status description for known patterns + if (statusDescription) { + const descUpper = statusDescription.toUpperCase(); + + if (descUpper.includes("INSUFFICIENT_PLAYABILITY")) { + return { + title: "Membership Upgrade Required", + description: + "Your current GeForce NOW membership is not high enough to play this game. Upgrade to a higher tier and try again.", + }; + } + + if (descUpper.includes("SESSION_LIMIT")) { + return { + title: "Session Limit Exceeded", + description: "You have reached your maximum number of concurrent sessions.", + }; + } + + if (descUpper.includes("MAINTENANCE")) { + return { + title: "Under Maintenance", + description: "The service is currently under maintenance. Please try again later.", + }; + } + + if (descUpper.includes("CAPACITY") || descUpper.includes("QUEUE")) { + return { + title: "No Capacity Available", + description: "All gaming rigs are currently in use. Please try again later.", + }; + } + + if (descUpper.includes("AUTH") || descUpper.includes("TOKEN")) { + return { + title: "Authentication Error", + description: "Please log in again.", + }; + } + + if (descUpper.includes("ENTITLEMENT")) { + return { + title: "Access Denied", + description: "You don't have access to this game or service.", + }; + } + } + + // Fallback based on HTTP status + switch (httpStatus) { + case 401: + return { + title: "Unauthorized", + description: "Please log in again.", + }; + case 403: + return { + title: "Access Denied", + description: "Access to this resource was denied.", + }; + case 404: + return { + title: "Not Found", + description: "The requested resource was not found.", + }; + case 429: + return { + title: "Too Many Requests", + description: "Please wait a moment and try again.", + }; + } + + if (httpStatus >= 500 && httpStatus < 600) { + return { + title: "Server Error", + description: "A server error occurred. Please try again later.", + }; + } + + return { + title: "Error", + description: `An error occurred (HTTP ${httpStatus}).`, + }; + } + + /** + * Check if this error indicates another session is running + */ + isSessionConflict(): boolean { + const sessionConflictCodes = [ + GfnErrorCode.SessionLimitExceeded, // 3237093643 + GfnErrorCode.SessionLimitPerDeviceReached, // 3237093682 + GfnErrorCode.MaxSessionNumberLimitExceeded, // 3237093715 + ]; + + if (sessionConflictCodes.includes(this.gfnErrorCode)) { + return true; + } + + return false; + } + + /** + * Check if this is a temporary error that might resolve with retry + */ + isRetryable(): boolean { + const retryableCodes = [ + GfnErrorCode.NetworkError, // 3237089282 + GfnErrorCode.ServerInternalTimeout, // 3237093635 + GfnErrorCode.ServerInternalError, // 3237093636 + GfnErrorCode.ForwardingZoneOutOfCapacity, // 3237093683 + GfnErrorCode.InsufficientVmCapacity, // 3237093690 + GfnErrorCode.SessionRejectedNoCapacity, // 3237093717 + GfnErrorCode.ConnectionTimeout, // 3237101584 + GfnErrorCode.DataReceiveTimeout, // 3237101585 + GfnErrorCode.PeerNoResponse, // 3237101586 + ]; + + return retryableCodes.includes(this.gfnErrorCode); + } + + /** + * Check if user needs to log in again + */ + needsReauth(): boolean { + const reauthCodes = [ + GfnErrorCode.AuthTokenNotUpdated, // 3237093377 + GfnErrorCode.AuthTokenUpdateTimeout, // 3237093387 + GfnErrorCode.AuthFailure, // 3237093646 + GfnErrorCode.InvalidAuthenticationMalformed, // 3237093647 + GfnErrorCode.InvalidAuthenticationExpired, // 3237093648 + GfnErrorCode.InvalidAuthenticationNotFound, // 3237093649 + GfnErrorCode.InvalidAuthenticationUnsupportedProtocol, // 3237093668 + GfnErrorCode.InvalidAuthenticationUnknownToken, // 3237093669 + GfnErrorCode.InvalidAuthenticationCredentials, // 3237093670 + ]; + + if (reauthCodes.includes(this.gfnErrorCode)) { + return true; + } + + if (this.httpStatus === 401) { + return true; + } + + return false; + } + + /** + * Convert to a plain object for serialization + */ + toJSON(): SessionErrorInfo { + return { + httpStatus: this.httpStatus, + statusCode: this.statusCode, + statusDescription: this.statusDescription, + unifiedErrorCode: this.unifiedErrorCode, + sessionErrorCode: this.sessionErrorCode, + gfnErrorCode: this.gfnErrorCode, + title: this.title, + description: this.message, + }; + } +} + +/** Helper function to check if an error is a SessionError */ +export function isSessionError(error: unknown): error is SessionError { + return error instanceof SessionError; +} + +/** Helper function to parse error from CloudMatch response */ +export function parseCloudMatchError(httpStatus: number, responseBody: string): SessionError { + return SessionError.fromResponse(httpStatus, responseBody); +} diff --git a/opennow-stable/src/main/platforms/gfn/gameAppMapper.test.ts b/opennow-stable/src/main/platforms/gfn/gameAppMapper.test.ts new file mode 100644 index 000000000..8125eb71b --- /dev/null +++ b/opennow-stable/src/main/platforms/gfn/gameAppMapper.test.ts @@ -0,0 +1,112 @@ +/// + +import test from "node:test"; +import assert from "node:assert/strict"; + +import { appToGame, dedupeGames } from "./gameAppMapper"; + +test("appToGame marks owned-but-unselected variants as inLibrary", () => { + const game = appToGame({ + id: "game-1", + title: "Owned Game", + variants: [ + { + id: "v1", + appStore: "Steam", + gfn: { + library: { + status: "PLATFORM_SYNC", + selected: false, + }, + }, + }, + { + id: "v2", + appStore: "Epic", + gfn: { + library: { + status: "NOT_OWNED", + selected: false, + }, + }, + }, + ], + }); + + assert.equal(game.isInLibrary, true); + assert.equal(game.variants[0]?.inLibrary, true); + assert.equal(game.variants[0]?.librarySelected, false); + assert.equal(game.variants[1]?.inLibrary, false); +}); + +test("dedupeGames preserves ownership metadata when merging same-id variants", () => { + const [merged] = dedupeGames([ + { + id: "game-1", + title: "Game", + selectedVariantIndex: 0, + isInLibrary: true, + variants: [{ + id: "v1", + store: "Steam", + supportedControls: [], + inLibrary: true, + libraryStatus: "MANUAL", + librarySelected: true, + }], + }, + { + id: "game-1", + title: "Game", + selectedVariantIndex: 0, + variants: [{ + id: "v1", + store: "Steam", + supportedControls: ["KEYBOARD_MOUSE"], + storeUrl: "https://store.example/steam", + }], + }, + ]); + + assert.equal(merged?.isInLibrary, true); + assert.equal(merged?.variants[0]?.inLibrary, true); + assert.equal(merged?.variants[0]?.libraryStatus, "MANUAL"); + assert.equal(merged?.variants[0]?.librarySelected, true); + assert.equal(merged?.variants[0]?.storeUrl, "https://store.example/steam"); + assert.deepEqual(merged?.variants[0]?.supportedControls, ["KEYBOARD_MOUSE"]); +}); + +test("dedupeGames accepts an explicit NOT_OWNED status from a later view", () => { + const [merged] = dedupeGames([ + { + id: "game-1", + title: "Game", + selectedVariantIndex: 0, + isInLibrary: true, + variants: [{ + id: "v1", + store: "Steam", + supportedControls: [], + inLibrary: true, + libraryStatus: "MANUAL", + }], + }, + { + id: "game-1", + title: "Game", + selectedVariantIndex: 0, + isInLibrary: false, + variants: [{ + id: "v1", + store: "Steam", + supportedControls: [], + inLibrary: false, + libraryStatus: "NOT_OWNED", + }], + }, + ]); + + assert.equal(merged?.isInLibrary, false); + assert.equal(merged?.variants[0]?.inLibrary, false); + assert.equal(merged?.variants[0]?.libraryStatus, "NOT_OWNED"); +}); diff --git a/opennow-stable/src/main/platforms/gfn/gameAppMapper.ts b/opennow-stable/src/main/platforms/gfn/gameAppMapper.ts new file mode 100644 index 000000000..60f3ef2df --- /dev/null +++ b/opennow-stable/src/main/platforms/gfn/gameAppMapper.ts @@ -0,0 +1,649 @@ +import type { + GameCatalogSkuStrings, + GameInfo, + GameVariant, +} from "@shared/gfn"; +import { isOwnedLibraryStatus, normalizeGameStore } from "@shared/gfn"; +import { + buildGfnGraphQlHeaders, + buildGfnLcarsHeaders, +} from "./clientHeaders"; +import { supportsInGameSettingsPersistence } from "./gameFeatures"; +import { fetchLcarsGraphQl } from "./lcarsGraphql"; +import { fetchWithOptionalProxy } from "./proxyFetch"; +import type { AppsPageResponse } from "./paginatedApps"; + +export const GRAPHQL_URL = "https://games.geforce.com/graphql"; +export const DEFAULT_LOCALE = "en_US"; +export const DEFAULT_CLOUDMATCH_BASE_URL = "https://prod.cloudmatchbeta.nvidiagrid.net/"; +export const GFN_FEATURE_FIELDS = ` + __typename + ... on GfnSubscriptionFeatureValue { + key + value + } + ... on GfnSubscriptionFeatureValueList { + key + values + } +`; + +export interface AppData { + id: string; + title: string; + shortName?: string; + description?: string; + longDescription?: string; + developerName?: string; + features?: unknown[]; + gameFeatures?: unknown[]; + appFeatures?: unknown[]; + genres?: unknown[]; + tags?: unknown[]; + supportedControls?: unknown[]; + nvidiaTech?: unknown[]; + maxLocalPlayers?: number; + maxOnlinePlayers?: number; + images?: Record; + publisherName?: string; + contentRatings?: unknown[]; + variants?: Array<{ + id: string; + appStore: string; + storeUrl?: string; + supportedControls?: string[]; + gfn?: { + status?: string; + features?: unknown; + library?: { + status?: string; + selected?: boolean; + lastPlayedDate?: string; + }; + }; + }>; + gfn?: { + playType?: string; + playabilityState?: string; + minimumMembershipTierLabel?: string; + catalogSkuStrings?: GameCatalogSkuStrings; + }; + itemMetadata?: { + campaignIds?: string[]; + }; +} + +export interface GraphQlResponse { + data?: { + panels: Array<{ + id?: string; + name: string; + sections: Array<{ + id?: string; + title?: string; + items: Array<{ + __typename: string; + app?: AppData; + }>; + }>; + }>; + }; + errors?: Array<{ message: string }>; +} + +export interface AppMetaDataResponse { + data?: { + apps: { + items: AppData[]; + }; + }; + errors?: Array<{ message: string }>; +} + +export type AppsPage = AppsPageResponse; + +interface ServerInfoResponse { + requestStatus?: { + serverId?: string; + }; +} + +interface AppResolution { + numericAppId?: string; + preferredVariantId?: string; + selectedVariantIndex: number; + lastPlayed?: string; + isInLibrary: boolean; +} + +const LANDSCAPE_IMAGE_KEYS = ["MARQUEE_HERO_IMAGE", "HERO_IMAGE", "TV_BANNER", "FEATURE_IMAGE", "KEY_IMAGE", "KEY_ART"] as const; +const POSTER_IMAGE_KEYS = ["GAME_BOX_ART", "KEY_IMAGE", "KEY_ART"] as const; + +function optimizeImage(url: string, width = 272): string { + if (url.includes("img.nvidiagrid.net")) { + return `${url};f=webp;w=${width}`; + } + return url; +} + +function normalizeImageValues(value: string | string[] | undefined, width: number): string[] { + const values = Array.isArray(value) ? value : value ? [value] : []; + return [...new Set(values.map((url) => url.trim()).filter(Boolean).map((url) => optimizeImage(url, width)))]; +} + +function getFirstImage(images: AppData["images"], keys: readonly string[], width: number): string | undefined { + if (!images) return undefined; + for (const key of keys) { + const value = normalizeImageValues(images[key], width)[0]; + if (value) return value; + } + return undefined; +} + +function getImageUrlsByType(images: AppData["images"]): Record | undefined { + if (!images) return undefined; + const entries = Object.entries(images) + .map(([key, value]) => [key, normalizeImageValues(value, 1200)] as const) + .filter(([, urls]) => urls.length > 0); + return entries.length > 0 ? Object.fromEntries(entries) : undefined; +} + +export function isNumericId(value: string | undefined): value is string { + if (!value) { + return false; + } + return /^\d+$/.test(value); +} + +export async function postGraphQl( + query: string, + variables: Record, + token?: string, + proxyUrl?: string, +): Promise { + const response = await fetchWithOptionalProxy(GRAPHQL_URL, { + method: "POST", + headers: buildGfnGraphQlHeaders(token), + body: JSON.stringify({ query, variables }), + }, proxyUrl); + + if (!response.ok) { + const text = await response.text(); + throw new Error(`GFN GraphQL failed (${response.status}): ${text.slice(0, 400)}`); + } + + return (await response.json()) as T; +} + +export async function getVpcId(token: string, providerStreamingBaseUrl?: string, proxyUrl?: string): Promise { + let validatedBaseUrl: URL; + try { + const candidate = new URL(providerStreamingBaseUrl?.trim() || DEFAULT_CLOUDMATCH_BASE_URL); + const hostname = candidate.hostname.toLowerCase(); + if ( + candidate.protocol !== "https:" || + ( + hostname !== "prod.cloudmatchbeta.nvidiagrid.net" && + hostname !== "img.nvidiagrid.net" && + !hostname.endsWith(".geforcenow.nvidiagrid.net") + ) + ) { + validatedBaseUrl = new URL(DEFAULT_CLOUDMATCH_BASE_URL); + } else { + validatedBaseUrl = candidate; + } + } catch { + validatedBaseUrl = new URL(DEFAULT_CLOUDMATCH_BASE_URL); + } + + const serverInfoUrl = new URL("v2/serverInfo", validatedBaseUrl); + + const response = await fetchWithOptionalProxy(serverInfoUrl.toString(), { + headers: buildGfnLcarsHeaders({ + token, + clientType: "NATIVE", + clientStreamer: "NVIDIA-CLASSIC", + includeUserAgent: true, + includeEmptyTokenAuthorization: true, + }), + }, proxyUrl); + + if (!response.ok) { + return "GFN-PC"; + } + + const payload = (await response.json()) as ServerInfoResponse; + return payload.requestStatus?.serverId ?? "GFN-PC"; +} + +function parseFeatureLabel(value: unknown): string | null { + if (typeof value === "string") { + const trimmed = value.trim(); + return trimmed.length > 0 ? trimmed : null; + } + if (value && typeof value === "object") { + const candidate = value as Record; + const keys = ["name", "label", "title", "displayName"]; + for (const key of keys) { + const raw = candidate[key]; + if (typeof raw === "string") { + const trimmed = raw.trim(); + if (trimmed.length > 0) { + return trimmed; + } + } + } + } + return null; +} + +function extractFeatureLabels(app: AppData): string[] { + const buckets: unknown[] = [ + app.features, + app.gameFeatures, + app.appFeatures, + app.genres, + app.tags, + app.gfn?.catalogSkuStrings?.SKU_BASED_TAG, + ]; + + const labels: string[] = []; + for (const bucket of buckets) { + if (!Array.isArray(bucket)) { + continue; + } + for (const entry of bucket) { + const label = parseFeatureLabel(entry); + if (label) { + labels.push(label); + } + } + } + + return [...new Set(labels)]; +} + +function extractGenres(app: AppData): string[] { + if (!Array.isArray(app.genres)) { + return []; + } + + const genres: string[] = []; + for (const entry of app.genres) { + const genre = parseFeatureLabel(entry); + if (genre) { + genres.push(genre); + } + } + + return [...new Set(genres)]; +} + +function extractContentRatings(app: AppData): string[] { + if (!Array.isArray(app.contentRatings)) { + return []; + } + + const labels: string[] = []; + for (const entry of app.contentRatings) { + const label = parseFeatureLabel(entry); + if (label) { + labels.push(label); + } + } + + return [...new Set(labels)]; +} + +function extractStringValues(value: unknown): string[] { + if (!Array.isArray(value)) return []; + return [...new Set(value + .map((entry) => typeof entry === "string" ? entry.trim() : parseFeatureLabel(entry)) + .filter((entry): entry is string => typeof entry === "string" && entry.length > 0))]; +} + +function buildSearchText(title: string, variants: GameVariant[], genres: string[], featureLabels: string[], publisherName?: string, developerName?: string): string { + const stores = variants.map((variant) => variant.store); + return [title, publisherName, developerName, ...stores, ...genres, ...featureLabels] + .filter((value): value is string => typeof value === "string" && value.trim().length > 0) + .join(" ") + .toLowerCase(); +} + +export function resolveAppData(app: AppData): AppResolution { + const variants = app.variants ?? []; + const selectedVariantIndex = variants.findIndex((variant) => variant.gfn?.library?.selected === true); + const preferredVariant = selectedVariantIndex >= 0 ? variants[selectedVariantIndex] : undefined; + const numericVariants = variants.filter((variant) => isNumericId(variant.id)); + const preferredNumericVariant = preferredVariant && isNumericId(preferredVariant.id) ? preferredVariant.id : undefined; + const fallbackNumericVariant = numericVariants[0]?.id; + const numericAppId = preferredNumericVariant ?? fallbackNumericVariant ?? (isNumericId(app.id) ? app.id : undefined); + const preferredVariantId = preferredVariant?.id ?? numericAppId ?? variants[0]?.id ?? app.id; + const lastPlayed = variants + .map((variant) => variant.gfn?.library?.lastPlayedDate) + .find((value): value is string => typeof value === "string" && value.length > 0); + const isInLibrary = variants.some((variant) => isOwnedLibraryStatus(variant.gfn?.library?.status)); + + return { + numericAppId, + preferredVariantId, + selectedVariantIndex: selectedVariantIndex >= 0 ? selectedVariantIndex : Math.max(0, variants.findIndex((variant) => variant.id === preferredVariantId)), + lastPlayed, + isInLibrary, + }; +} + +export function appToVariants(app: AppData): GameVariant[] { + return app.variants?.map((variant) => { + const supportsPersistence = supportsInGameSettingsPersistence(variant); + const libraryStatus = variant.gfn?.library?.status; + return { + id: variant.id, + store: variant.appStore, + storeUrl: variant.storeUrl, + supportedControls: variant.supportedControls ?? [], + ...(supportsPersistence ? { supportsInGameSettingsPersistence: true } : {}), + librarySelected: variant.gfn?.library?.selected, + // Owned via PLATFORM_SYNC/MANUAL/IN_LIBRARY even when not the selected variant. + inLibrary: isOwnedLibraryStatus(libraryStatus), + libraryStatus, + lastPlayedDate: variant.gfn?.library?.lastPlayedDate, + gfnStatus: variant.gfn?.status, + }; + }) ?? []; +} + +export function appToGame(app: AppData): GameInfo { + const variants = appToVariants(app); + const resolution = resolveAppData(app); + const heroImageUrl = getFirstImage(app.images, LANDSCAPE_IMAGE_KEYS, 1200); + const posterImageUrl = getFirstImage(app.images, POSTER_IMAGE_KEYS, 900); + const imageUrl = heroImageUrl ?? posterImageUrl; + const screenshotUrls = normalizeImageValues(app.images?.SCREENSHOTS, 720); + const genres = extractGenres(app); + const featureLabels = extractFeatureLabels(app); + const supportedControls = extractStringValues(app.supportedControls); + const nvidiaTech = extractStringValues(app.nvidiaTech); + + return { + id: app.id, + uuid: app.id, + launchAppId: resolution.numericAppId, + title: app.title, + shortName: app.shortName, + description: app.description, + longDescription: app.longDescription, + developerName: app.developerName, + maxLocalPlayers: app.maxLocalPlayers, + maxOnlinePlayers: app.maxOnlinePlayers, + featureLabels, + genres, + supportedControls: supportedControls.length > 0 ? supportedControls : undefined, + nvidiaTech: nvidiaTech.length > 0 ? nvidiaTech : undefined, + imageUrl, + heroImageUrl, + screenshotUrl: screenshotUrls[0], + screenshotUrls: screenshotUrls.length > 0 ? screenshotUrls : undefined, + imageUrlsByType: getImageUrlsByType(app.images), + playType: app.gfn?.playType, + membershipTierLabel: app.gfn?.minimumMembershipTierLabel, + catalogSkuStrings: app.gfn?.catalogSkuStrings, + publisherName: app.publisherName, + contentRatings: extractContentRatings(app), + playabilityState: app.gfn?.playabilityState, + availableStores: [...new Set(variants.map((variant) => variant.store).filter(Boolean))], + searchText: buildSearchText(app.title, variants, genres, featureLabels, app.publisherName, app.developerName), + lastPlayed: resolution.lastPlayed, + isInLibrary: resolution.isInLibrary, + selectedVariantIndex: Math.max(0, Math.min(resolution.selectedVariantIndex, Math.max(variants.length - 1, 0))), + variants, + }; +} + +function resolveMergedLibraryOwnership( + primary?: Pick, + fallback?: Pick, +): { inLibrary: boolean; libraryStatus?: string } { + const libraryStatus = primary?.libraryStatus ?? fallback?.libraryStatus; + if (libraryStatus !== undefined) { + return { + libraryStatus, + inLibrary: isOwnedLibraryStatus(libraryStatus), + }; + } + return { + libraryStatus, + inLibrary: Boolean(primary?.inLibrary || fallback?.inLibrary), + }; +} + +function mergeAppMetaIntoGame(game: GameInfo, app: AppData): GameInfo { + const merged = appToGame(app); + const selectedVariantId = game.variants[game.selectedVariantIndex]?.id; + const variants = merged.variants.map((variant) => { + const existing = game.variants.find((candidate) => candidate.id === variant.id); + const ownership = resolveMergedLibraryOwnership(variant, existing); + return { + ...variant, + librarySelected: variant.librarySelected ?? existing?.librarySelected, + inLibrary: ownership.inLibrary, + libraryStatus: ownership.libraryStatus, + lastPlayedDate: variant.lastPlayedDate ?? existing?.lastPlayedDate, + }; + }); + const selectedVariantIndex = selectedVariantId + ? variants.findIndex((variant) => variant.id === selectedVariantId) + : -1; + + return { + ...game, + ...merged, + id: game.id, + isInLibrary: merged.isInLibrary || game.isInLibrary || variants.some((variant) => variant.inLibrary), + lastPlayed: merged.lastPlayed ?? game.lastPlayed, + variants, + selectedVariantIndex: selectedVariantIndex >= 0 ? selectedVariantIndex : merged.selectedVariantIndex, + }; +} + +function mergeGameVariants(existing: GameVariant, incoming: GameVariant): GameVariant { + const ownership = resolveMergedLibraryOwnership(incoming, existing); + return { + ...existing, + ...incoming, + storeUrl: incoming.storeUrl ?? existing.storeUrl, + supportedControls: + incoming.supportedControls.length > 0 + ? incoming.supportedControls + : existing.supportedControls, + supportsInGameSettingsPersistence: + incoming.supportsInGameSettingsPersistence ?? existing.supportsInGameSettingsPersistence, + librarySelected: incoming.librarySelected ?? existing.librarySelected, + inLibrary: ownership.inLibrary, + libraryStatus: ownership.libraryStatus, + lastPlayedDate: incoming.lastPlayedDate ?? existing.lastPlayedDate, + gfnStatus: incoming.gfnStatus ?? existing.gfnStatus, + }; +} + +export function dedupeGames(games: GameInfo[]): GameInfo[] { + const byId = new Map(); + + for (const game of games) { + const existing = byId.get(game.id); + if (!existing) { + byId.set(game.id, game); + continue; + } + + const mergedVariants = new Map(); + for (const variant of [...existing.variants, ...game.variants]) { + const prior = mergedVariants.get(variant.id); + mergedVariants.set(variant.id, prior ? mergeGameVariants(prior, variant) : variant); + } + + const mergedVariantList = [...mergedVariants.values()]; + const hasResolvedLibraryStatus = mergedVariantList.some( + (variant) => variant.libraryStatus !== undefined, + ); + const merged: GameInfo = { + ...existing, + ...game, + id: existing.id, + uuid: existing.uuid ?? game.uuid, + launchAppId: existing.launchAppId ?? game.launchAppId, + title: existing.title || game.title, + shortName: existing.shortName ?? game.shortName, + description: existing.description ?? game.description, + longDescription: existing.longDescription ?? game.longDescription, + developerName: existing.developerName ?? game.developerName, + maxLocalPlayers: existing.maxLocalPlayers ?? game.maxLocalPlayers, + maxOnlinePlayers: existing.maxOnlinePlayers ?? game.maxOnlinePlayers, + imageUrl: existing.imageUrl ?? game.imageUrl, + heroImageUrl: existing.heroImageUrl ?? game.heroImageUrl, + screenshotUrl: existing.screenshotUrl ?? game.screenshotUrl, + screenshotUrls: [...new Set([...(existing.screenshotUrls ?? []), ...(game.screenshotUrls ?? [])])], + imageUrlsByType: { + ...(game.imageUrlsByType ?? {}), + ...(existing.imageUrlsByType ?? {}), + }, + playType: existing.playType ?? game.playType, + membershipTierLabel: existing.membershipTierLabel ?? game.membershipTierLabel, + catalogSkuStrings: existing.catalogSkuStrings ?? game.catalogSkuStrings, + publisherName: existing.publisherName ?? game.publisherName, + playabilityState: existing.playabilityState ?? game.playabilityState, + lastPlayed: existing.lastPlayed ?? game.lastPlayed, + variants: mergedVariantList, + // Prefer variant libraryStatus when present so NOT_OWNED can clear stale flags. + isInLibrary: hasResolvedLibraryStatus + ? mergedVariantList.some((variant) => variant.inLibrary) + : Boolean( + existing.isInLibrary || + game.isInLibrary || + mergedVariantList.some((variant) => variant.inLibrary), + ), + genres: [...new Set([...(existing.genres ?? []), ...(game.genres ?? [])])], + featureLabels: [...new Set([...(existing.featureLabels ?? []), ...(game.featureLabels ?? [])])], + supportedControls: [...new Set([...(existing.supportedControls ?? []), ...(game.supportedControls ?? [])])], + nvidiaTech: [...new Set([...(existing.nvidiaTech ?? []), ...(game.nvidiaTech ?? [])])], + contentRatings: [...new Set([...(existing.contentRatings ?? []), ...(game.contentRatings ?? [])])], + availableStores: [...new Set([...(existing.availableStores ?? []), ...(game.availableStores ?? [])])], + searchText: [existing.searchText, game.searchText].filter(Boolean).join(" ").trim() || undefined, + selectedVariantIndex: Math.max(0, existing.variants[existing.selectedVariantIndex] + ? mergedVariantList.findIndex((variant) => variant.id === existing.variants[existing.selectedVariantIndex]?.id) + : game.selectedVariantIndex), + }; + + byId.set(game.id, merged); + } + + return [...byId.values()]; +} + +export async function fetchAppMetaData( + token: string, + appIds: string[], + vpcId: string, + proxyUrl?: string, +): Promise { + const normalizedIds = [...new Set(appIds.map((id) => id.trim()).filter((id) => id.length > 0))]; + if (normalizedIds.length === 0) { + return { data: { apps: { items: [] } } }; + } + + return await fetchLcarsGraphQl( + "AppDataForAppId", + { + vpcId, + locale: DEFAULT_LOCALE, + appIds: normalizedIds, + }, + token, + proxyUrl, + { context: "App metadata failed" }, + ); +} + +export async function enrichGamesWithMetadata(token: string, vpcId: string, games: GameInfo[], proxyUrl?: string): Promise { + const uuids = [...new Set(games.map((game) => game.uuid).filter((uuid): uuid is string => !!uuid))]; + + if (uuids.length === 0) { + return games; + } + + const chunkSize = 40; + const appById = new Map(); + + for (let index = 0; index < uuids.length; index += chunkSize) { + const chunk = uuids.slice(index, index + chunkSize); + const payload = await fetchAppMetaData(token, chunk, vpcId, proxyUrl); + if (payload.errors?.length) { + throw new Error(payload.errors.map((error) => error.message).join(", ")); + } + + for (const app of payload.data?.apps.items ?? []) { + appById.set(app.id, app); + } + } + + return dedupeGames( + games.map((game) => { + const metadata = game.uuid ? appById.get(game.uuid) : undefined; + return metadata ? mergeAppMetaIntoGame(game, metadata) : game; + }), + ); +} + +export async function resolveLaunchAppId( + token: string, + appIdOrUuid: string, + providerStreamingBaseUrl?: string, + proxyUrl?: string, +): Promise { + if (isNumericId(appIdOrUuid)) { + return appIdOrUuid; + } + + const vpcId = await getVpcId(token, providerStreamingBaseUrl, proxyUrl); + const payload = await fetchAppMetaData(token, [appIdOrUuid], vpcId, proxyUrl); + + if (payload.errors?.length) { + throw new Error(payload.errors.map((error) => error.message).join(", ")); + } + + const app = payload.data?.apps.items?.[0]; + if (!app) { + return null; + } + + return resolveAppData(app).numericAppId ?? null; +} + +export async function resolveStoreUrl( + token: string, + appIdOrUuid: string, + providerStreamingBaseUrl?: string, + options: { variantId?: string; store?: string; proxyUrl?: string } = {}, +): Promise { + const vpcId = await getVpcId(token, providerStreamingBaseUrl, options.proxyUrl); + const payload = await fetchAppMetaData(token, [appIdOrUuid], vpcId, options.proxyUrl); + + if (payload.errors?.length) { + throw new Error(payload.errors.map((error) => error.message).join(", ")); + } + + const app = payload.data?.apps.items?.[0]; + const variants = app?.variants ?? []; + const selectedVariant = options.variantId + ? variants.find((variant) => variant.id === options.variantId) + : undefined; + if (selectedVariant?.storeUrl) return selectedVariant.storeUrl; + + const storeKey = options.store ? normalizeGameStore(options.store) : undefined; + const matchingStoreVariant = storeKey + ? variants.find((variant) => normalizeGameStore(variant.appStore) === storeKey && variant.storeUrl) + : undefined; + if (matchingStoreVariant?.storeUrl) return matchingStoreVariant.storeUrl; + + return variants.find((variant) => variant.storeUrl)?.storeUrl ?? null; +} diff --git a/opennow-stable/src/main/gfn/gameFeatures.ts b/opennow-stable/src/main/platforms/gfn/gameFeatures.ts similarity index 100% rename from opennow-stable/src/main/gfn/gameFeatures.ts rename to opennow-stable/src/main/platforms/gfn/gameFeatures.ts diff --git a/opennow-stable/src/main/gfn/games.test.ts b/opennow-stable/src/main/platforms/gfn/games.test.ts similarity index 100% rename from opennow-stable/src/main/gfn/games.test.ts rename to opennow-stable/src/main/platforms/gfn/games.test.ts diff --git a/opennow-stable/src/main/platforms/gfn/games.ts b/opennow-stable/src/main/platforms/gfn/games.ts new file mode 100644 index 000000000..46bcb3083 --- /dev/null +++ b/opennow-stable/src/main/platforms/gfn/games.ts @@ -0,0 +1,44 @@ +/** + * GFN games facade — public API for IPC/index/tests. + * + * Implementation lives in focused modules: + * - gamesCache.ts — account/proxy cache keys + invalidation + public games cache + * - gameAppMapper.ts — AppData mapping, metadata enrichment, launch/store resolution + * - catalogBrowse.ts — catalog browse + main/featured/store panels + * - libraryGames.ts — library pagination/fetch/cache + mark owned + */ + +export { + getAccountCatalogGamesCachePrefix, + getAccountGamesCacheKeys, + getLegacyTokenScopedAccountGamesCacheKeys, + invalidateAccountGameCaches, + fetchPublicGames, + type AccountGameCacheInvalidationInput, +} from "./gamesCache"; + +export { + resolveLaunchAppId, + resolveStoreUrl, +} from "./gameAppMapper"; + +export { + browseCatalog, + browseCatalogUncached, + peekCachedBrowseCatalog, + fetchMainGames, + fetchMainGamesUncached, + fetchFeaturedGames, + fetchStorePanels, +} from "./catalogBrowse"; + +export { + peekCachedLibraryGames, + fetchLibraryGamesFromCache, + fetchLibraryGames, + fetchLibraryGamesUncached, + markGameOwned, + type MarkGameOwnedInput, +} from "./libraryGames"; + +export { fetchPublicGamesUncached } from "./publicGames"; diff --git a/opennow-stable/src/main/platforms/gfn/gamesCache.ts b/opennow-stable/src/main/platforms/gfn/gamesCache.ts new file mode 100644 index 000000000..41bc4e231 --- /dev/null +++ b/opennow-stable/src/main/platforms/gfn/gamesCache.ts @@ -0,0 +1,200 @@ +import { createHash } from "node:crypto"; +import { cacheManager } from "../../services/cacheManager"; +import { sessionProxyCacheKeyPart, sessionProxyHasCredentials } from "./proxyUrl"; +import { fetchPublicGamesUncached } from "./publicGames"; +import type { GameInfo } from "@shared/gfn"; + +export const LIBRARY_GAMES_CACHE_SCOPE = "library:v2"; +export const CATALOG_GAMES_CACHE_SCOPE = "catalog"; +const PUBLIC_GAMES_CACHE_KEY = "games:public:v2"; + +function addProxyCacheScope(hash: ReturnType, proxyUrl?: string): void { + const proxyCachePart = sessionProxyCacheKeyPart(proxyUrl); + if (proxyCachePart) { + hash.update("\0").update(proxyCachePart); + } +} + +export function publicGamesCacheKey(proxyUrl?: string): string { + const proxyCachePart = sessionProxyCacheKeyPart(proxyUrl); + return proxyCachePart ? `${PUBLIC_GAMES_CACHE_KEY}:${proxyCachePart}` : PUBLIC_GAMES_CACHE_KEY; +} + +export function shouldBypassGamesCache(proxyUrl?: string): boolean { + return sessionProxyHasCredentials(proxyUrl); +} + +export function accountScopedGamesCacheKey(scope: string, accountId: string, providerStreamingBaseUrl?: string, proxyUrl?: string): string { + const hash = createHash("sha256") + .update(accountId) + .update("\0") + .update(providerStreamingBaseUrl ?? ""); + addProxyCacheScope(hash, proxyUrl); + const digest = hash.digest("hex").slice(0, 16); + return `games:${scope}:${digest}`; +} + +export function legacyTokenScopedGamesCacheKey(scope: string, token: string, providerStreamingBaseUrl?: string, proxyUrl?: string): string { + const hash = createHash("sha256") + .update(token) + .update("\0") + .update(providerStreamingBaseUrl ?? ""); + addProxyCacheScope(hash, proxyUrl); + const digest = hash.digest("hex").slice(0, 16); + return `games:${scope}:${digest}`; +} + +export function resolveAccountCacheId(accountId: string | undefined, token: string): string { + return accountId?.trim() || token; +} + +export async function loadAccountScopedFromCache( + scope: string, + accountId: string | undefined, + token: string, + providerStreamingBaseUrl?: string, + proxyUrl?: string, +): Promise>>> { + if (shouldBypassGamesCache(proxyUrl)) { + return null; + } + + const resolvedAccountId = resolveAccountCacheId(accountId, token); + const primaryKey = accountScopedGamesCacheKey(scope, resolvedAccountId, providerStreamingBaseUrl, proxyUrl); + const cached = await cacheManager.loadFromCache(primaryKey); + if (cached) { + return cached; + } + + if (resolvedAccountId !== token) { + const legacyKey = legacyTokenScopedGamesCacheKey(scope, token, providerStreamingBaseUrl, proxyUrl); + if (legacyKey !== primaryKey) { + const legacy = await cacheManager.loadFromCache(legacyKey); + if (legacy) { + void cacheManager.saveToCache(primaryKey, legacy.data); + void cacheManager.invalidateCache(legacyKey); + return legacy; + } + } + } + + return null; +} + +export function catalogBrowseCacheKey(input: { + searchQuery?: string; + sortId?: string; + filterIds?: string[]; + fetchCount?: number; + providerStreamingBaseUrl?: string; + proxyUrl?: string; +}, accountId: string): string { + const queryDigest = createHash("sha256") + .update(input.searchQuery?.trim() ?? "") + .update("\0") + .update(input.sortId ?? "") + .update("\0") + .update((input.filterIds ?? []).join(",")) + .update("\0") + .update(String(input.fetchCount ?? "")) + .digest("hex") + .slice(0, 12); + return `${getAccountCatalogGamesCachePrefix(accountId, input.providerStreamingBaseUrl, input.proxyUrl)}:${queryDigest}`; +} + +export function getAccountCatalogGamesCachePrefix( + accountId: string, + providerStreamingBaseUrl?: string, + proxyUrl?: string, +): string { + return accountScopedGamesCacheKey(CATALOG_GAMES_CACHE_SCOPE, accountId, providerStreamingBaseUrl, proxyUrl); +} + +export function getAccountGamesCacheKeys(accountId: string, providerStreamingBaseUrl?: string, proxyUrl?: string): { + main: string; + featured: string; + storePanels: string; + library: string; + catalogPrefix: string; + public: string; +} { + return { + main: accountScopedGamesCacheKey("main", accountId, providerStreamingBaseUrl, proxyUrl), + featured: accountScopedGamesCacheKey("featured", accountId, providerStreamingBaseUrl, proxyUrl), + storePanels: accountScopedGamesCacheKey("store-panels", accountId, providerStreamingBaseUrl, proxyUrl), + library: accountScopedGamesCacheKey(LIBRARY_GAMES_CACHE_SCOPE, accountId, providerStreamingBaseUrl, proxyUrl), + catalogPrefix: getAccountCatalogGamesCachePrefix(accountId, providerStreamingBaseUrl, proxyUrl), + public: publicGamesCacheKey(proxyUrl), + }; +} + +export function getLegacyTokenScopedAccountGamesCacheKeys(token: string, providerStreamingBaseUrl?: string, proxyUrl?: string): { + main: string; + featured: string; + storePanels: string; + library: string; + catalogPrefix: string; +} { + return { + main: legacyTokenScopedGamesCacheKey("main", token, providerStreamingBaseUrl, proxyUrl), + featured: legacyTokenScopedGamesCacheKey("featured", token, providerStreamingBaseUrl, proxyUrl), + storePanels: legacyTokenScopedGamesCacheKey("store-panels", token, providerStreamingBaseUrl, proxyUrl), + library: legacyTokenScopedGamesCacheKey(LIBRARY_GAMES_CACHE_SCOPE, token, providerStreamingBaseUrl, proxyUrl), + catalogPrefix: legacyTokenScopedGamesCacheKey(CATALOG_GAMES_CACHE_SCOPE, token, providerStreamingBaseUrl, proxyUrl), + }; +} + +export interface AccountGameCacheInvalidationInput { + userId: string; + providerStreamingBaseUrl?: string; + tokens?: Array; + proxyUrl?: string; + logPrefix?: string; +} + +export async function invalidateAccountGameCaches(input: AccountGameCacheInvalidationInput): Promise { + const cacheKeySets: Array<{ main: string; featured: string; storePanels: string; library: string; catalogPrefix: string }> = [ + getAccountGamesCacheKeys(input.userId, input.providerStreamingBaseUrl), + ]; + const legacyTokens = [...new Set((input.tokens ?? []).filter((token): token is string => Boolean(token)))]; + cacheKeySets.push( + ...legacyTokens.map((token) => getLegacyTokenScopedAccountGamesCacheKeys(token, input.providerStreamingBaseUrl)), + ); + + if (input.proxyUrl?.trim()) { + try { + cacheKeySets.push(getAccountGamesCacheKeys(input.userId, input.providerStreamingBaseUrl, input.proxyUrl)); + cacheKeySets.push( + ...legacyTokens.map((token) => getLegacyTokenScopedAccountGamesCacheKeys(token, input.providerStreamingBaseUrl, input.proxyUrl)), + ); + } catch (error) { + console.warn(`${input.logPrefix ?? "[Games]"} Skipping proxy-scoped game cache invalidation:`, error); + } + } + + const invalidations = new Map>(); + for (const keys of cacheKeySets) { + invalidations.set(keys.main, cacheManager.invalidateCache(keys.main)); + invalidations.set(keys.featured, cacheManager.invalidateCache(keys.featured)); + invalidations.set(keys.storePanels, cacheManager.invalidateCache(keys.storePanels)); + invalidations.set(keys.library, cacheManager.invalidateCache(keys.library)); + invalidations.set(keys.catalogPrefix, cacheManager.invalidateCachesByPrefix(keys.catalogPrefix)); + } + await Promise.allSettled(invalidations.values()); +} + +export async function fetchPublicGames(proxyUrl?: string): Promise { + if (shouldBypassGamesCache(proxyUrl)) { + return fetchPublicGamesUncached(proxyUrl); + } + + const cacheKey = publicGamesCacheKey(proxyUrl); + const cached = await cacheManager.loadFromCache(cacheKey); + if (cached) { + return cached.data; + } + + const games = await fetchPublicGamesUncached(proxyUrl); + await cacheManager.saveToCache(cacheKey, games); + return games; +} diff --git a/opennow-stable/src/main/platforms/gfn/gfnErrorCodeEnum.ts b/opennow-stable/src/main/platforms/gfn/gfnErrorCodeEnum.ts new file mode 100644 index 000000000..64f5a6178 --- /dev/null +++ b/opennow-stable/src/main/platforms/gfn/gfnErrorCodeEnum.ts @@ -0,0 +1,134 @@ +/** Session error code constants. */ +export enum GfnErrorCode { + // Success codes + Success = 15859712, + + // Client-side errors (3237085xxx - 3237093xxx) + InvalidOperation = 3237085186, + NetworkError = 3237089282, + GetActiveSessionServerError = 3237089283, + AuthTokenNotUpdated = 3237093377, + SessionFinishedState = 3237093378, + ResponseParseFailure = 3237093379, + InvalidServerResponse = 3237093381, + PutOrPostInProgress = 3237093382, + GridServerNotInitialized = 3237093383, + DOMExceptionInSessionControl = 3237093384, + InvalidAdStateTransition = 3237093386, + AuthTokenUpdateTimeout = 3237093387, + + // Server error codes (base 3237093632 + statusCode) + SessionServerErrorBegin = 3237093632, + RequestForbidden = 3237093634, // statusCode 2 + ServerInternalTimeout = 3237093635, // statusCode 3 + ServerInternalError = 3237093636, // statusCode 4 + ServerInvalidRequest = 3237093637, // statusCode 5 + ServerInvalidRequestVersion = 3237093638, // statusCode 6 + SessionListLimitExceeded = 3237093639, // statusCode 7 + InvalidRequestDataMalformed = 3237093640, // statusCode 8 + InvalidRequestDataMissing = 3237093641, // statusCode 9 + RequestLimitExceeded = 3237093642, // statusCode 10 + SessionLimitExceeded = 3237093643, // statusCode 11 + InvalidRequestVersionOutOfDate = 3237093644, // statusCode 12 + SessionEntitledTimeExceeded = 3237093645, // statusCode 13 + AuthFailure = 3237093646, // statusCode 14 + InvalidAuthenticationMalformed = 3237093647, // statusCode 15 + InvalidAuthenticationExpired = 3237093648, // statusCode 16 + InvalidAuthenticationNotFound = 3237093649, // statusCode 17 + EntitlementFailure = 3237093650, // statusCode 18 + InvalidAppIdNotAvailable = 3237093651, // statusCode 19 + InvalidAppIdNotFound = 3237093652, // statusCode 20 + InvalidSessionIdMalformed = 3237093653, // statusCode 21 + InvalidSessionIdNotFound = 3237093654, // statusCode 22 + EulaUnAccepted = 3237093655, // statusCode 23 + MaintenanceStatus = 3237093656, // statusCode 24 + ServiceUnAvailable = 3237093657, // statusCode 25 + SteamGuardRequired = 3237093658, // statusCode 26 + SteamLoginRequired = 3237093659, // statusCode 27 + SteamGuardInvalid = 3237093660, // statusCode 28 + SteamProfilePrivate = 3237093661, // statusCode 29 + InvalidCountryCode = 3237093662, // statusCode 30 + InvalidLanguageCode = 3237093663, // statusCode 31 + MissingCountryCode = 3237093664, // statusCode 32 + MissingLanguageCode = 3237093665, // statusCode 33 + SessionNotPaused = 3237093666, // statusCode 34 + EmailNotVerified = 3237093667, // statusCode 35 + InvalidAuthenticationUnsupportedProtocol = 3237093668, // statusCode 36 + InvalidAuthenticationUnknownToken = 3237093669, // statusCode 37 + InvalidAuthenticationCredentials = 3237093670, // statusCode 38 + SessionNotPlaying = 3237093671, // statusCode 39 + InvalidServiceResponse = 3237093672, // statusCode 40 + AppPatching = 3237093673, // statusCode 41 + GameNotFound = 3237093674, // statusCode 42 + NotEnoughCredits = 3237093675, // statusCode 43 + InvitationOnlyRegistration = 3237093676, // statusCode 44 + RegionNotSupportedForRegistration = 3237093677, // statusCode 45 + SessionTerminatedByAnotherClient = 3237093678, // statusCode 46 + DeviceIdAlreadyUsed = 3237093679, // statusCode 47 + ServiceNotExist = 3237093680, // statusCode 48 + SessionExpired = 3237093681, // statusCode 49 + SessionLimitPerDeviceReached = 3237093682, // statusCode 50 + ForwardingZoneOutOfCapacity = 3237093683, // statusCode 51 + RegionNotSupportedIndefinitely = 3237093684, // statusCode 52 + RegionBanned = 3237093685, // statusCode 53 + RegionOnHoldForFree = 3237093686, // statusCode 54 + RegionOnHoldForPaid = 3237093687, // statusCode 55 + AppMaintenanceStatus = 3237093688, // statusCode 56 + ResourcePoolNotConfigured = 3237093689, // statusCode 57 + InsufficientVmCapacity = 3237093690, // statusCode 58 + InsufficientRouteCapacity = 3237093691, // statusCode 59 + InsufficientScratchSpaceCapacity = 3237093692, // statusCode 60 + RequiredSeatInstanceTypeNotSupported = 3237093693, // statusCode 61 + ServerSessionQueueLengthExceeded = 3237093694, // statusCode 62 + RegionNotSupportedForStreaming = 3237093695, // statusCode 63 + SessionForwardRequestAllocationTimeExpired = 3237093696, // statusCode 64 + SessionForwardGameBinariesNotAvailable = 3237093697, // statusCode 65 + GameBinariesNotAvailableInRegion = 3237093698, // statusCode 66 + UekRetrievalFailed = 3237093699, // statusCode 67 + EntitlementFailureForResource = 3237093700, // statusCode 68 + SessionInQueueAbandoned = 3237093701, // statusCode 69 + MemberTerminated = 3237093702, // statusCode 70 + SessionRemovedFromQueueMaintenance = 3237093703, // statusCode 71 + ZoneMaintenanceStatus = 3237093704, // statusCode 72 + GuestModeCampaignDisabled = 3237093705, // statusCode 73 + RegionNotSupportedAnonymousAccess = 3237093706, // statusCode 74 + InstanceTypeNotSupportedInSingleRegion = 3237093707, // statusCode 75 + InvalidZoneForQueuedSession = 3237093710, // statusCode 78 + SessionWaitingAdsTimeExpired = 3237093711, // statusCode 79 + UserCancelledWatchingAds = 3237093712, // statusCode 80 + StreamingNotAllowedInLimitedMode = 3237093713, // statusCode 81 + ForwardRequestJPMFailed = 3237093714, // statusCode 82 + MaxSessionNumberLimitExceeded = 3237093715, // statusCode 83 + GuestModePartnerCapacityDisabled = 3237093716, // statusCode 84 + SessionRejectedNoCapacity = 3237093717, // statusCode 85 + SessionInsufficientPlayabilityLevel = 3237093718, // statusCode 86 + ForwardRequestLOFNFailed = 3237093719, // statusCode 87 + InvalidTransportRequest = 3237093720, // statusCode 88 + UserStorageNotAvailable = 3237093721, // statusCode 89 + GfnStorageNotAvailable = 3237093722, // statusCode 90 + AppNotAllowedToStream = 3237093723, // statusCode 91 + SessionServerErrorEnd = 3237093887, + + // Session setup cancelled + SessionSetupCancelled = 15867905, + SessionSetupCancelledDuringQueuing = 15867906, + RequestCancelled = 15867907, + SystemSleepDuringSessionSetup = 15867909, + NoInternetDuringSessionSetup = 15868417, + + // Network errors (3237101xxx) + SocketError = 3237101580, + AddressResolveFailed = 3237101581, + ConnectFailed = 3237101582, + SslError = 3237101583, + ConnectionTimeout = 3237101584, + DataReceiveTimeout = 3237101585, + PeerNoResponse = 3237101586, + UnexpectedHttpRedirect = 3237101587, + DataSendFailure = 3237101588, + DataReceiveFailure = 3237101589, + CertificateRejected = 3237101590, + DataNotAllowed = 3237101591, + NetworkErrorUnknown = 3237101592, +} + diff --git a/opennow-stable/src/main/platforms/gfn/gfnErrorMessages.ts b/opennow-stable/src/main/platforms/gfn/gfnErrorMessages.ts new file mode 100644 index 000000000..a85518a2b --- /dev/null +++ b/opennow-stable/src/main/platforms/gfn/gfnErrorMessages.ts @@ -0,0 +1,504 @@ +/** Error message entry with title and description */ +export interface ErrorMessageEntry { + title: string; + description: string; +} + +/** User-friendly error messages map */ +export const ERROR_MESSAGES: Map = new Map([ + // Success + [15859712, { title: "Success", description: "Session started successfully." }], + + // Client errors + [ + 3237085186, + { + title: "Invalid Operation", + description: "The requested operation is not valid at this time.", + }, + ], + [ + 3237089282, + { + title: "Network Error", + description: "A network error occurred. Please check your internet connection.", + }, + ], + [ + 3237093377, + { + title: "Authentication Required", + description: "Your session has expired. Please log in again.", + }, + ], + [ + 3237093379, + { + title: "Server Response Error", + description: "Failed to parse server response. Please try again.", + }, + ], + [ + 3237093381, + { + title: "Invalid Server Response", + description: "The server returned an invalid response.", + }, + ], + [ + 3237093384, + { + title: "Session Error", + description: "An error occurred during session setup.", + }, + ], + [ + 3237093387, + { + title: "Authentication Timeout", + description: "Authentication token update timed out. Please log in again.", + }, + ], + + // Server errors + [ + 3237093634, + { + title: "Access Forbidden", + description: "Access to this service is forbidden.", + }, + ], + [ + 3237093635, + { + title: "Server Timeout", + description: "The server timed out. Please try again.", + }, + ], + [ + 3237093636, + { + title: "Server Error", + description: "An internal server error occurred. Please try again later.", + }, + ], + [ + 3237093637, + { + title: "Invalid Request", + description: "The request was invalid.", + }, + ], + [ + 3237093639, + { + title: "Too Many Sessions", + description: "You have too many active sessions. Please close some sessions and try again.", + }, + ], + [ + 3237093643, + { + title: "Session Limit Exceeded", + description: "You have reached your session limit. Another session may already be running on your account.", + }, + ], + [ + 3237093645, + { + title: "Session Time Exceeded", + description: "Your session time has been exceeded.", + }, + ], + [ + 3237093646, + { + title: "Authentication Failed", + description: "Authentication failed. Please log in again.", + }, + ], + [ + 3237093648, + { + title: "Session Expired", + description: "Your authentication has expired. Please log in again.", + }, + ], + [ + 3237093650, + { + title: "Entitlement Error", + description: "You don't have access to this game or service.", + }, + ], + [ + 3237093651, + { + title: "Game Not Available", + description: "This game is not currently available.", + }, + ], + [ + 3237093652, + { + title: "Game Not Found", + description: "This game was not found in the library.", + }, + ], + [ + 3237093655, + { + title: "EULA Required", + description: "You must accept the End User License Agreement to continue.", + }, + ], + [ + 3237093656, + { + title: "Under Maintenance", + description: "The service is currently under maintenance. Please try again later.", + }, + ], + [ + 3237093657, + { + title: "Service Unavailable", + description: "The service is temporarily unavailable. Please try again later.", + }, + ], + [ + 3237093658, + { + title: "Steam Guard Required", + description: "Steam Guard authentication is required. Please complete Steam Guard verification.", + }, + ], + [ + 3237093659, + { + title: "Steam Login Required", + description: "You need to link your Steam account to play this game.", + }, + ], + [ + 3237093660, + { + title: "Steam Guard Invalid", + description: "Steam Guard code is invalid. Please try again.", + }, + ], + [ + 3237093661, + { + title: "Steam Profile Private", + description: "Your Steam profile is private. Please make it public or friends-only.", + }, + ], + [ + 3237093667, + { + title: "Email Not Verified", + description: "Please verify your email address to continue.", + }, + ], + [ + 3237093673, + { + title: "Game Updating", + description: "This game is currently being updated. Please try again later.", + }, + ], + [ + 3237093674, + { + title: "Game Not Found", + description: "This game was not found.", + }, + ], + [ + 3237093675, + { + title: "Insufficient Credits", + description: "You don't have enough credits for this session.", + }, + ], + [ + 3237093678, + { + title: "Session Taken Over", + description: "Your session was taken over by another device.", + }, + ], + [ + 3237093681, + { + title: "Session Expired", + description: "Your session has expired.", + }, + ], + [ + 3237093682, + { + title: "Device Limit Reached", + description: "You have reached the session limit for this device.", + }, + ], + [ + 3237093683, + { + title: "Region At Capacity", + description: "Your region is currently at capacity. Please try again later.", + }, + ], + [ + 3237093684, + { + title: "Region Not Supported", + description: "The service is not available in your region.", + }, + ], + [ + 3237093685, + { + title: "Region Banned", + description: "The service is not available in your region.", + }, + ], + [ + 3237093686, + { + title: "Free Tier On Hold", + description: "Free tier is temporarily unavailable in your region.", + }, + ], + [ + 3237093687, + { + title: "Paid Tier On Hold", + description: "Paid tier is temporarily unavailable in your region.", + }, + ], + [ + 3237093688, + { + title: "Game Maintenance", + description: "This game is currently under maintenance.", + }, + ], + [ + 3237093690, + { + title: "No Capacity", + description: "No gaming rigs are available right now. Please try again later or join the queue.", + }, + ], + [ + 3237093694, + { + title: "Queue Full", + description: "The queue is currently full. Please try again later.", + }, + ], + [ + 3237093695, + { + title: "GeForce NOW Unavailable in Your Region", + description: + "GeForce NOW has restricted streaming in your region. This is not an OpenNOW issue — NVIDIA has blocked access from your location. You may need to use a VPN or check GeForce NOW's supported countries list.", + }, + ], + [ + 3237093698, + { + title: "Game Not Available", + description: "This game is not available in your region.", + }, + ], + [ + 3237093701, + { + title: "Queue Abandoned", + description: "Your session in queue was abandoned.", + }, + ], + [ + 3237093702, + { + title: "Account Terminated", + description: "Your account has been terminated.", + }, + ], + [ + 3237093703, + { + title: "Queue Maintenance", + description: "The queue was cleared due to maintenance.", + }, + ], + [ + 3237093704, + { + title: "Zone Maintenance", + description: "This server zone is under maintenance.", + }, + ], + [ + 3237093711, + { + title: "Ads Timeout", + description: "Session expired while waiting for ads. Free tier users must watch ads to play. Please start a new session.", + }, + ], + [ + 3237093712, + { + title: "Ads Cancelled", + description: "Session cancelled because ads were skipped. Free tier users must watch ads to play.", + }, + ], + [ + 3237093713, + { + title: "Limited Mode", + description: "Streaming is not allowed in limited mode.", + }, + ], + [ + 3237093715, + { + title: "Session Limit", + description: "Maximum number of sessions reached.", + }, + ], + [ + 3237093717, + { + title: "No Capacity", + description: "No gaming rigs are available. Please try again later.", + }, + ], + [ + 3237093718, + { + title: "Membership Upgrade Required", + description: "Your current GeForce NOW membership is not high enough to play this game. Upgrade to a higher tier and try again.", + }, + ], + [ + 3237093721, + { + title: "Storage Unavailable", + description: "User storage is not available.", + }, + ], + [ + 3237093722, + { + title: "Storage Error", + description: "Service storage is not available.", + }, + ], + [ + 3237093723, + { + title: "Streaming Not Allowed", + description: "This app is not allowed to stream on your current GeForce NOW account or region.", + }, + ], + + // Cancellation + [ + 15867905, + { + title: "Session Cancelled", + description: "Session setup was cancelled.", + }, + ], + [ + 15867906, + { + title: "Queue Cancelled", + description: "You left the queue.", + }, + ], + [ + 15867907, + { + title: "Request Cancelled", + description: "The request was cancelled.", + }, + ], + [ + 15867909, + { + title: "System Sleep", + description: "Session setup was interrupted by system sleep.", + }, + ], + [ + 15868417, + { + title: "No Internet", + description: "No internet connection during session setup.", + }, + ], + + // Network errors + [ + 3237101580, + { + title: "Socket Error", + description: "A socket error occurred. Please check your network.", + }, + ], + [ + 3237101581, + { + title: "DNS Error", + description: "Failed to resolve server address. Please check your network.", + }, + ], + [ + 3237101582, + { + title: "Connection Failed", + description: "Failed to connect to the server. Please check your network.", + }, + ], + [ + 3237101583, + { + title: "SSL Error", + description: "A secure connection error occurred.", + }, + ], + [ + 3237101584, + { + title: "Connection Timeout", + description: "Connection timed out. Please check your network.", + }, + ], + [ + 3237101585, + { + title: "Receive Timeout", + description: "Data receive timed out. Please check your network.", + }, + ], + [ + 3237101586, + { + title: "No Response", + description: "Server not responding. Please try again.", + }, + ], + [ + 3237101590, + { + title: "Certificate Error", + description: "Server certificate was rejected.", + }, + ], +]); + diff --git a/opennow-stable/src/main/platforms/gfn/index.ts b/opennow-stable/src/main/platforms/gfn/index.ts new file mode 100644 index 000000000..aa0fdafa0 --- /dev/null +++ b/opennow-stable/src/main/platforms/gfn/index.ts @@ -0,0 +1,61 @@ +/** + * GeForce NOW main-process platform surface. + * + * Prefer importing focused modules (`./auth`, `./cloudmatch`, …) from feature + * code. This barrel is for platform-registry and cross-cutting consumers. + */ + +export { AuthService } from "./auth"; +export { + claimSession, + createSession, + getActiveSessions, + pollSession, + reportSessionAd, + stopSession, +} from "./cloudmatch"; +export { SessionError, isSessionError } from "./errorCodes"; +export { + browseCatalog, + fetchFeaturedGames, + fetchLibraryGames, + fetchMainGames, + fetchStorePanels, + getAccountGamesCacheKeys, + markGameOwned, + resolveLaunchAppId, + resolveStoreUrl, +} from "./games"; +export { initSessionProxyAuth } from "./proxyFetch"; +export { normalizeSessionProxyUrl, sessionProxyHasCredentials } from "./proxyUrl"; +export { getStableDeviceId } from "./deviceId"; +export { + STEAM_DECK_DEVICE_IDENTITY, + configureIdentifyAsSteamDeck, + resolveGfnDeviceIdentity, +} from "./deviceIdentity"; +export { fetchSubscription, fetchDynamicRegions } from "./subscription"; +export { + fetchPersistentStorageLocations, + resetPersistentStorage, +} from "./persistentStorage"; +export { + fetchGameAccountConnections, + linkGameAccount, + resyncGameAccount, + unlinkGameAccount, +} from "./accountConnections"; +export { GfnSignalingClient } from "./signaling"; +export { + GFN_CLIENT_VERSION, + GFN_PLAY_ORIGIN, + GFN_PLAY_REFERER, + GFN_USER_AGENT, + LCARS_CLIENT_ID, + buildGfnCloudMatchClaimHeaders, + buildGfnCloudMatchHeaders, + buildGfnGraphQlHeaders, + buildGfnLcarsHeaders, + buildNvidiaAuthHeaders, + gfnJwtAuthorization, +} from "./clientHeaders"; diff --git a/opennow-stable/src/main/gfn/lcarsGraphql.test.ts b/opennow-stable/src/main/platforms/gfn/lcarsGraphql.test.ts similarity index 100% rename from opennow-stable/src/main/gfn/lcarsGraphql.test.ts rename to opennow-stable/src/main/platforms/gfn/lcarsGraphql.test.ts diff --git a/opennow-stable/src/main/gfn/lcarsGraphql.ts b/opennow-stable/src/main/platforms/gfn/lcarsGraphql.ts similarity index 100% rename from opennow-stable/src/main/gfn/lcarsGraphql.ts rename to opennow-stable/src/main/platforms/gfn/lcarsGraphql.ts diff --git a/opennow-stable/src/main/platforms/gfn/libraryGames.ts b/opennow-stable/src/main/platforms/gfn/libraryGames.ts new file mode 100644 index 000000000..68821579f --- /dev/null +++ b/opennow-stable/src/main/platforms/gfn/libraryGames.ts @@ -0,0 +1,239 @@ +import type { + GameInfo, + MarkGameOwnedResult, +} from "@shared/gfn"; +import { cacheManager } from "../../services/cacheManager"; +import { mergePublicGameVariants } from "./publicGames"; +import { fetchAllAppsPages } from "./paginatedApps"; +import { postLcarsMutation } from "./lcarsGraphql"; +import { + accountScopedGamesCacheKey, + fetchPublicGames, + invalidateAccountGameCaches, + LIBRARY_GAMES_CACHE_SCOPE, + loadAccountScopedFromCache, + resolveAccountCacheId, + shouldBypassGamesCache, +} from "./gamesCache"; +import { + appToGame, + type AppData, + type AppsPage, + DEFAULT_LOCALE, + dedupeGames, + enrichGamesWithMetadata, + GFN_FEATURE_FIELDS, + type GraphQlResponse, + getVpcId, + postGraphQl, +} from "./gameAppMapper"; +import { fetchPanels, flattenPanels } from "./catalogBrowse"; + +const LIBRARY_FETCH_COUNT = 200; +const MAX_LIBRARY_PAGES = 25; +const DEFAULT_LIBRARY_SORT = "variants.gfn.library.lastPlayedDate:DESC,computedValues.libraryAddedDate:DESC,sortName:ASC"; +const LIBRARY_APPS_FILTER = { + variants: { + gfn: { + library: { + status: { + notEquals: "NOT_OWNED", + }, + }, + }, + }, +} satisfies Record; + +interface AddOwnedVariantResponse { + data?: { + addOwnedVariant?: { + app?: { + id?: string; + }; + }; + }; + errors?: Array<{ message: string }>; +} + +export interface MarkGameOwnedInput { + token: string; + userId: string; + variantId: string; + providerStreamingBaseUrl?: string; + proxyUrl?: string; + tokens?: Array; +} + +export async function peekCachedLibraryGames( + token: string, + providerStreamingBaseUrl?: string, + accountId?: string, + proxyUrl?: string, +): Promise { + const cached = await loadAccountScopedFromCache(LIBRARY_GAMES_CACHE_SCOPE, accountId, token, providerStreamingBaseUrl, proxyUrl); + return cached?.data ?? null; +} + +export async function fetchLibraryGamesFromCache( + token: string, + providerStreamingBaseUrl?: string, + accountId?: string, + proxyUrl?: string, +): Promise { + const cached = await peekCachedLibraryGames(token, providerStreamingBaseUrl, accountId, proxyUrl); + if (!cached) { + return null; + } + return mergePublicGameVariants(cached, await fetchPublicGames(proxyUrl)); +} + +export async function fetchLibraryGames( + token: string, + providerStreamingBaseUrl?: string, + accountId?: string, + proxyUrl?: string, +): Promise { + const cached = await loadAccountScopedFromCache(LIBRARY_GAMES_CACHE_SCOPE, accountId, token, providerStreamingBaseUrl, proxyUrl); + if (cached) { + return mergePublicGameVariants(cached.data, await fetchPublicGames(proxyUrl)); + } + + const games = await fetchLibraryGamesUncached(token, providerStreamingBaseUrl, proxyUrl); + if (!shouldBypassGamesCache(proxyUrl)) { + const cacheKey = accountScopedGamesCacheKey(LIBRARY_GAMES_CACHE_SCOPE, resolveAccountCacheId(accountId, token), providerStreamingBaseUrl, proxyUrl); + await cacheManager.saveToCache(cacheKey, games); + } + return games; +} + +export async function fetchLibraryGamesUncached( + token: string, + providerStreamingBaseUrl?: string, + proxyUrl?: string, +): Promise { + const vpcId = await getVpcId(token, providerStreamingBaseUrl, proxyUrl); + try { + const apps = await fetchPaginatedLibraryApps(token, vpcId, proxyUrl); + const games = dedupeGames(apps.map(appToGame)); + return mergePublicGameVariants(await enrichGamesWithMetadata(token, vpcId, games, proxyUrl), await fetchPublicGames(proxyUrl)); + } catch (error) { + console.warn("Paginated library query failed, falling back to library panel:", error); + } + + let payload: GraphQlResponse; + + try { + payload = await fetchPanels(token, ["LIBRARY"], vpcId, { withLibraryTime: true }, proxyUrl); + } catch { + payload = await fetchPanels(token, ["LIBRARY"], vpcId, undefined, proxyUrl); + } + + const games = flattenPanels(payload); + return mergePublicGameVariants(await enrichGamesWithMetadata(token, vpcId, games, proxyUrl), await fetchPublicGames(proxyUrl)); +} + +async function fetchPaginatedLibraryApps(token: string, vpcId: string, proxyUrl?: string): Promise { + const query = `query GetLibraryApps( + $vpcId: String!, + $locale: String!, + $sortString: String!, + $fetchCount: Int!, + $cursor: String!, + $filters: AppFilterFields! + ) { + apps( + vpcId: $vpcId, + language: $locale, + orderBy: $sortString, + first: $fetchCount, + after: $cursor, + filters: $filters + ) { + numberReturned + numberSupported + pageInfo { hasNextPage endCursor totalCount } + items { + id + title + images { KEY_ART KEY_IMAGE GAME_BOX_ART TV_BANNER HERO_IMAGE MARQUEE_HERO_IMAGE FEATURE_IMAGE GAME_LOGO SCREENSHOTS } + variants { + id + appStore + storeUrl + supportedControls + gfn { + status + features { +${GFN_FEATURE_FIELDS} + } + library { status selected lastPlayedDate } + } + } + gfn { + playabilityState + minimumMembershipTierLabel + catalogSkuStrings { + SKU_BASED_TAG + SKU_BASED_PLAYABILITY_TEXT + SKU_BASED_UNPLAYABLE_DIALOG_HEADER + SKU_BASED_UNPLAYABLE_DIALOG_BODY_UPGRADE + SKU_BASED_UNPLAYABLE_DIALOG_BODY_UPGRADE_ECOMM_RESTRICTED + } + } + itemMetadata { campaignIds } + } + } + }`; + + const result = await fetchAllAppsPages( + (cursor) => postGraphQl( + query, + { + vpcId, + locale: DEFAULT_LOCALE, + sortString: DEFAULT_LIBRARY_SORT, + fetchCount: LIBRARY_FETCH_COUNT, + cursor, + filters: LIBRARY_APPS_FILTER, + }, + token, + proxyUrl, + ), + { maxPages: MAX_LIBRARY_PAGES }, + ); + return result.items; +} + +export async function markGameOwned(input: MarkGameOwnedInput): Promise { + const variantId = input.variantId.trim(); + if (!variantId) { + throw new Error("Cannot mark game as owned without a variant ID"); + } + + const payload = await postLcarsMutation( + "AddOwnedVariant", + { + cmsId: variantId, + locale: DEFAULT_LOCALE, + }, + input.token, + input.proxyUrl, + ); + + if (!payload.data?.addOwnedVariant?.app?.id) { + throw new Error("GFN library mutation failed: missing AddOwnedVariant response"); + } + + await invalidateAccountGameCaches({ + userId: input.userId, + providerStreamingBaseUrl: input.providerStreamingBaseUrl, + tokens: [input.token, ...(input.tokens ?? [])], + proxyUrl: input.proxyUrl, + }); + + return { + ok: true, + variantId, + libraryStatus: "MANUAL", + }; +} diff --git a/opennow-stable/src/main/platforms/gfn/nvstRtsp/probe.test.ts b/opennow-stable/src/main/platforms/gfn/nvstRtsp/probe.test.ts new file mode 100644 index 000000000..ccd6e135a --- /dev/null +++ b/opennow-stable/src/main/platforms/gfn/nvstRtsp/probe.test.ts @@ -0,0 +1,66 @@ +/// + +import test from "node:test"; +import assert from "node:assert/strict"; + +import { + collectRtspsEndpoints, + rtspsUrlToWssUrl, + selectPrimaryRtspsEndpoint, +} from "./probe"; + +test("selectPrimaryRtspsEndpoint prefers :322", () => { + const selected = selectPrimaryRtspsEndpoint([ + "rtsps://host.example:48322", + "rtsps://host.example:322", + ]); + assert.equal(selected, "rtsps://host.example:322"); +}); + +test("rtspsUrlToWssUrl is host:port with no path (empty upgrade path is manual)", () => { + assert.equal( + rtspsUrlToWssUrl("rtsps://80-250-97-37.cloudmatchbeta.nvidiagrid.net:322"), + "wss://80-250-97-37.cloudmatchbeta.nvidiagrid.net:322", + ); +}); + +test("collectRtspsEndpoints keeps both usage=14 paths", () => { + const endpoints = collectRtspsEndpoints( + [ + { + usage: 14, + port: 322, + resourcePath: "rtsps://host.example:322", + }, + { + usage: 14, + port: 48322, + resourcePath: "rtsps://host.example:48322", + }, + { + usage: 2, + port: 49006, + resourcePath: null, + }, + ], + "host.example", + ); + assert.deepEqual(endpoints, [ + "rtsps://host.example:322", + "rtsps://host.example:48322", + ]); +}); + +test("collectRtspsEndpoints synthesizes from port when resourcePath missing", () => { + const endpoints = collectRtspsEndpoints( + [ + { usage: 14, port: 322, resourcePath: null }, + { usage: 14, port: 48322, resourcePath: null }, + ], + "host.example", + ); + assert.deepEqual(endpoints, [ + "rtsps://host.example:322", + "rtsps://host.example:48322", + ]); +}); diff --git a/opennow-stable/src/main/platforms/gfn/nvstRtsp/probe.ts b/opennow-stable/src/main/platforms/gfn/nvstRtsp/probe.ts new file mode 100644 index 000000000..da5da8264 --- /dev/null +++ b/opennow-stable/src/main/platforms/gfn/nvstRtsp/probe.ts @@ -0,0 +1,329 @@ +/** + * Classic NVST RTSPS-over-WSS handshake (GO-with-Moonlight-hypothesis). + * + * Runs OPTIONS → DESCRIBE → SETUP video/0/0 → ANNOUNCE → PLAY against `:322`. + * Extracts or generates runtime.encryptionKey for SRTP, then returns nvstVideo + * handoff fields for the native UDP receive scaffold. Does not keep the UDP + * socket open across the process boundary — native rebinds clientUdpPort. + * + * Evidence: docs/research/nvst-wire-format.md, nvst-srtp-key-derivation.md, + * nvst-announce-allowlist-1080p60.json. + */ + +import { createSocket, type Socket } from "node:dgram"; + +import type { NvstVideoSession } from "@shared/gfn"; + +import { + extractVideoPeer, + header, + RtspOverWssClient, +} from "./rtspClient"; +import { + buildAnnounceSdp, + extractHmacSeed, + extractRuntimeEncryptionKey, + generateClientEncryptionKey, + packSrtpMasterKeySalt, + redactKey, +} from "./sdp"; + +const DEFAULT_PROBE_TIMEOUT_MS = 20_000; + +export interface NvstRtspProbeInput { + sessionId: string; + rtspsEndpoints: string[]; + resolution?: string; + fps?: number; + codec?: string; + timeoutMs?: number; + onLog?: (message: string) => void; +} + +export interface NvstSrtpMaterial { + /** 64-char hex AES-256 key (runtime.encryptionKey). */ + aesKeyHex: string; + /** Unsigned key id used for salt packing (runtime.encryptionKeyId as u32). */ + keyId: number; + /** 88-char hex libsrtp master key||salt (AES-256 || 12-byte salt). */ + masterKeySaltHex: string; + /** True when OpenNOW generated the key for ANNOUNCE (DESCRIBE lacked it). */ + clientGenerated: boolean; +} + +export interface NvstRtspProbeResult { + ok: boolean; + endpoint: string; + session?: string; + hmacSeedPresent: boolean; + videoPeer?: { ip: string; port: number }; + clientUdpPort?: number; + srtp?: NvstSrtpMaterial; + pingPayload?: string; + pingVersion?: number; + /** Handoff for native UDP video (shared NvstVideoSession shape). */ + videoSession?: NvstVideoSession; + steps: string[]; + error?: string; +} + +function log(onLog: NvstRtspProbeInput["onLog"], message: string): void { + console.log(`[NvstRtspProbe] ${message}`); + onLog?.(message); +} + +export function selectPrimaryRtspsEndpoint(endpoints: string[]): string | null { + const normalized = endpoints + .map((value) => value.trim()) + .filter((value) => /^rtsps?:\/\//i.test(value)); + if (normalized.length === 0) { + return null; + } + const port322 = normalized.find((url) => /:322(?:\/|$)/.test(url)); + return port322 ?? normalized[0] ?? null; +} + +/** + * Logging identity only (`wss://host:port`, no path). Real `:322` connect uses + * {@link connectNvstWss} with absolute-form upgrade cascade (rtsps/wss/https). + * See docs/research/nvst-rtsps-wss-connect.md and `_tmp-bifrost2-ws-400-followup.txt`. + */ +export function rtspsUrlToWssUrl(rtspsUrl: string): string { + const parsed = new URL(rtspsUrl.replace(/^rtsps:/i, "https:").replace(/^rtsp:/i, "http:")); + const port = parsed.port || "322"; + return `wss://${parsed.hostname}:${port}`; +} + +export function collectRtspsEndpoints( + connections: Array<{ usage?: number; port?: number; resourcePath?: string | null }>, + fallbackHost?: string | null, +): string[] { + const endpoints: string[] = []; + const seen = new Set(); + + for (const conn of connections) { + if (conn.usage !== 14) { + continue; + } + const resourcePath = typeof conn.resourcePath === "string" ? conn.resourcePath.trim() : ""; + if (/^rtsps?:\/\//i.test(resourcePath)) { + if (!seen.has(resourcePath)) { + seen.add(resourcePath); + endpoints.push(resourcePath); + } + continue; + } + if (!fallbackHost || !conn.port) { + continue; + } + const synthesized = `rtsps://${fallbackHost}:${conn.port}`; + if (!seen.has(synthesized)) { + seen.add(synthesized); + endpoints.push(synthesized); + } + } + + return endpoints; +} + +async function bindEphemeralUdp(): Promise<{ socket: Socket; port: number }> { + const socket = createSocket("udp4"); + await new Promise((resolve, reject) => { + socket.once("error", reject); + socket.bind(0, "0.0.0.0", () => { + socket.off("error", reject); + resolve(); + }); + }); + const address = socket.address(); + if (typeof address === "string") { + socket.close(); + throw new Error("Unexpected UDP socket address shape"); + } + return { socket, port: address.port }; +} + +export async function runNvstRtspHandshakeProbe(input: NvstRtspProbeInput): Promise { + const steps: string[] = []; + const endpoint = selectPrimaryRtspsEndpoint(input.rtspsEndpoints); + if (!endpoint) { + return { + ok: false, + endpoint: "", + hmacSeedPresent: false, + steps, + error: "No rtsps:// endpoints available on the session", + }; + } + + const timeoutMs = input.timeoutMs ?? DEFAULT_PROBE_TIMEOUT_MS; + const wssUrl = rtspsUrlToWssUrl(endpoint); + const parsedEndpoint = new URL(endpoint.replace(/^rtsps:/i, "https:").replace(/^rtsp:/i, "http:")); + const host = parsedEndpoint.hostname; + const port = Number(parsedEndpoint.port || "322"); + const client = new RtspOverWssClient( + host, + port, + timeoutMs, + (message) => log(input.onLog, message), + ); + let udp: { socket: Socket; port: number } | null = null; + + try { + log( + input.onLog, + `Connecting RTSPS WSS ${wssUrl} via raw-TLS Bifrost-shaped upgrade (GET / then /v2/session/) (session ${input.sessionId})`, + ); + await client.connect(input.sessionId); + steps.push("wss-open"); + + const options = await client.request("OPTIONS", endpoint); + if (options.statusCode !== 200) { + throw new Error(`OPTIONS failed: ${options.statusCode} ${options.statusText}`); + } + steps.push("options"); + log(input.onLog, `OPTIONS ok (X-GS-Version=${header(options.headers, "x-gs-version") ?? "n/a"})`); + + const describe = await client.request("DESCRIBE", endpoint, { + Accept: "application/sdp", + }); + if (describe.statusCode !== 200) { + throw new Error(`DESCRIBE failed: ${describe.statusCode} ${describe.statusText}`); + } + steps.push("describe"); + const session = header(describe.headers, "session")?.split(";")[0]?.trim(); + if (!session) { + throw new Error("DESCRIBE response missing Session header"); + } + const hmacSeed = extractHmacSeed(describe.body); + const describedKey = extractRuntimeEncryptionKey(describe.body); + let encryptionKeyHex: string; + let encryptionKeyId: number; + let clientGenerated = false; + if (describedKey) { + encryptionKeyHex = describedKey.aesKeyHex; + encryptionKeyId = describedKey.keyId; + log( + input.onLog, + `DESCRIBE ok (Session=${session}, HMAC ${hmacSeed ? "present" : "missing"}, encryptionKey ${redactKey(encryptionKeyHex)} from server)`, + ); + } else { + const generated = generateClientEncryptionKey(); + encryptionKeyHex = generated.aesKeyHex; + encryptionKeyId = generated.keyId; + clientGenerated = true; + log( + input.onLog, + `DESCRIBE ok (Session=${session}, HMAC ${hmacSeed ? "present" : "missing"}, encryptionKey absent — client-generated ${redactKey(encryptionKeyHex)} for ANNOUNCE)`, + ); + } + + udp = await bindEphemeralUdp(); + const clientPort = udp.port; + // Transport uses X-GS-ClientPort (GameStream/Moonlight family). Official logs omit the + // client Transport summary string; server still returns X-GS-ServerPort + source. + const setup = await client.request("SETUP", `${endpoint}/streamid=video/0/0`, { + Session: session, + Transport: `unicast;X-GS-ClientPort=${clientPort}-${clientPort + 1}`, + }); + if (setup.statusCode !== 200) { + throw new Error(`SETUP video failed: ${setup.statusCode} ${setup.statusText}`); + } + steps.push("setup-video"); + const videoPeer = extractVideoPeer(header(setup.headers, "transport")); + const pingPayload = header(setup.headers, "x-nv-ping-payload"); + const pingVersionRaw = header(setup.headers, "x-nv-ping"); + const pingVersion = pingVersionRaw ? Number(pingVersionRaw) : undefined; + log( + input.onLog, + `SETUP video/0/0 ok (clientPort=${clientPort}, peer=${videoPeer ? `${videoPeer.ip}:${videoPeer.port}` : "unknown"})`, + ); + + const srtp: NvstSrtpMaterial = { + aesKeyHex: encryptionKeyHex, + keyId: encryptionKeyId, + masterKeySaltHex: packSrtpMasterKeySalt(encryptionKeyHex, encryptionKeyId), + clientGenerated, + }; + + const announceBody = buildAnnounceSdp({ + resolution: input.resolution, + fps: input.fps, + encryptionKeyHex, + encryptionKeyId, + }); + const announce = await client.request( + "ANNOUNCE", + endpoint, + { + Session: session, + "Content-Type": "application/sdp", + }, + announceBody, + ); + if (announce.statusCode !== 200) { + throw new Error(`ANNOUNCE failed: ${announce.statusCode} ${announce.statusText}`); + } + steps.push("announce"); + log(input.onLog, "ANNOUNCE ok (allowlist + encryptionKey; ICE/DTLS omitted)"); + + const play = await client.request("PLAY", endpoint, { + Session: session, + Range: "npt=0.000-", + }); + if (play.statusCode !== 200) { + throw new Error(`PLAY failed: ${play.statusCode} ${play.statusText}`); + } + steps.push("play"); + + // Close the probe UDP socket so the native streamer can rebind the same port. + udp.socket.close(); + udp = null; + + if (!videoPeer) { + throw new Error("SETUP did not return video peer (X-GS-ServerPort/source)"); + } + + const videoSession: NvstVideoSession = { + clientUdpPort: clientPort, + videoPeerIp: videoPeer.ip, + videoPeerPort: videoPeer.port, + srtpAesKeyHex: srtp.aesKeyHex, + srtpKeyId: srtp.keyId, + pingPayload, + codec: input.codec, + }; + + log( + input.onLog, + `PLAY ok — NVST video handoff ready (peer ${videoPeer.ip}:${videoPeer.port}, clientUdp ${clientPort}); WebRTC remains for SCTP input`, + ); + + return { + ok: true, + endpoint, + session, + hmacSeedPresent: Boolean(hmacSeed), + videoPeer, + clientUdpPort: clientPort, + srtp, + pingPayload, + pingVersion: Number.isFinite(pingVersion) ? pingVersion : undefined, + videoSession, + steps, + }; + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + log(input.onLog, `Probe failed: ${message}`); + return { + ok: false, + endpoint, + hmacSeedPresent: false, + steps, + error: message, + }; + } finally { + client.close(); + udp?.socket.close(); + } +} diff --git a/opennow-stable/src/main/platforms/gfn/nvstRtsp/rtspClient.test.ts b/opennow-stable/src/main/platforms/gfn/nvstRtsp/rtspClient.test.ts new file mode 100644 index 000000000..a021bb644 --- /dev/null +++ b/opennow-stable/src/main/platforms/gfn/nvstRtsp/rtspClient.test.ts @@ -0,0 +1,29 @@ +/// + +import test from "node:test"; +import assert from "node:assert/strict"; + +import { extractVideoPeer, parseRtspResponse } from "./rtspClient"; + +test("extractVideoPeer prefers SETUP X-GS-ServerPort", () => { + assert.deepEqual( + extractVideoPeer("unicast;X-GS-ServerPort=5004-5005;source=80.250.97.37"), + { ip: "80.250.97.37", port: 5004 }, + ); +}); + +test("parseRtspResponse preserves status, normalized headers, and SDP body", () => { + const response = parseRtspResponse( + "RTSP/1.0 200 OK\r\nCSeq: 2\r\nSession: session-id;timeout=60\r\nContent-Length: 5\r\n\r\nv=0\r\n", + ); + assert.deepEqual(response, { + statusCode: 200, + statusText: "OK", + headers: { + cseq: "2", + session: "session-id;timeout=60", + "content-length": "5", + }, + body: "v=0\n", + }); +}); diff --git a/opennow-stable/src/main/platforms/gfn/nvstRtsp/rtspClient.ts b/opennow-stable/src/main/platforms/gfn/nvstRtsp/rtspClient.ts new file mode 100644 index 000000000..2136dfbc2 --- /dev/null +++ b/opennow-stable/src/main/platforms/gfn/nvstRtsp/rtspClient.ts @@ -0,0 +1,237 @@ +import type { Duplex } from "node:stream"; + +import { connectNvstWss, encodeWsTextFrame, WsFrameReader } from "./websocketTransport"; + +const GS_VERSION = "14.2"; + +export interface ParsedRtspResponse { + statusCode: number; + statusText: string; + headers: Record; + body: string; +} + +export function parseRtspResponse(raw: string): ParsedRtspResponse { + const normalized = raw.replace(/\r\n/g, "\n"); + const splitAt = normalized.indexOf("\n\n"); + const headerBlock = splitAt >= 0 ? normalized.slice(0, splitAt) : normalized; + const body = splitAt >= 0 ? normalized.slice(splitAt + 2) : ""; + const headerLines = headerBlock.split("\n").filter((line) => line.length > 0); + const statusLine = headerLines[0] ?? ""; + const statusMatch = /^(?:RTSP|HTTP)\/\d(?:\.\d)?\s+(\d{3})\s*(.*)$/i.exec(statusLine); + if (!statusMatch) { + throw new Error(`Invalid RTSP status line: ${statusLine.slice(0, 120)}`); + } + const headers: Record = {}; + for (const line of headerLines.slice(1)) { + const idx = line.indexOf(":"); + if (idx <= 0) { + continue; + } + headers[line.slice(0, idx).trim().toLowerCase()] = line.slice(idx + 1).trim(); + } + return { + statusCode: Number(statusMatch[1]), + statusText: statusMatch[2]?.trim() ?? "", + headers, + body, + }; +} + +export function header(headers: Record, name: string): string | undefined { + return headers[name.toLowerCase()]; +} + +export function extractVideoPeer( + transport: string | undefined, +): { ip: string; port: number } | undefined { + if (!transport) { + return undefined; + } + const portMatch = /X-GS-ServerPort=(\d+)/i.exec(transport); + const sourceMatch = /source=([^;,\s]+)/i.exec(transport); + if (!portMatch || !sourceMatch) { + return undefined; + } + return { ip: sourceMatch[1]!, port: Number(portMatch[1]) }; +} + +export class RtspOverWssClient { + private socket: Duplex | null = null; + private frameReader = new WsFrameReader(); + private buffer = Buffer.alloc(0); + private cseq = 0; + private pending: { + resolve: (response: ParsedRtspResponse) => void; + reject: (error: Error) => void; + } | null = null; + + constructor( + private readonly host: string, + private readonly port: number, + private readonly timeoutMs: number, + private readonly onLog?: (message: string) => void, + ) {} + + async connect(sessionId?: string): Promise { + const socket = await connectNvstWss( + this.host, + this.port, + this.timeoutMs, + sessionId, + this.onLog, + ); + this.socket = socket; + socket.on("data", (chunk: Buffer) => this.onSocketData(chunk)); + socket.on("error", (error) => { + if (this.pending) { + this.pending.reject(error instanceof Error ? error : new Error(String(error))); + this.pending = null; + } + }); + socket.on("close", () => { + if (this.pending) { + this.pending.reject(new Error("RTSPS WebSocket closed")); + this.pending = null; + } + }); + } + + close(): void { + try { + this.socket?.destroy(); + } catch { + // ignore + } + this.socket = null; + if (this.pending) { + this.pending.reject(new Error("RTSPS probe closed")); + this.pending = null; + } + } + + async request( + method: string, + uri: string, + extraHeaders: Record = {}, + body = "", + ): Promise { + if (!this.socket || this.socket.destroyed) { + throw new Error("RTSPS WebSocket is not open"); + } + if (this.pending) { + throw new Error("Overlapping RTSP requests are not supported"); + } + + this.cseq += 1; + const headers: Record = { + CSeq: String(this.cseq), + "Request-Id": String(this.cseq), + "X-GS-Version": GS_VERSION, + ...extraHeaders, + }; + if (body.length > 0) { + headers["Content-Length"] = String(Buffer.byteLength(body, "utf8")); + } + + let message = `${method} ${uri} RTSP/1.0\r\n`; + for (const [key, value] of Object.entries(headers)) { + message += `${key}: ${value}\r\n`; + } + message += "\r\n"; + if (body.length > 0) { + message += body; + } + + return await new Promise((resolve, reject) => { + const timer = setTimeout(() => { + if (this.pending) { + this.pending = null; + reject(new Error(`RTSP ${method} timed out after ${this.timeoutMs}ms`)); + } + }, this.timeoutMs); + + this.pending = { + resolve: (response) => { + clearTimeout(timer); + resolve(response); + }, + reject: (error) => { + clearTimeout(timer); + reject(error); + }, + }; + + try { + this.socket?.write(encodeWsTextFrame(Buffer.from(message, "utf8"))); + } catch (error) { + clearTimeout(timer); + this.pending = null; + reject(error instanceof Error ? error : new Error(String(error))); + } + }); + } + + private onSocketData(chunk: Buffer): void { + try { + for (const payload of this.frameReader.push(chunk)) { + this.buffer = Buffer.concat([this.buffer, payload]); + this.tryCompleteResponse(); + } + } catch (error) { + if (this.pending) { + const pending = this.pending; + this.pending = null; + pending.reject(error instanceof Error ? error : new Error(String(error))); + } + } + } + + private tryCompleteResponse(): void { + if (!this.pending) { + return; + } + + const text = this.buffer.toString("utf8"); + const crlfSplit = text.indexOf("\r\n\r\n"); + const lfSplit = text.indexOf("\n\n"); + let headerEnd = -1; + let sepLen = 0; + if (crlfSplit >= 0 && (lfSplit < 0 || crlfSplit <= lfSplit)) { + headerEnd = crlfSplit; + sepLen = 4; + } else if (lfSplit >= 0) { + headerEnd = lfSplit; + sepLen = 2; + } + if (headerEnd < 0) { + return; + } + + const headerText = text.slice(0, headerEnd); + const contentLengthMatch = /^Content-Length:\s*(\d+)\s*$/im.exec(headerText); + const contentLength = contentLengthMatch ? Number(contentLengthMatch[1]) : 0; + const bodyStart = headerEnd + sepLen; + // Use byte length of remaining buffer after header separator. + const headerByteLength = Buffer.byteLength(text.slice(0, bodyStart), "utf8"); + const availableBodyBytes = this.buffer.length - headerByteLength; + if (availableBodyBytes < contentLength) { + return; + } + + const totalBytes = headerByteLength + contentLength; + const raw = this.buffer.subarray(0, totalBytes).toString("utf8"); + this.buffer = this.buffer.subarray(totalBytes); + + try { + const parsed = parseRtspResponse(raw); + const pending = this.pending; + this.pending = null; + pending?.resolve(parsed); + } catch (error) { + const pending = this.pending; + this.pending = null; + pending?.reject(error instanceof Error ? error : new Error(String(error))); + } + } +} diff --git a/opennow-stable/src/main/platforms/gfn/nvstRtsp/sdp.test.ts b/opennow-stable/src/main/platforms/gfn/nvstRtsp/sdp.test.ts new file mode 100644 index 000000000..d6900a87c --- /dev/null +++ b/opennow-stable/src/main/platforms/gfn/nvstRtsp/sdp.test.ts @@ -0,0 +1,46 @@ +/// + +import test from "node:test"; +import assert from "node:assert/strict"; + +import { + buildAnnounceSdp, + extractHmacSeed, + extractRuntimeEncryptionKey, + packSrtpMasterKeySalt, +} from "./sdp"; + +test("extractHmacSeed reads DESCRIBE k= line", () => { + const seed = extractHmacSeed( + "v=0\r\nk=HMAC:76A28E94D8C07CB67C04C29CFAAAAF64BE4BA0899456217CB73D070E5060965F\r\na=x-nv-general.rtspWebSocketPerConnection:1\r\n", + ); + assert.equal(seed, "76A28E94D8C07CB67C04C29CFAAAAF64BE4BA0899456217CB73D070E5060965F"); +}); + +test("buildAnnounceSdp uses allowlist shape and omits ICE/DTLS", () => { + const sdp = buildAnnounceSdp({ resolution: "1920x1080", fps: 60 }); + assert.match(sdp, /a=x-nv-video\[0\]\.clientViewportWd:1920/); + assert.match(sdp, /a=x-nv-video\[0\]\.maxFPS:60/); + assert.match(sdp, /a=x-nv-general\.controlProtocol:udp_ag/); + assert.doesNotMatch(sdp, /iceUsernameFragment|dtlsFingerprint/); +}); + +test("packSrtpMasterKeySalt matches geronimo keyId packing", () => { + const aes = `${"1C98".padEnd(60, "0")}07D2`; + const packed = packSrtpMasterKeySalt(aes, 2664076126); + assert.equal(packed.length, 88); + assert.equal(packed.slice(0, 64), aes.toUpperCase()); + assert.equal(packed.slice(64), "00000000000000009ECA935E"); +}); + +test("extractRuntimeEncryptionKey reads DESCRIBE attrs", () => { + const sdp = [ + "v=0", + "a=x-nv-runtime.encryptionKey:AABBCCDDEEFF00112233445566778899AABBCCDDEEFF00112233445566778899", + "a=x-nv-runtime.encryptionKeyId:-1630891170", + "", + ].join("\r\n"); + const parsed = extractRuntimeEncryptionKey(sdp); + assert.ok(parsed); + assert.equal(parsed?.keyId, 2664076126); +}); diff --git a/opennow-stable/src/main/platforms/gfn/nvstRtsp/sdp.ts b/opennow-stable/src/main/platforms/gfn/nvstRtsp/sdp.ts new file mode 100644 index 000000000..6f009571b --- /dev/null +++ b/opennow-stable/src/main/platforms/gfn/nvstRtsp/sdp.ts @@ -0,0 +1,195 @@ +import { randomBytes } from "node:crypto"; + +/** Minimal ANNOUNCE attrs from docs/research/nvst-announce-allowlist-1080p60.json */ +const ANNOUNCE_ALLOWLIST = { + video: { + clientViewportWd: "1920", + clientViewportHt: "1080", + maxFPS: "60", + videoSplitEncodeStripsPerFrame: "3", + updateSplitEncodeStateDynamically: "1", + packetSize: "1408", + enableRtpNack: "1", + rtpNackQueueLength: "2048", + rtpNackQueueMaxPackets: "1024", + rtpNackMaxPacketCount: "64", + "framePacing.mode": "2", + "framePacing.pid.minTargetFrameTimeUs": "16666", + "adaptiveQuantization.spatialAQSetting": "7", + "adaptiveQuantization.temporalAQSetting": "0", + "adaptiveQuantization.spatialAQStrength": "12", + "adaptiveQuantization.qpThresholdAdjPercent": "2", + "adaptiveQuantization.saqAdaptMinQpThresholdPercent": "40", + "adaptiveQuantization.saqAdaptMaxQpThresholdPercent": "100", + "adaptiveQuantization.saqAdaptDecayStrengthX100": "250", + "adaptiveQuantization.perfAdjEnablement": "1", + enableAv1RcPrecisionFactor: "1", + }, + vqos: { + "fec.enable": "1", + "fec.repairPercent": "20", + "fec.repairMinPercent": "5", + "fec.repairMaxPercent": "40", + "bllFec.enable": "1", + "grc.enable": "0", + "drc.enable": "0", + "dfc.adjustResAndFps": "0", + calculateAvgVideoStreamingBitrate: "1", + "bw.maximumBitrateKbps": "100000", + "bw.minimumBitrateKbps": "1000", + }, + packetPacing: { + version: "3", + mode: "1", + numGroups: "5", + maxDelayUs: "1000", + minNumPacketsFrame: "10", + minNumPacketsPerGroup: "0", + enableAccurateSleep: "1", + enableSmoothTransition: "1", + allowFpsBasedToggle: "1", + }, + ri: { + partialReliableThresholdMs: "300", + timestampsEnabled: "1", + useMultipleGamepads: "1", + usePartiallyReliableUdpChannel: "0", + enablePartiallyReliableTransferGamepad: "255", + enablePartiallyReliableTransferHid: "-1", + }, + aqos: { + enableRedundancy: "1", + redundancyLevel: "2", + }, + general: { + rtspWebSocketPerConnection: "1", + "enetControlChannel.mtuSize": "1191", + pingIntervalBeforeConnectionMs: "20", + pingIntervalAfterConnectionMs: "100", + }, + runtime: { + audioSrtp: "0", + micSrtp: "0", + }, +} as const; + +function parseResolution(resolution: string | undefined): { width: number; height: number } { + const match = /^(\d+)\s*[xX]\s*(\d+)$/.exec(resolution?.trim() ?? ""); + if (!match) { + return { width: 1920, height: 1080 }; + } + return { width: Number(match[1]), height: Number(match[2]) }; +} + +export function buildAnnounceSdp( + options: { + resolution?: string; + fps?: number; + encryptionKeyHex?: string; + encryptionKeyId?: number; + } = {}, +): string { + const { width, height } = parseResolution(options.resolution); + const fps = options.fps && options.fps > 0 ? Math.round(options.fps) : 60; + const frameTimeUs = String(Math.round(1_000_000 / fps)); + + const lines: string[] = [ + "v=0", + "o=- 0 0 IN IP4 127.0.0.1", + "s=OpenNOW NVST Handshake", + "t=0 0", + ]; + + const pushGroup = ( + prefix: string, + indexed: boolean, + values: Record, + ): void => { + for (const [key, value] of Object.entries(values)) { + let nextValue = value; + if (prefix === "video" && key === "clientViewportWd") { + nextValue = String(width); + } else if (prefix === "video" && key === "clientViewportHt") { + nextValue = String(height); + } else if (prefix === "video" && key === "maxFPS") { + nextValue = String(fps); + } else if (prefix === "video" && key === "framePacing.pid.minTargetFrameTimeUs") { + nextValue = frameTimeUs; + } + const name = indexed ? `x-nv-${prefix}[0].${key}` : `x-nv-${prefix}.${key}`; + lines.push(`a=${name}:${nextValue}`); + } + }; + + pushGroup("video", true, ANNOUNCE_ALLOWLIST.video); + pushGroup("vqos", true, ANNOUNCE_ALLOWLIST.vqos); + pushGroup("packetPacing", false, ANNOUNCE_ALLOWLIST.packetPacing); + pushGroup("ri", false, ANNOUNCE_ALLOWLIST.ri); + pushGroup("aqos", false, ANNOUNCE_ALLOWLIST.aqos); + pushGroup("general", false, ANNOUNCE_ALLOWLIST.general); + pushGroup("runtime", false, ANNOUNCE_ALLOWLIST.runtime); + lines.push("a=x-nv-runtime.videoSrtp:1"); + if (options.encryptionKeyHex && options.encryptionKeyId !== undefined) { + lines.push(`a=x-nv-runtime.encryptionKey:${options.encryptionKeyHex.toUpperCase()}`); + // Signed i32 form matches geronimo runtime.encryptionKeyId dumps. + const signedId = options.encryptionKeyId > 0x7fffffff + ? options.encryptionKeyId - 0x1_0000_0000 + : options.encryptionKeyId; + lines.push(`a=x-nv-runtime.encryptionKeyId:${signedId}`); + } + // Control protocol preference evidenced in geronimo: udp_ag (not encrypted). + lines.push("a=x-nv-general.controlProtocol:udp_ag"); + lines.push(""); + return lines.join("\r\n"); +} + +export function extractHmacSeed(sdp: string): string | null { + const match = /^k=HMAC:([0-9A-Fa-f]{64})\s*$/m.exec(sdp); + return match?.[1] ?? null; +} + +/** + * Pack AES-256 key + keyId into libsrtp master key||salt (88 hex). + * Salt = keyId as `%024x` (12 bytes BE). See docs/research/nvst-srtp-key-derivation.md. + */ +export function packSrtpMasterKeySalt(aesKeyHex: string, keyId: number): string { + const key = aesKeyHex.trim().toUpperCase(); + if (!/^[0-9A-F]{64}$/.test(key)) { + throw new Error(`encryptionKey must be 64 hex chars, got length ${key.length}`); + } + const id = keyId >>> 0; + const salt = id.toString(16).toUpperCase().padStart(24, "0"); + return `${key}${salt}`; +} + +export function extractRuntimeEncryptionKey( + sdp: string, +): { aesKeyHex: string; keyId: number } | null { + const keyMatch = /^a=x-nv-runtime\.encryptionKey:([0-9A-Fa-f]{64})\s*$/m.exec(sdp); + const idMatch = /^a=x-nv-runtime\.encryptionKeyId:(-?\d+)\s*$/m.exec(sdp); + if (!keyMatch || !idMatch) { + return null; + } + let keyId = Number(idMatch[1]); + if (!Number.isFinite(keyId)) { + return null; + } + // Signed i32 in SDP → unsigned u32 for salt packing. + if (keyId < 0) { + keyId = keyId + 0x1_0000_0000; + } + return { aesKeyHex: keyMatch[1]!.toUpperCase(), keyId: keyId >>> 0 }; +} + +export function generateClientEncryptionKey(): { aesKeyHex: string; keyId: number } { + const aesKeyHex = randomBytes(32).toString("hex").toUpperCase(); + const keyId = randomBytes(4).readUInt32BE(0); + return { aesKeyHex, keyId }; +} + +export function redactKey(aesKeyHex: string): string { + if (aesKeyHex.length < 8) { + return "****"; + } + return `${aesKeyHex.slice(0, 4)}…${aesKeyHex.slice(-4)}`; +} diff --git a/opennow-stable/src/main/platforms/gfn/nvstRtsp/websocketTransport.test.ts b/opennow-stable/src/main/platforms/gfn/nvstRtsp/websocketTransport.test.ts new file mode 100644 index 000000000..f6cf8004e --- /dev/null +++ b/opennow-stable/src/main/platforms/gfn/nvstRtsp/websocketTransport.test.ts @@ -0,0 +1,67 @@ +/// + +import test from "node:test"; +import assert from "node:assert/strict"; + +import { + buildEmptyPathUpgradeRequest, + buildNvstWssUpgradeRequest, + buildNvstWssUpgradeRequestTarget, + encodeWsTextFrame, + WsFrameReader, +} from "./websocketTransport"; + +test("buildNvstWssUpgradeRequest uses Bifrost-shaped GET / by default", () => { + const request = buildNvstWssUpgradeRequest( + "80-250-97-40.cloudmatchbeta.nvidiagrid.net", + 322, + "dGVzdGtleTEyMzQ1Njc4OQ==", + ); + const requestLine = request.split("\r\n")[0] ?? ""; + assert.equal(requestLine, "GET / HTTP/1.1"); + assert.equal(Buffer.from(requestLine, "utf8").toString("hex"), "474554202f20485454502f312e31"); + assert.equal(buildNvstWssUpgradeRequestTarget("host.example", 322, "slash"), "/"); + assert.equal( + buildNvstWssUpgradeRequestTarget("host.example", 322, "sessionPath", "abc-uuid"), + "/v2/session/abc-uuid", + ); + assert.match(request, /^GET \/ HTTP\/1\.1\r\nHost: 80-250-97-40\.cloudmatchbeta\.nvidiagrid\.net:322\r\n/); + assert.match(request, /\r\nConnection: Upgrade\r\n/); + assert.match(request, /\r\nUpgrade: websocket\r\n/); + assert.match(request, /\r\nSec-WebSocket-Version: 13\r\n/); + assert.match(request, /\r\nSec-WebSocket-Key: dGVzdGtleTEyMzQ1Njc4OQ==\r\n/); + assert.match(request, /\r\nContent-Length: 0\r\n\r\n$/); + assert.doesNotMatch(request, /Sec-WebSocket-Protocol/i); + assert.doesNotMatch(request, /User-Agent/i); + assert.doesNotMatch(request, /x-nv-sessionid/i); +}); + +test("buildEmptyPathUpgradeRequest keeps empty URI for research (live → 400)", () => { + const request = buildEmptyPathUpgradeRequest( + "80-250-97-40.cloudmatchbeta.nvidiagrid.net", + 322, + "dGVzdGtleTEyMzQ1Njc4OQ==", + ); + const requestLine = request.split("\r\n")[0] ?? ""; + assert.equal(requestLine, "GET HTTP/1.1"); + assert.equal(Buffer.from(requestLine, "utf8").toString("hex"), "4745542020485454502f312e31"); +}); + +test("buildNvstWssUpgradeRequest can attach x-nv-sessionid for 403 retry", () => { + const request = buildNvstWssUpgradeRequest("host.example", 322, "abc", { + form: "slash", + sessionId: "sess-uuid", + }); + assert.match(request, /\r\nx-nv-sessionid: sess-uuid\r\n/); +}); + +test("WebSocket framing preserves masked client payload bytes", () => { + const payload = Buffer.from("OPTIONS rtsps://host.example:322 RTSP/1.0\r\n\r\n"); + const frame = encodeWsTextFrame(payload); + assert.equal(frame[0], 0x81); + assert.equal(frame[1]! & 0x80, 0x80); + + const reader = new WsFrameReader(); + assert.deepEqual(reader.push(frame.subarray(0, 3)), []); + assert.deepEqual(reader.push(frame.subarray(3)), [payload]); +}); diff --git a/opennow-stable/src/main/platforms/gfn/nvstRtsp/websocketTransport.ts b/opennow-stable/src/main/platforms/gfn/nvstRtsp/websocketTransport.ts new file mode 100644 index 000000000..3d0f334f5 --- /dev/null +++ b/opennow-stable/src/main/platforms/gfn/nvstRtsp/websocketTransport.ts @@ -0,0 +1,332 @@ +import { createHash, randomBytes } from "node:crypto"; +import type { Duplex } from "node:stream"; +import { connect as tlsConnect, type TLSSocket } from "node:tls"; + +/** + * Official `:322` upgrade request-target is still under research. + * + * Live matrix (raw TLS unless noted): + * - empty (`GET HTTP/1.1`) → HTTP 400 + * - absolute `rtsps://` / `wss://` / `https://` → HTTP 404 + * - `/` via Node `ws` → HTTP 404 (extra Client headers; not Bifrost-shaped) + * - `/` via raw TLS Bifrost-shaped headers → **pending** (never tried) + * + * Poco 1.14.1 WebSocket::connect does not set method/URI; it only adds Upgrade + * headers. Default HTTPRequest is GET `/`. See docs/research/_tmp-bifrost2-ws-uri-resolved.txt. + */ +export type NvstWssUpgradeTargetForm = + | "slash" + | "sessionPath" + | "rtsps" + | "wss" + | "https" + | "empty"; + +export function buildNvstWssUpgradeRequestTarget( + host: string, + port: number, + form: NvstWssUpgradeTargetForm, + sessionId?: string, +): string { + switch (form) { + case "slash": + return "/"; + case "sessionPath": + return sessionId && sessionId.trim().length > 0 + ? `/v2/session/${sessionId.trim()}` + : "/"; + case "rtsps": + return `rtsps://${host}:${port}`; + case "wss": + return `wss://${host}:${port}`; + case "https": + return `https://${host}:${port}`; + case "empty": + return ""; + } +} + +/** + * Raw TLS WebSocket upgrade for NVST `:322`. + * Header order matches Poco WebSocket::connect + Bifrost Content-Length: 0. + * Optional `x-nv-sessionid` is only for a 403 retry. + */ +export function buildNvstWssUpgradeRequest( + host: string, + port: number, + secWebSocketKey: string, + options: { + form?: NvstWssUpgradeTargetForm; + sessionId?: string; + } = {}, +): string { + const form = options.form ?? "slash"; + const target = buildNvstWssUpgradeRequestTarget(host, port, form, options.sessionId); + const requestLine = `GET ${target} HTTP/1.1`; + // Poco sets Connection/Upgrade/Version/Key; Bifrost presets Content-Length: 0. + // Host is auto-set by HTTPClientSession (host:port for non-443). + let request = + `${requestLine}\r\n` + + `Host: ${host}:${port}\r\n` + + `Connection: Upgrade\r\n` + + `Upgrade: websocket\r\n` + + `Sec-WebSocket-Version: 13\r\n` + + `Sec-WebSocket-Key: ${secWebSocketKey}\r\n` + + `Content-Length: 0\r\n`; + const sessionId = options.sessionId?.trim(); + if (sessionId && form !== "sessionPath") { + // Only attach as header on 403 retry paths; sessionPath already uses UUID in URI. + request += `x-nv-sessionid: ${sessionId}\r\n`; + } + return `${request}\r\n`; +} + +/** @deprecated Empty path is live-falsified (HTTP 400). */ +export function buildEmptyPathUpgradeRequest( + host: string, + port: number, + secWebSocketKey: string, + sessionId?: string, +): string { + return buildNvstWssUpgradeRequest(host, port, secWebSocketKey, { + form: "empty", + sessionId, + }); +} + +function connectNvstWssOnce( + host: string, + port: number, + timeoutMs: number, + form: NvstWssUpgradeTargetForm, + sessionId?: string, + attachSessionHeader = false, +): Promise { + const key = randomBytes(16).toString("base64"); + const expectedAccept = createHash("sha1") + .update(key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11") + .digest("base64"); + const request = buildNvstWssUpgradeRequest(host, port, key, { + form, + // For slash/rtsps/… first attempt: no session header. + // For 403 retry or sessionPath: pass sessionId. + sessionId: attachSessionHeader || form === "sessionPath" ? sessionId : undefined, + }); + const requestLine = request.split("\r\n")[0] ?? ""; + const requestLineHex = Buffer.from(requestLine, "utf8").toString("hex"); + + return new Promise((resolve, reject) => { + let settled = false; + let buffer = Buffer.alloc(0); + let socket: TLSSocket | null = null; + + const fail = (error: Error): void => { + if (settled) { + return; + } + settled = true; + try { + socket?.destroy(); + } catch { + // ignore + } + reject(error); + }; + + const succeed = (tlsSocket: TLSSocket, leftover: Buffer): void => { + if (settled) { + return; + } + settled = true; + tlsSocket.removeAllListeners("data"); + tlsSocket.removeAllListeners("error"); + tlsSocket.removeAllListeners("timeout"); + tlsSocket.setTimeout(0); + if (leftover.length > 0) { + tlsSocket.unshift(leftover); + } + resolve(tlsSocket); + }; + + socket = tlsConnect( + { + host, + port, + servername: host, + rejectUnauthorized: true, + }, + () => { + socket?.write(request); + }, + ); + + socket.setTimeout(timeoutMs); + socket.on("timeout", () => fail(new Error(`WSS upgrade timed out after ${timeoutMs}ms`))); + socket.on("error", (error) => fail(error instanceof Error ? error : new Error(String(error)))); + socket.on("data", (chunk: Buffer) => { + buffer = Buffer.concat([buffer, chunk]); + const headerEnd = buffer.indexOf("\r\n\r\n"); + if (headerEnd < 0) { + if (buffer.length > 16_384) { + fail(new Error("WSS upgrade failed: response headers too large")); + } + return; + } + + const headerText = buffer.subarray(0, headerEnd).toString("utf8"); + const leftover = buffer.subarray(headerEnd + 4); + const statusLine = headerText.split("\r\n")[0] ?? ""; + const statusMatch = /^HTTP\/\d(?:\.\d)?\s+(\d{3})\s*(.*)$/i.exec(statusLine); + const statusCode = statusMatch ? Number(statusMatch[1]) : 0; + const headers: Record = {}; + for (const line of headerText.split("\r\n").slice(1)) { + const idx = line.indexOf(":"); + if (idx <= 0) { + continue; + } + headers[line.slice(0, idx).trim().toLowerCase()] = line.slice(idx + 1).trim(); + } + + if (statusCode !== 101) { + fail( + new Error( + `WSS upgrade failed: HTTP ${statusCode || "unknown"} (${statusLine || "no status"}); ` + + `form=${form} request-line=${requestLine} hex=${requestLineHex}` + + (attachSessionHeader ? " with x-nv-sessionid" : ""), + ), + ); + return; + } + if (headers["sec-websocket-accept"] !== expectedAccept) { + fail(new Error("WSS upgrade failed: invalid Sec-WebSocket-Accept")); + return; + } + succeed(socket!, leftover); + }); + }); +} + +/** Primary: Bifrost-shaped GET /. Fallback: CloudMatch-style /v2/session/. */ +const UPGRADE_TARGET_FORMS: NvstWssUpgradeTargetForm[] = ["slash", "sessionPath"]; + +export async function connectNvstWss( + host: string, + port: number, + timeoutMs: number, + sessionId?: string, + onLog?: (message: string) => void, +): Promise { + let lastError: Error | null = null; + for (const form of UPGRADE_TARGET_FORMS) { + try { + const target = buildNvstWssUpgradeRequestTarget(host, port, form, sessionId); + onLog?.( + `Trying WSS upgrade form=${form} (GET ${target} HTTP/1.1) raw-TLS Bifrost headers`, + ); + return await connectNvstWssOnce(host, port, timeoutMs, form, sessionId, false); + } catch (error) { + const err = error instanceof Error ? error : new Error(String(error)); + lastError = err; + const message = err.message; + if (sessionId && /\bHTTP 403\b/.test(message)) { + onLog?.(`Upgrade HTTP 403 on form=${form}; retrying with x-nv-sessionid`); + try { + return await connectNvstWssOnce(host, port, timeoutMs, form, sessionId, true); + } catch (retryError) { + lastError = retryError instanceof Error ? retryError : new Error(String(retryError)); + } + } + if (!/\bHTTP (400|404)\b/.test(message)) { + throw lastError; + } + onLog?.(message); + } + } + throw lastError ?? new Error("WSS upgrade failed for all request-target forms"); +} + +export function encodeWsTextFrame(payload: Buffer): Buffer { + const len = payload.length; + const mask = randomBytes(4); + let header: Buffer; + if (len < 126) { + header = Buffer.alloc(2); + header[0] = 0x81; // FIN + text + header[1] = 0x80 | len; // MASK bit set + } else if (len < 65536) { + header = Buffer.alloc(4); + header[0] = 0x81; + header[1] = 0x80 | 126; + header.writeUInt16BE(len, 2); + } else { + header = Buffer.alloc(10); + header[0] = 0x81; + header[1] = 0x80 | 127; + header.writeUInt32BE(0, 2); + header.writeUInt32BE(len, 6); + } + const masked = Buffer.alloc(len); + for (let i = 0; i < len; i++) { + masked[i] = payload[i]! ^ mask[i % 4]!; + } + return Buffer.concat([header, mask, masked]); +} + +/** Minimal client-side WS frame reader (text + close; ignores ping/pong/control). */ +export class WsFrameReader { + private buffer = Buffer.alloc(0); + + push(chunk: Buffer): Buffer[] { + this.buffer = Buffer.concat([this.buffer, chunk]); + const messages: Buffer[] = []; + while (true) { + if (this.buffer.length < 2) { + break; + } + const b1 = this.buffer[1]!; + const masked = (b1 & 0x80) !== 0; + let payloadLen = b1 & 0x7f; + let offset = 2; + if (payloadLen === 126) { + if (this.buffer.length < 4) { + break; + } + payloadLen = this.buffer.readUInt16BE(2); + offset = 4; + } else if (payloadLen === 127) { + if (this.buffer.length < 10) { + break; + } + const high = this.buffer.readUInt32BE(2); + const low = this.buffer.readUInt32BE(6); + if (high !== 0 || low > 0x7fffffff) { + throw new Error("WS frame too large"); + } + payloadLen = low; + offset = 10; + } + const maskLen = masked ? 4 : 0; + if (this.buffer.length < offset + maskLen + payloadLen) { + break; + } + const opcode = this.buffer[0]! & 0x0f; + let payload = this.buffer.subarray(offset + maskLen, offset + maskLen + payloadLen); + if (masked) { + const mask = this.buffer.subarray(offset, offset + 4); + const unmasked = Buffer.alloc(payloadLen); + for (let i = 0; i < payloadLen; i++) { + unmasked[i] = payload[i]! ^ mask[i % 4]!; + } + payload = unmasked; + } + this.buffer = this.buffer.subarray(offset + maskLen + payloadLen); + if (opcode === 0x1 || opcode === 0x2) { + messages.push(payload); + } else if (opcode === 0x8) { + throw new Error("RTSPS WebSocket closed"); + } + // 0x9/0xa ping/pong ignored for probe + } + return messages; + } +} diff --git a/opennow-stable/src/main/platforms/gfn/nvstRtspProbe.test.ts b/opennow-stable/src/main/platforms/gfn/nvstRtspProbe.test.ts new file mode 100644 index 000000000..72806e68b --- /dev/null +++ b/opennow-stable/src/main/platforms/gfn/nvstRtspProbe.test.ts @@ -0,0 +1,23 @@ +/// + +import test from "node:test"; +import assert from "node:assert/strict"; + +import * as nvstRtspProbe from "./nvstRtspProbe"; + +test("nvstRtspProbe preserves its public runtime exports", () => { + assert.deepEqual(Object.keys(nvstRtspProbe).sort(), [ + "buildAnnounceSdp", + "buildEmptyPathUpgradeRequest", + "buildNvstWssUpgradeRequest", + "buildNvstWssUpgradeRequestTarget", + "collectRtspsEndpoints", + "extractHmacSeed", + "extractRuntimeEncryptionKey", + "extractVideoPeer", + "packSrtpMasterKeySalt", + "rtspsUrlToWssUrl", + "runNvstRtspHandshakeProbe", + "selectPrimaryRtspsEndpoint", + ]); +}); diff --git a/opennow-stable/src/main/platforms/gfn/nvstRtspProbe.ts b/opennow-stable/src/main/platforms/gfn/nvstRtspProbe.ts new file mode 100644 index 000000000..5f36f318f --- /dev/null +++ b/opennow-stable/src/main/platforms/gfn/nvstRtspProbe.ts @@ -0,0 +1,24 @@ +export { + buildAnnounceSdp, + extractHmacSeed, + extractRuntimeEncryptionKey, + packSrtpMasterKeySalt, +} from "./nvstRtsp/sdp"; +export { + buildEmptyPathUpgradeRequest, + buildNvstWssUpgradeRequest, + buildNvstWssUpgradeRequestTarget, +} from "./nvstRtsp/websocketTransport"; +export type { NvstWssUpgradeTargetForm } from "./nvstRtsp/websocketTransport"; +export { extractVideoPeer } from "./nvstRtsp/rtspClient"; +export { + collectRtspsEndpoints, + rtspsUrlToWssUrl, + runNvstRtspHandshakeProbe, + selectPrimaryRtspsEndpoint, +} from "./nvstRtsp/probe"; +export type { + NvstRtspProbeInput, + NvstRtspProbeResult, + NvstSrtpMaterial, +} from "./nvstRtsp/probe"; diff --git a/opennow-stable/src/main/gfn/paginatedApps.test.ts b/opennow-stable/src/main/platforms/gfn/paginatedApps.test.ts similarity index 100% rename from opennow-stable/src/main/gfn/paginatedApps.test.ts rename to opennow-stable/src/main/platforms/gfn/paginatedApps.test.ts diff --git a/opennow-stable/src/main/gfn/paginatedApps.ts b/opennow-stable/src/main/platforms/gfn/paginatedApps.ts similarity index 100% rename from opennow-stable/src/main/gfn/paginatedApps.ts rename to opennow-stable/src/main/platforms/gfn/paginatedApps.ts diff --git a/opennow-stable/src/main/gfn/paywall.ts b/opennow-stable/src/main/platforms/gfn/paywall.ts similarity index 100% rename from opennow-stable/src/main/gfn/paywall.ts rename to opennow-stable/src/main/platforms/gfn/paywall.ts diff --git a/opennow-stable/src/main/gfn/persistentStorage.test.ts b/opennow-stable/src/main/platforms/gfn/persistentStorage.test.ts similarity index 100% rename from opennow-stable/src/main/gfn/persistentStorage.test.ts rename to opennow-stable/src/main/platforms/gfn/persistentStorage.test.ts diff --git a/opennow-stable/src/main/gfn/persistentStorage.ts b/opennow-stable/src/main/platforms/gfn/persistentStorage.ts similarity index 100% rename from opennow-stable/src/main/gfn/persistentStorage.ts rename to opennow-stable/src/main/platforms/gfn/persistentStorage.ts diff --git a/opennow-stable/src/main/gfn/proxyFetch.test.ts b/opennow-stable/src/main/platforms/gfn/proxyFetch.test.ts similarity index 100% rename from opennow-stable/src/main/gfn/proxyFetch.test.ts rename to opennow-stable/src/main/platforms/gfn/proxyFetch.test.ts diff --git a/opennow-stable/src/main/gfn/proxyFetch.ts b/opennow-stable/src/main/platforms/gfn/proxyFetch.ts similarity index 100% rename from opennow-stable/src/main/gfn/proxyFetch.ts rename to opennow-stable/src/main/platforms/gfn/proxyFetch.ts diff --git a/opennow-stable/src/main/gfn/proxyUrl.ts b/opennow-stable/src/main/platforms/gfn/proxyUrl.ts similarity index 100% rename from opennow-stable/src/main/gfn/proxyUrl.ts rename to opennow-stable/src/main/platforms/gfn/proxyUrl.ts diff --git a/opennow-stable/src/main/gfn/publicGames.ts b/opennow-stable/src/main/platforms/gfn/publicGames.ts similarity index 100% rename from opennow-stable/src/main/gfn/publicGames.ts rename to opennow-stable/src/main/platforms/gfn/publicGames.ts diff --git a/opennow-stable/src/main/gfn/request.ts b/opennow-stable/src/main/platforms/gfn/request.ts similarity index 100% rename from opennow-stable/src/main/gfn/request.ts rename to opennow-stable/src/main/platforms/gfn/request.ts diff --git a/opennow-stable/src/main/gfn/signaling.ts b/opennow-stable/src/main/platforms/gfn/signaling.ts similarity index 97% rename from opennow-stable/src/main/gfn/signaling.ts rename to opennow-stable/src/main/platforms/gfn/signaling.ts index da39cb316..168d7c4cb 100644 --- a/opennow-stable/src/main/gfn/signaling.ts +++ b/opennow-stable/src/main/platforms/gfn/signaling.ts @@ -8,9 +8,7 @@ import type { MainToRendererSignalingEvent, SendAnswerRequest, } from "@shared/gfn"; - -const USER_AGENT = - "Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/131.0.0.0 Safari/537.36"; +import { GFN_PLAY_ORIGIN, GFN_USER_AGENT } from "./clientHeaders"; interface SignalingMessage { ackid?: number; @@ -136,11 +134,10 @@ export class GfnSignalingClient { const urlHost = url.replace(/^wss?:\/\//, "").split("/")[0]; const ws = new WebSocket(url, protocol, { - rejectUnauthorized: false, headers: { Host: urlHost, - Origin: "https://play.geforcenow.com", - "User-Agent": USER_AGENT, + Origin: GFN_PLAY_ORIGIN, + "User-Agent": GFN_USER_AGENT, "Sec-WebSocket-Key": randomBytes(16).toString("base64"), }, }); diff --git a/opennow-stable/src/main/gfn/subscription.test.ts b/opennow-stable/src/main/platforms/gfn/subscription.test.ts similarity index 89% rename from opennow-stable/src/main/gfn/subscription.test.ts rename to opennow-stable/src/main/platforms/gfn/subscription.test.ts index d628055ef..6e702315c 100644 --- a/opennow-stable/src/main/gfn/subscription.test.ts +++ b/opennow-stable/src/main/platforms/gfn/subscription.test.ts @@ -38,6 +38,12 @@ test("fetchSubscription exposes only entitled resolution and fps profiles", asyn framesPerSecond: 60, isEntitled: true, }, + { + widthInPixels: 1280, + heightInPixels: 800, + framesPerSecond: 90, + isEntitled: true, + }, { widthInPixels: 1920, heightInPixels: 1080, @@ -59,5 +65,6 @@ test("fetchSubscription exposes only entitled resolution and fps profiles", asyn assert.deepEqual(subscription.entitledResolutions, [ { width: 1920, height: 1080, fps: 60 }, + { width: 1280, height: 800, fps: 90 }, ]); }); diff --git a/opennow-stable/src/main/gfn/subscription.ts b/opennow-stable/src/main/platforms/gfn/subscription.ts similarity index 100% rename from opennow-stable/src/main/gfn/subscription.ts rename to opennow-stable/src/main/platforms/gfn/subscription.ts diff --git a/opennow-stable/src/main/gfn/types.ts b/opennow-stable/src/main/platforms/gfn/types.ts similarity index 100% rename from opennow-stable/src/main/gfn/types.ts rename to opennow-stable/src/main/platforms/gfn/types.ts diff --git a/opennow-stable/src/main/platforms/index.ts b/opennow-stable/src/main/platforms/index.ts new file mode 100644 index 000000000..c93992e73 --- /dev/null +++ b/opennow-stable/src/main/platforms/index.ts @@ -0,0 +1,11 @@ +/** + * Main-process cloud platform registry. + * + * Today only GeForce NOW is wired. New providers should live under + * `platforms//` and be registered here so IPC/session code can resolve + * the active adapter without hard-coding GFN imports throughout the app. + */ + +export { DEFAULT_CLOUD_PLATFORM_ID, type CloudPlatformId } from "@shared/platforms"; + +export * as gfn from "./gfn"; diff --git a/opennow-stable/src/main/services/refreshScheduler.ts b/opennow-stable/src/main/services/refreshScheduler.ts index b51a28e3b..0a4f8bc7b 100644 --- a/opennow-stable/src/main/services/refreshScheduler.ts +++ b/opennow-stable/src/main/services/refreshScheduler.ts @@ -1,212 +1,212 @@ -import type { GameInfo } from "@shared/gfn"; -import { getAccountGamesCacheKeys } from "../gfn/games"; -import { sessionProxyHasCredentials } from "../gfn/proxyUrl"; -import { cacheEventBus } from "./cacheEventBus"; -import { cacheManager } from "./cacheManager"; - -export interface RefreshAuthContext { - token: string; - userId: string; - providerStreamingBaseUrl?: string; - proxyUrl?: string; -} - -type FetchFunction = ( - token: string, - providerStreamingBaseUrl?: string, - proxyUrl?: string, -) => Promise; -type PublicFetchFunction = (proxyUrl?: string) => Promise; - -class RefreshScheduler { - private refreshTimer: ReturnType | null = null; - private isRefreshing: boolean = false; - private authContext: RefreshAuthContext | null = null; - private fetchMainGamesUncached: FetchFunction | null = null; - private fetchLibraryGamesUncached: FetchFunction | null = null; - private fetchPublicGamesUncached: PublicFetchFunction | null = null; - private refreshIntervalMs: number = 12 * 60 * 60 * 1000; - - initialize( - fetchMainGamesUncached: FetchFunction, - fetchLibraryGamesUncached: FetchFunction, - fetchPublicGamesUncached: PublicFetchFunction, - ): void { - this.fetchMainGamesUncached = fetchMainGamesUncached; - this.fetchLibraryGamesUncached = fetchLibraryGamesUncached; - this.fetchPublicGamesUncached = fetchPublicGamesUncached; - console.log(`[CACHE] RefreshScheduler initialized (interval: ${this.refreshIntervalMs / 60000} minutes)`); - } - - updateAuthContext(token: string, userId: string, providerStreamingBaseUrl?: string, proxyUrl?: string): void { - this.authContext = { token, userId, providerStreamingBaseUrl, proxyUrl }; - console.log(`[CACHE] Auth context updated for refresh scheduler`); - } - - start(): void { - if (this.refreshTimer) { - console.warn(`[CACHE] RefreshScheduler already started`); - return; - } - - if (!this.fetchMainGamesUncached || !this.fetchLibraryGamesUncached || !this.fetchPublicGamesUncached) { - console.error(`[CACHE] Cannot start RefreshScheduler: fetch functions not initialized`); - return; - } - - console.log(`[CACHE] Starting RefreshScheduler`); - void this.performRefresh(); - this.refreshTimer = setInterval(() => { - void this.performRefresh(); - }, this.refreshIntervalMs); - this.refreshTimer.unref?.(); - } - - stop(): void { - if (!this.refreshTimer) { - console.log(`[CACHE] RefreshScheduler already stopped`); - return; - } - - clearInterval(this.refreshTimer); - this.refreshTimer = null; - console.log(`[CACHE] RefreshScheduler stopped`); - } - - async performRefresh(options: { force?: boolean } = {}): Promise { - if (this.isRefreshing) { - console.log(`[CACHE] Refresh already in progress, skipping`); - return; - } - - if (!this.authContext) { - console.log(`[CACHE] Auth context not available, skipping refresh`); - return; - } - - if (!this.fetchMainGamesUncached || !this.fetchLibraryGamesUncached || !this.fetchPublicGamesUncached) { - console.error(`[CACHE] Fetch functions not available`); - return; - } - - const { token, userId, providerStreamingBaseUrl, proxyUrl } = this.authContext; - if (sessionProxyHasCredentials(proxyUrl)) { - console.log("[CACHE] Credentialed proxy configured, skipping background game cache refresh"); - return; - } - - const cacheKeys = getAccountGamesCacheKeys(userId, providerStreamingBaseUrl, proxyUrl); - const force = options.force === true; - - const [mainNeedsRefresh, libraryNeedsRefresh, publicNeedsRefresh] = force - ? [true, true, true] - : await Promise.all([ - cacheManager.isStaleOrMissing(cacheKeys.main), - cacheManager.isStaleOrMissing(cacheKeys.library), - cacheManager.isStaleOrMissing(cacheKeys.public), - ]); - - if (!mainNeedsRefresh && !libraryNeedsRefresh && !publicNeedsRefresh) { - console.log("[CACHE] All game caches are fresh, skipping background refresh"); - return; - } - - this.isRefreshing = true; - const startTime = Date.now(); - console.log("[CACHE] Refresh cycle started", { - main: mainNeedsRefresh, - library: libraryNeedsRefresh, - public: publicNeedsRefresh, - force, - }); - - try { - cacheEventBus.emit("cache:refresh-start"); - - const refreshTasks: Promise[] = []; - - if (mainNeedsRefresh) { - refreshTasks.push( - this.fetchMainGamesUncached(token, providerStreamingBaseUrl, proxyUrl) - .then(async (games) => { - await cacheManager.saveToCache(cacheKeys.main, games); - }), - ); - } - - if (libraryNeedsRefresh) { - refreshTasks.push( - this.fetchLibraryGamesUncached(token, providerStreamingBaseUrl, proxyUrl) - .then(async (games) => { - await cacheManager.saveToCache(cacheKeys.library, games); - }), - ); - } - - if (publicNeedsRefresh) { - refreshTasks.push( - this.fetchPublicGamesUncached(proxyUrl) - .then(async (games) => { - await cacheManager.saveToCache(cacheKeys.public, games); - }), - ); - } - - const results = await Promise.allSettled(refreshTasks); - - let hasErrors = false; - const taskNames: string[] = []; - if (mainNeedsRefresh) taskNames.push("main"); - if (libraryNeedsRefresh) taskNames.push("library"); - if (publicNeedsRefresh) taskNames.push("public"); - - for (let i = 0; i < results.length; i += 1) { - const result = results[i]; - if (result.status === "rejected") { - hasErrors = true; - const name = taskNames[i] ?? "unknown"; - console.error(`[CACHE] Refresh failed for ${name} games:`, result.reason); - cacheEventBus.emit("cache:refresh-error", { - key: `games:${name}`, - error: result.reason instanceof Error ? result.reason.message : String(result.reason), - }); - } - } - - const duration = Date.now() - startTime; - console.log(`[CACHE] Refresh cycle completed in ${duration}ms`); - - if (!hasErrors) { - cacheEventBus.emit("cache:refresh-success"); - } - } catch (error) { - console.error(`[CACHE] Refresh cycle error:`, error); - cacheEventBus.emit("cache:refresh-error", { - key: "refresh-cycle", - error: error instanceof Error ? error.message : "Unknown error", - }); - } finally { - this.isRefreshing = false; - } - } - - async manualRefresh(): Promise { - console.log(`[CACHE] Manual refresh requested`); - await this.performRefresh({ force: true }); - } - - setRefreshInterval(intervalMs: number): void { - console.log(`[CACHE] Refresh interval updated: ${this.refreshIntervalMs}ms -> ${intervalMs}ms`); - this.refreshIntervalMs = intervalMs; - - if (this.refreshTimer) { - clearInterval(this.refreshTimer); - this.refreshTimer = setInterval(() => { - void this.performRefresh(); - }, this.refreshIntervalMs); - this.refreshTimer.unref?.(); - } - } -} - -export const refreshScheduler = new RefreshScheduler(); +import type { GameInfo } from "@shared/gfn"; +import { getAccountGamesCacheKeys } from "../platforms/gfn/games"; +import { sessionProxyHasCredentials } from "../platforms/gfn/proxyUrl"; +import { cacheEventBus } from "./cacheEventBus"; +import { cacheManager } from "./cacheManager"; + +export interface RefreshAuthContext { + token: string; + userId: string; + providerStreamingBaseUrl?: string; + proxyUrl?: string; +} + +type FetchFunction = ( + token: string, + providerStreamingBaseUrl?: string, + proxyUrl?: string, +) => Promise; +type PublicFetchFunction = (proxyUrl?: string) => Promise; + +class RefreshScheduler { + private refreshTimer: ReturnType | null = null; + private isRefreshing: boolean = false; + private authContext: RefreshAuthContext | null = null; + private fetchMainGamesUncached: FetchFunction | null = null; + private fetchLibraryGamesUncached: FetchFunction | null = null; + private fetchPublicGamesUncached: PublicFetchFunction | null = null; + private refreshIntervalMs: number = 12 * 60 * 60 * 1000; + + initialize( + fetchMainGamesUncached: FetchFunction, + fetchLibraryGamesUncached: FetchFunction, + fetchPublicGamesUncached: PublicFetchFunction, + ): void { + this.fetchMainGamesUncached = fetchMainGamesUncached; + this.fetchLibraryGamesUncached = fetchLibraryGamesUncached; + this.fetchPublicGamesUncached = fetchPublicGamesUncached; + console.log(`[CACHE] RefreshScheduler initialized (interval: ${this.refreshIntervalMs / 60000} minutes)`); + } + + updateAuthContext(token: string, userId: string, providerStreamingBaseUrl?: string, proxyUrl?: string): void { + this.authContext = { token, userId, providerStreamingBaseUrl, proxyUrl }; + console.log(`[CACHE] Auth context updated for refresh scheduler`); + } + + start(): void { + if (this.refreshTimer) { + console.warn(`[CACHE] RefreshScheduler already started`); + return; + } + + if (!this.fetchMainGamesUncached || !this.fetchLibraryGamesUncached || !this.fetchPublicGamesUncached) { + console.error(`[CACHE] Cannot start RefreshScheduler: fetch functions not initialized`); + return; + } + + console.log(`[CACHE] Starting RefreshScheduler`); + void this.performRefresh(); + this.refreshTimer = setInterval(() => { + void this.performRefresh(); + }, this.refreshIntervalMs); + this.refreshTimer.unref?.(); + } + + stop(): void { + if (!this.refreshTimer) { + console.log(`[CACHE] RefreshScheduler already stopped`); + return; + } + + clearInterval(this.refreshTimer); + this.refreshTimer = null; + console.log(`[CACHE] RefreshScheduler stopped`); + } + + async performRefresh(options: { force?: boolean } = {}): Promise { + if (this.isRefreshing) { + console.log(`[CACHE] Refresh already in progress, skipping`); + return; + } + + if (!this.authContext) { + console.log(`[CACHE] Auth context not available, skipping refresh`); + return; + } + + if (!this.fetchMainGamesUncached || !this.fetchLibraryGamesUncached || !this.fetchPublicGamesUncached) { + console.error(`[CACHE] Fetch functions not available`); + return; + } + + const { token, userId, providerStreamingBaseUrl, proxyUrl } = this.authContext; + if (sessionProxyHasCredentials(proxyUrl)) { + console.log("[CACHE] Credentialed proxy configured, skipping background game cache refresh"); + return; + } + + const cacheKeys = getAccountGamesCacheKeys(userId, providerStreamingBaseUrl, proxyUrl); + const force = options.force === true; + + const [mainNeedsRefresh, libraryNeedsRefresh, publicNeedsRefresh] = force + ? [true, true, true] + : await Promise.all([ + cacheManager.isStaleOrMissing(cacheKeys.main), + cacheManager.isStaleOrMissing(cacheKeys.library), + cacheManager.isStaleOrMissing(cacheKeys.public), + ]); + + if (!mainNeedsRefresh && !libraryNeedsRefresh && !publicNeedsRefresh) { + console.log("[CACHE] All game caches are fresh, skipping background refresh"); + return; + } + + this.isRefreshing = true; + const startTime = Date.now(); + console.log("[CACHE] Refresh cycle started", { + main: mainNeedsRefresh, + library: libraryNeedsRefresh, + public: publicNeedsRefresh, + force, + }); + + try { + cacheEventBus.emit("cache:refresh-start"); + + const refreshTasks: Promise[] = []; + + if (mainNeedsRefresh) { + refreshTasks.push( + this.fetchMainGamesUncached(token, providerStreamingBaseUrl, proxyUrl) + .then(async (games) => { + await cacheManager.saveToCache(cacheKeys.main, games); + }), + ); + } + + if (libraryNeedsRefresh) { + refreshTasks.push( + this.fetchLibraryGamesUncached(token, providerStreamingBaseUrl, proxyUrl) + .then(async (games) => { + await cacheManager.saveToCache(cacheKeys.library, games); + }), + ); + } + + if (publicNeedsRefresh) { + refreshTasks.push( + this.fetchPublicGamesUncached(proxyUrl) + .then(async (games) => { + await cacheManager.saveToCache(cacheKeys.public, games); + }), + ); + } + + const results = await Promise.allSettled(refreshTasks); + + let hasErrors = false; + const taskNames: string[] = []; + if (mainNeedsRefresh) taskNames.push("main"); + if (libraryNeedsRefresh) taskNames.push("library"); + if (publicNeedsRefresh) taskNames.push("public"); + + for (let i = 0; i < results.length; i += 1) { + const result = results[i]; + if (result.status === "rejected") { + hasErrors = true; + const name = taskNames[i] ?? "unknown"; + console.error(`[CACHE] Refresh failed for ${name} games:`, result.reason); + cacheEventBus.emit("cache:refresh-error", { + key: `games:${name}`, + error: result.reason instanceof Error ? result.reason.message : String(result.reason), + }); + } + } + + const duration = Date.now() - startTime; + console.log(`[CACHE] Refresh cycle completed in ${duration}ms`); + + if (!hasErrors) { + cacheEventBus.emit("cache:refresh-success"); + } + } catch (error) { + console.error(`[CACHE] Refresh cycle error:`, error); + cacheEventBus.emit("cache:refresh-error", { + key: "refresh-cycle", + error: error instanceof Error ? error.message : "Unknown error", + }); + } finally { + this.isRefreshing = false; + } + } + + async manualRefresh(): Promise { + console.log(`[CACHE] Manual refresh requested`); + await this.performRefresh({ force: true }); + } + + setRefreshInterval(intervalMs: number): void { + console.log(`[CACHE] Refresh interval updated: ${this.refreshIntervalMs}ms -> ${intervalMs}ms`); + this.refreshIntervalMs = intervalMs; + + if (this.refreshTimer) { + clearInterval(this.refreshTimer); + this.refreshTimer = setInterval(() => { + void this.performRefresh(); + }, this.refreshIntervalMs); + this.refreshTimer.unref?.(); + } + } +} + +export const refreshScheduler = new RefreshScheduler(); diff --git a/opennow-stable/src/main/session/sessionConflict.ts b/opennow-stable/src/main/session/sessionConflict.ts index 842a803d2..292098547 100644 --- a/opennow-stable/src/main/session/sessionConflict.ts +++ b/opennow-stable/src/main/session/sessionConflict.ts @@ -2,7 +2,7 @@ import type { BrowserWindow } from "electron"; import { serializeSessionErrorTransport } from "@shared/sessionError"; import type { SessionConflictChoice } from "@shared/gfn"; import { enrichErrorForIpc } from "@shared/networkError"; -import { isSessionError, SessionError } from "../gfn/errorCodes"; +import { isSessionError, SessionError } from "../platforms/gfn/errorCodes"; export interface SessionConflictDialogDeps { dialog: Electron.Dialog; diff --git a/opennow-stable/src/main/session/sessionLifecycle.ts b/opennow-stable/src/main/session/sessionLifecycle.ts index f1a7e8785..4474e9a3c 100644 --- a/opennow-stable/src/main/session/sessionLifecycle.ts +++ b/opennow-stable/src/main/session/sessionLifecycle.ts @@ -1,4 +1,4 @@ -import { stopSession, getActiveSessions } from "../gfn/cloudmatch"; +import { stopSession, getActiveSessions } from "../platforms/gfn/cloudmatch"; import { isActiveCreateSessionConflict } from "./sessionSelection"; export async function stopActiveSessionsForCreate(params: { diff --git a/opennow-stable/src/main/settings.ts b/opennow-stable/src/main/settings.ts index 9cc4b8537..6279c0239 100644 --- a/opennow-stable/src/main/settings.ts +++ b/opennow-stable/src/main/settings.ts @@ -2,171 +2,43 @@ import { app } from "electron"; import { join } from "node:path"; import { readFileSync, writeFileSync, existsSync, mkdirSync } from "node:fs"; import type { - VideoCodec, - ColorQuality, - VideoAccelerationPreference, - MicrophoneMode, - GameLanguage, - AspectRatio, - KeyboardLayout, - StreamClientMode, - NativeStreamerBackendPreference, NativeVideoBackendPreference, - NativeStreamerFeatureMode, - NativeTransitionDiagnostics, AppAccentColor, AppTheme, - VideoShaderSettings, + ErrorReportingConsent, + Settings, } from "@shared/gfn"; import { - DEFAULT_KEYBOARD_LAYOUT, - DEFAULT_VIDEO_SHADER_SETTINGS, - getDefaultStreamPreferences, + createDefaultSettings, + createPlatformShortcutDefaults, + SHORTCUT_SETTING_KEYS, + normalizeNativeExternalRendererForPlatform, normalizeStreamClientModeForPlatform, normalizeStreamPreferences, + normalizeTransportModeForPlatform, normalizeVideoShaderSettings, + normalizeUpdateChannel, } from "@shared/gfn"; -export interface Settings { - /** Video resolution (e.g., "1920x1080") */ - resolution: string; - /** Aspect ratio (16:9, 16:10, 21:9, 32:9) */ - aspectRatio: AspectRatio; - /** Game poster size multiplier used by the renderer */ - posterSizeScale: number; - /** Target FPS (30, 60, 120, etc.) */ - fps: number; - /** Maximum bitrate in Mbps (cap at 150) */ - maxBitrateMbps: number; - /** Recording video bitrate in Mbps (null = MediaRecorder auto, cap at 200) */ - recordingBitrateMbps: number | null; - /** Stream client implementation to use for new sessions */ - streamClientMode: StreamClientMode; - /** Native streamer backend preference for new native sessions */ - nativeStreamerBackend: NativeStreamerBackendPreference; - /** Native GStreamer video backend preference for Windows DirectX paths */ - nativeVideoBackend: NativeVideoBackendPreference; - /** Optional path to a custom native streamer executable */ - nativeStreamerExecutablePath: string; - /** Native-only override for Cloud G-Sync / VRR display detection */ - nativeCloudGsyncMode: NativeStreamerFeatureMode; - /** Native D3D sink fullscreen presentation override */ - nativeD3dFullscreenMode: NativeStreamerFeatureMode; - /** Use the native GStreamer renderer window instead of Electron HWND embedding */ - nativeExternalRenderer: boolean; - /** Show the native streamer's own stats overlay while native streaming */ - showNativeStreamerStats: boolean; - /** Preferred video codec */ - codec: VideoCodec; - /** Preferred video decode acceleration mode */ - decoderPreference: VideoAccelerationPreference; - /** Preferred video encode acceleration mode */ - encoderPreference: VideoAccelerationPreference; - /** Color quality (bit depth + chroma subsampling) */ - colorQuality: ColorQuality; - /** Preferred region URL (empty = auto) */ - region: string; - /** Enable the optional proxy for Nvidia games catalog, session creation, and queue polling */ - sessionProxyEnabled: boolean; - /** Optional proxy used for Nvidia games catalog, session creation, and queue polling */ - sessionProxyUrl: string; - /** Enable clipboard paste into stream */ - clipboardPaste: boolean; - /** Enable experimental gyroscope controller input mapping */ - enableGyroscopeControls: boolean; - /** macOS-only workaround that restores Chromium's older HID path for Steam Controller compatibility */ - steamControllerCompatibilityMode: boolean; - /** Use the WebRTC cursor_channel overlay instead of leaving cursor rendering to the stream */ - nativeCursorOverlay: boolean; - /** Mouse sensitivity multiplier */ - mouseSensitivity: number; - /** Software mouse acceleration strength percentage (1-150) */ - mouseAcceleration: number; - /** Toggle stats overlay shortcut */ - shortcutToggleStats: string; - /** Toggle pointer lock shortcut */ - shortcutTogglePointerLock: string; - /** Toggle fullscreen shortcut */ - shortcutToggleFullscreen: string; - /** Stop stream shortcut */ - shortcutStopStream: string; - /** Toggle anti-AFK shortcut */ - shortcutToggleAntiAfk: string; - /** Toggle microphone shortcut */ - shortcutToggleMicrophone: string; - /** Take screenshot shortcut */ - shortcutScreenshot: string; - /** Toggle stream recording shortcut */ - shortcutToggleRecording: string; - /** How often to re-show the session timer while streaming (0 = off) */ - sessionClockShowEveryMinutes: number; - /** How long the session timer stays visible when it appears */ - sessionClockShowDurationSeconds: number; - /** Microphone mode: disabled, push-to-talk, or voice-activity */ - microphoneMode: MicrophoneMode; - /** Preferred microphone device ID (empty = default) */ - microphoneDeviceId: string; - /** Hide stream buttons (mic/fullscreen/end-session) while streaming */ - hideStreamButtons: boolean; - /** Show the Anti-AFK indicator badge while streaming */ - showAntiAfkIndicator: boolean; - /** Show the stats overlay automatically when a stream launches */ - showStatsOnLaunch: boolean; - /** Skip the free-tier queue server selection modal and launch with default routing */ - hideServerSelector: boolean; - /** Desktop UI accent preset */ - appAccentColor: AppAccentColor; - /** UI Theme */ - appTheme: AppTheme; - /** Use translucent overlays for settings and navbars */ - translucentUI: boolean; - /** Use the large-screen controller-oriented shell and library layout */ - controllerMode: boolean; - /** Launch fullscreen with Controller Mode enabled, like GeForce NOW's TV mode */ - launchInConsoleMode: boolean; - /** Automatically enter fullscreen when launching a stream */ - autoFullScreen: boolean; - favoriteGameIds: string[]; - /** Enable the live elapsed session counter */ - sessionCounterEnabled: boolean; - /** Also show the session-limit countdown in the stats overlay while streaming */ - showSessionTimeRemainingInStatsOverlay: boolean; - /** Window width */ - windowWidth: number; - /** Window height */ - windowHeight: number; - /** Keyboard layout for mapping physical keys inside the remote session */ - keyboardLayout: KeyboardLayout; - /** In-game language setting (sent to GFN servers via languageCode parameter) */ - gameLanguage: GameLanguage; - /** User opt-in for NVIDIA's per-game in-game graphics/settings persistence */ - enablePersistingInGameSettings: boolean; - /** Experimental request for Low Latency, Low Loss, Scalable throughput on new sessions */ - enableL4S: boolean; - /** Request Cloud G-Sync / Variable Refresh Rate on new sessions */ - enableCloudGsync: boolean; - /** Hidden diagnostics for native transition recovery and 240 FPS server-side stream changes */ - nativeTransitionDiagnostics?: NativeTransitionDiagnostics; - /** Show the currently streaming game as Discord Rich Presence activity */ - discordRichPresence: boolean; - /** Automatically check GitHub Releases for app updates in the background */ - autoCheckForUpdates: boolean; - /** When true, pressing Escape will exit fullscreen; when false Escape is sent to the game while pointer-locked */ - allowEscapeToExitFullscreen?: boolean; - /** Last version for which the release highlights modal was acknowledged (empty = never) */ - lastSeenReleaseHighlightsVersion: string; - /** Client-side GPU post-processing shaders applied to the stream (web client mode) */ - videoShader: VideoShaderSettings; -} +export type { Settings } from "@shared/gfn"; -const defaultStopShortcut = "Ctrl+Shift+Q"; -const defaultAntiAfkShortcut = "Ctrl+Shift+K"; -const defaultMicShortcut = "Ctrl+Shift+M"; +const DEFAULT_SHORTCUTS = createPlatformShortcutDefaults(process.platform).bindings; +const defaultStopShortcut = DEFAULT_SHORTCUTS.shortcutStopStream; +const defaultAntiAfkShortcut = DEFAULT_SHORTCUTS.shortcutToggleAntiAfk; +const defaultMicShortcut = DEFAULT_SHORTCUTS.shortcutToggleMicrophone; const LEGACY_STOP_SHORTCUTS = new Set(["META+SHIFT+Q", "CMD+SHIFT+Q"]); const LEGACY_ANTI_AFK_SHORTCUTS = new Set(["META+SHIFT+F10", "CMD+SHIFT+F10", "CTRL+SHIFT+F10"]); -const DEFAULT_STREAM_PREFERENCES = getDefaultStreamPreferences(); -const NATIVE_VIDEO_BACKEND_PREFERENCES = new Set(["auto", "d3d11", "d3d12"]); +const NATIVE_VIDEO_BACKEND_PREFERENCES = new Set([ + "auto", + "d3d11", + "d3d12", + "nvdec", + "vaapi", + "v4l2", + "vulkan", + "software", +]); const APP_ACCENT_COLORS = new Set(["green", "blue", "violet", "amber", "rose"]); const APP_THEMES = new Set(["light", "dark", "auto"]); @@ -195,84 +67,18 @@ function normalizeRecordingBitrateMbps(raw: unknown): number | null { return Math.max(1, Math.min(200, Math.round(value))); } -const DEFAULT_SETTINGS: Settings = { - resolution: "1920x1080", - aspectRatio: "16:9", - posterSizeScale: 1, - fps: 60, - maxBitrateMbps: 75, - recordingBitrateMbps: null, - streamClientMode: "web", - nativeStreamerBackend: "gstreamer", - nativeVideoBackend: "auto", - nativeStreamerExecutablePath: "", - nativeCloudGsyncMode: "auto", - nativeD3dFullscreenMode: "auto", - nativeExternalRenderer: true, - showNativeStreamerStats: false, - codec: DEFAULT_STREAM_PREFERENCES.codec, - decoderPreference: "auto", - encoderPreference: "auto", - colorQuality: DEFAULT_STREAM_PREFERENCES.colorQuality, - region: "", - sessionProxyEnabled: false, - sessionProxyUrl: "", - clipboardPaste: false, - enableGyroscopeControls: false, - steamControllerCompatibilityMode: false, - nativeCursorOverlay: true, - mouseSensitivity: 1, - mouseAcceleration: 1, - shortcutToggleStats: "F3", - shortcutTogglePointerLock: "F8", - shortcutToggleFullscreen: "F10", - shortcutStopStream: defaultStopShortcut, - shortcutToggleAntiAfk: defaultAntiAfkShortcut, - shortcutToggleMicrophone: defaultMicShortcut, - shortcutScreenshot: "F11", - shortcutToggleRecording: "F12", - microphoneMode: "disabled", - microphoneDeviceId: "", - hideStreamButtons: false, - showAntiAfkIndicator: true, - showStatsOnLaunch: false, - hideServerSelector: false, - appAccentColor: "green", - appTheme: "auto", - translucentUI: false, - controllerMode: false, - launchInConsoleMode: false, - autoFullScreen: false, - favoriteGameIds: [], - sessionCounterEnabled: false, - showSessionTimeRemainingInStatsOverlay: false, - sessionClockShowEveryMinutes: 60, - sessionClockShowDurationSeconds: 30, - windowWidth: 1400, - windowHeight: 900, - keyboardLayout: DEFAULT_KEYBOARD_LAYOUT, - gameLanguage: "en_US", - enablePersistingInGameSettings: false, - enableL4S: false, - enableCloudGsync: false, - nativeTransitionDiagnostics: undefined, - discordRichPresence: false, - autoCheckForUpdates: true, - allowEscapeToExitFullscreen: false, - lastSeenReleaseHighlightsVersion: "", - videoShader: { ...DEFAULT_VIDEO_SHADER_SETTINGS }, -}; -const SHORTCUT_SETTING_KEYS = [ - "shortcutToggleStats", - "shortcutTogglePointerLock", - "shortcutToggleFullscreen", - "shortcutStopStream", - "shortcutToggleAntiAfk", - "shortcutToggleMicrophone", - "shortcutScreenshot", - "shortcutToggleRecording", -] as const satisfies readonly (keyof Settings)[]; +const ERROR_REPORTING_CONSENTS = new Set(["unset", "granted", "denied"]); + +function normalizeErrorReportingConsent(raw: unknown): ErrorReportingConsent { + return ERROR_REPORTING_CONSENTS.has(raw as ErrorReportingConsent) + ? (raw as ErrorReportingConsent) + : "unset"; +} + +function normalizeTelemetryInstallId(raw: unknown): string { + return typeof raw === "string" ? raw.trim() : ""; +} type ShortcutSettingKey = typeof SHORTCUT_SETTING_KEYS[number]; @@ -332,7 +138,7 @@ export class SettingsManager { private load(): Settings { try { if (!existsSync(this.settingsPath)) { - const defaults = { ...DEFAULT_SETTINGS }; + const defaults = createDefaultSettings(process.platform); this.enforceCompatibility(defaults); return defaults; } @@ -349,7 +155,7 @@ export class SettingsManager { // Merge with defaults to ensure all fields exist const merged: Settings = { - ...DEFAULT_SETTINGS, + ...createDefaultSettings(process.platform), ...parsedSettings, }; @@ -393,7 +199,7 @@ export class SettingsManager { return merged; } catch (error) { console.error("Failed to load settings, using defaults:", error); - const defaults = { ...DEFAULT_SETTINGS }; + const defaults = createDefaultSettings(process.platform); this.enforceCompatibility(defaults); return defaults; } @@ -431,12 +237,34 @@ export class SettingsManager { settings.appTheme = appTheme; migrated = true; } + const updateChannel = normalizeUpdateChannel(settings.updateChannel); + if (settings.updateChannel !== updateChannel) { + settings.updateChannel = updateChannel; + migrated = true; + } if (typeof settings.translucentUI !== "boolean") { settings.translucentUI = false; migrated = true; } - if (!settings.nativeExternalRenderer) { - settings.nativeExternalRenderer = true; + if (typeof settings.nativeExternalRenderer !== "boolean") { + settings.nativeExternalRenderer = false; + migrated = true; + } + const nativeExternalRenderer = normalizeNativeExternalRendererForPlatform( + settings.nativeExternalRenderer, + process.platform, + ); + if (settings.nativeExternalRenderer !== nativeExternalRenderer) { + settings.nativeExternalRenderer = nativeExternalRenderer; + migrated = true; + } + const transportMode = normalizeTransportModeForPlatform( + settings.transportMode === "nvst" ? "nvst" : "webrtc", + process.platform, + settings.streamClientMode, + ); + if (settings.transportMode !== transportMode) { + settings.transportMode = transportMode; migrated = true; } const nativeVideoBackend = normalizeNativeVideoBackendPreference(settings.nativeVideoBackend); @@ -455,6 +283,10 @@ export class SettingsManager { settings.steamControllerCompatibilityMode = false; migrated = true; } + if (typeof settings.identifyAsSteamDeck !== "boolean") { + settings.identifyAsSteamDeck = false; + migrated = true; + } const videoShader = normalizeVideoShaderSettings(settings.videoShader); if (JSON.stringify(settings.videoShader) !== JSON.stringify(videoShader)) { @@ -462,6 +294,18 @@ export class SettingsManager { migrated = true; } + const consentBefore = settings.errorReportingConsent; + settings.errorReportingConsent = normalizeErrorReportingConsent(settings.errorReportingConsent); + if (settings.errorReportingConsent !== consentBefore) { + migrated = true; + } + + const installIdBefore = settings.telemetryInstallId; + settings.telemetryInstallId = normalizeTelemetryInstallId(settings.telemetryInstallId); + if (settings.telemetryInstallId !== installIdBefore) { + migrated = true; + } + return migrated; } @@ -489,7 +333,7 @@ export class SettingsManager { const fallback = SIDEBAR_RESERVED_SHORTCUT_FALLBACKS[key].find((candidate) => isShortcutAvailable(settings, key, candidate), - ) ?? DEFAULT_SETTINGS[key]; + ) ?? DEFAULT_SHORTCUTS[key]; settings[key] = fallback; migrated = true; } @@ -552,7 +396,7 @@ export class SettingsManager { * Reset all settings to defaults */ reset(): Settings { - this.settings = { ...DEFAULT_SETTINGS }; + this.settings = createDefaultSettings(process.platform); this.enforceCompatibility(this.settings); this.save(); return { ...this.settings }; @@ -562,7 +406,7 @@ export class SettingsManager { * Get the default settings */ getDefaults(): Settings { - const defaults = { ...DEFAULT_SETTINGS }; + const defaults = createDefaultSettings(process.platform); this.enforceCompatibility(defaults); return defaults; } @@ -577,5 +421,3 @@ export function getSettingsManager(): SettingsManager { } return settingsManager; } - -export { DEFAULT_SETTINGS }; diff --git a/opennow-stable/src/main/signaling/signalingCoordinator.ts b/opennow-stable/src/main/signaling/signalingCoordinator.ts index 5796ac78b..a88036398 100644 --- a/opennow-stable/src/main/signaling/signalingCoordinator.ts +++ b/opennow-stable/src/main/signaling/signalingCoordinator.ts @@ -13,7 +13,7 @@ import type { Settings, SignalingConnectRequest, } from "@shared/gfn"; -import { GfnSignalingClient } from "../gfn/signaling"; +import { GfnSignalingClient } from "../platforms/gfn/signaling"; import { NativeStreamerManager } from "../nativeStreamer/manager"; import { normalizeNativeInputPacket } from "../nativeStreamer/input"; import { normalizeNativeRenderSurface } from "../nativeStreamer/surface"; @@ -227,7 +227,8 @@ export class SignalingCoordinator { key === "nativeStreamerExecutablePath" || key === "nativeCloudGsyncMode" || key === "nativeD3dFullscreenMode" || - key === "nativeExternalRenderer" + key === "nativeExternalRenderer" || + key === "transportMode" ) { this.stopNativeStreamer( key === "nativeStreamerBackend" @@ -240,7 +241,9 @@ export class SignalingCoordinator { ? "native D3D fullscreen mode changed" : key === "nativeExternalRenderer" ? "native external renderer setting changed" - : "native streamer disabled", + : key === "transportMode" + ? "native transport mode changed" + : "native streamer disabled", ); this.resetNativeStreamerContext(); } @@ -270,6 +273,8 @@ export class SignalingCoordinator { resolution: this.nativeStreamerContext.settings.resolution, fps: this.nativeStreamerContext.settings.fps, codec: this.nativeStreamerContext.settings.codec, + transportMode: this.nativeStreamerContext.settings.transportMode ?? "webrtc", + rtspsEndpoints: this.nativeStreamerContext.session.rtspsEndpoints ?? [], negotiatedStreamProfile: this.nativeStreamerContext.session.negotiatedStreamProfile, requestedStreamingFeatures: @@ -340,7 +345,8 @@ export class SignalingCoordinator { this.deps.settingsManager?.get("nativeCloudGsyncMode") ?? "auto", getD3dFullscreenMode: () => this.deps.settingsManager?.get("nativeD3dFullscreenMode") ?? "auto", - getExternalRendererEnabled: () => true, + getExternalRendererEnabled: () => + this.deps.settingsManager?.get("nativeExternalRenderer") ?? false, emit: (event) => this.emitToRenderer(event), sendAnswer: async (payload) => { if (!this.signalingClient) { diff --git a/opennow-stable/src/main/telemetry/posthog.ts b/opennow-stable/src/main/telemetry/posthog.ts new file mode 100644 index 000000000..2aba713f5 --- /dev/null +++ b/opennow-stable/src/main/telemetry/posthog.ts @@ -0,0 +1,181 @@ +import { randomUUID } from "node:crypto"; +import { PostHog } from "posthog-node"; +import type { Settings } from "@shared/gfn"; +import { + APP_OPENED_EVENT_NAME, + isPostHogConfigured, + POSTHOG_HOST, + POSTHOG_PROJECT_TOKEN, +} from "@shared/telemetry"; +import { getAppBuildInfo } from "../appBuildInfo"; +import type { SettingsManager } from "../settings"; + +let client: PostHog | null = null; +let activeDistinctId: string | null = null; +let processHandlersInstalled = false; +/** One anonymous open per process lifetime (not per settings sync). */ +let appOpenedCapturedThisProcess = false; + +function buildCommonProperties(): Record { + const build = getAppBuildInfo(); + return { + app_version: build.version, + app_display_version: build.displayVersion, + app_build_number: build.buildNumber, + app_commit: build.commit, + platform: process.platform, + arch: process.arch, + electron_version: process.versions.electron, + }; +} + +function ensureInstallId(settingsManager: SettingsManager): string { + const existing = settingsManager.get("telemetryInstallId").trim(); + if (existing) { + return existing; + } + const generated = randomUUID(); + settingsManager.set("telemetryInstallId", generated); + return generated; +} + +function onUncaughtException(error: Error): void { + captureMainException(error, { + mechanism: "uncaughtException", + }); +} + +function onUnhandledRejection(reason: unknown): void { + captureMainException(reason, { + mechanism: "unhandledRejection", + }); +} + +function installProcessHandlers(): void { + if (processHandlersInstalled) { + return; + } + process.on("uncaughtException", onUncaughtException); + process.on("unhandledRejection", onUnhandledRejection); + processHandlersInstalled = true; +} + +function uninstallProcessHandlers(): void { + if (!processHandlersInstalled) { + return; + } + process.off("uncaughtException", onUncaughtException); + process.off("unhandledRejection", onUnhandledRejection); + processHandlersInstalled = false; +} + +function stopClient(): void { + uninstallProcessHandlers(); + if (!client) { + return; + } + const shuttingDown = client; + client = null; + activeDistinctId = null; + void shuttingDown.shutdown().catch((error) => { + console.warn("[Telemetry] Failed to shut down PostHog client:", error); + }); +} + +function startClient(settingsManager: SettingsManager): void { + if (!isPostHogConfigured()) { + console.warn("[Telemetry] PostHog project token is not configured; skipping main-process telemetry."); + return; + } + + const distinctId = ensureInstallId(settingsManager); + if (client && activeDistinctId === distinctId) { + installProcessHandlers(); + return; + } + + stopClient(); + + // Use manual process handlers instead of enableExceptionAutocapture so we + // keep a stable install distinct ID and avoid the SDK forcing process.exit. + client = new PostHog(POSTHOG_PROJECT_TOKEN, { + host: POSTHOG_HOST, + enableExceptionAutocapture: false, + }); + activeDistinctId = distinctId; + installProcessHandlers(); +} + +function captureAppOpened(): void { + if (!client || !activeDistinctId || appOpenedCapturedThisProcess) { + return; + } + client.capture({ + distinctId: activeDistinctId, + event: APP_OPENED_EVENT_NAME, + properties: { + ...buildCommonProperties(), + process: "main", + }, + }); + appOpenedCapturedThisProcess = true; +} + +/** + * Start or stop main-process PostHog based on persisted consent. + * Call after SettingsManager is ready and whenever consent changes. + * When consent is granted, records one anonymous {@link APP_OPENED_EVENT_NAME} per process. + */ +export function syncMainTelemetry(settingsManager: SettingsManager): void { + if (settingsManager.get("errorReportingConsent") === "granted") { + startClient(settingsManager); + captureAppOpened(); + return; + } + stopClient(); +} + +export function captureMainException( + error: unknown, + additionalProperties?: Record, +): void { + if (!client || !activeDistinctId) { + return; + } + + const normalized = error instanceof Error + ? error + : new Error(typeof error === "string" ? error : "Unknown main-process error"); + + client.captureException(normalized, activeDistinctId, { + ...buildCommonProperties(), + ...additionalProperties, + process: "main", + }); +} + +export function applyTelemetrySettingsChange( + settingsManager: SettingsManager, + key: keyof Settings, + _value: Settings[keyof Settings], +): void { + if (key !== "errorReportingConsent" && key !== "telemetryInstallId") { + return; + } + syncMainTelemetry(settingsManager); +} + +export async function shutdownMainTelemetry(): Promise { + uninstallProcessHandlers(); + if (!client) { + return; + } + const shuttingDown = client; + client = null; + activeDistinctId = null; + try { + await shuttingDown.shutdown(); + } catch (error) { + console.warn("[Telemetry] Failed to flush PostHog on shutdown:", error); + } +} diff --git a/opennow-stable/src/main/thanks/fetchThanksData.ts b/opennow-stable/src/main/thanks/fetchThanksData.ts new file mode 100644 index 000000000..9b929bcf4 --- /dev/null +++ b/opennow-stable/src/main/thanks/fetchThanksData.ts @@ -0,0 +1,274 @@ +import type { + ThankYouContributor, + ThankYouDataResult, + ThankYouSupporter, +} from "@shared/gfn"; +import { fetchWithTimeout, withTimeout } from "../services/requestTimeout"; + +const THANKS_CONTRIBUTORS_URL = + "https://api.github.com/repos/OpenCloudGaming/OpenNOW/contributors?per_page=100"; +const THANKS_SUPPORTERS_URL = "https://github.com/sponsors/zortos293"; +const THANKS_REQUEST_HEADERS = { + Accept: "application/vnd.github+json", + "User-Agent": "OpenNOW-DesktopClient", +} as const; +const THANKS_EXCLUDED_PATTERN = /(copilot|claude|cappy)/i; +const THANKS_FETCH_TIMEOUT_MS = 8000; +const THANKS_CUSTOM_SUPPORTERS: readonly ThankYouSupporter[] = [ + { + name: "DarkevilPT", + avatarUrl: "https://github.com/DarkevilPT.png?size=96", + profileUrl: "https://github.com/DarkevilPT", + isPrivate: false, + source: "custom", + }, +] as const; + +interface GitHubContributorResponse { + login?: string; + avatar_url?: string; + html_url?: string; + contributions?: number; + type?: string; + name?: string | null; +} + +export function decodeHtmlEntities(value: string): string { + return value + .replace(/&/g, "&") + .replace(/"/g, '"') + .replace(/'/g, "'") + .replace(/</g, "<") + .replace(/>/g, ">"); +} + +export function stripHtml(value: string): string { + return decodeHtmlEntities(value.replace(/<[^>]+>/g, " ")) + .replace(/\s+/g, " ") + .trim(); +} + +export function normalizeUrl(value: string | undefined): string | undefined { + if (!value) return undefined; + const decoded = decodeHtmlEntities(value.trim()); + if (!decoded) return undefined; + if (decoded.startsWith("//")) return `https:${decoded}`; + if (decoded.startsWith("/")) return `https://github.com${decoded}`; + return decoded; +} + +export function shouldExcludeContributor( + contributor: GitHubContributorResponse, +): boolean { + const login = contributor.login?.trim() ?? ""; + const name = contributor.name?.trim() ?? ""; + if (!login || !contributor.avatar_url || !contributor.html_url) return true; + if (contributor.type === "Bot") return true; + if (/\[bot\]$/i.test(login)) return true; + if (THANKS_EXCLUDED_PATTERN.test(login) || THANKS_EXCLUDED_PATTERN.test(name)) + return true; + return false; +} + +export async function fetchThanksContributors(): Promise { + const response = await fetchWithTimeout( + THANKS_CONTRIBUTORS_URL, + { headers: THANKS_REQUEST_HEADERS }, + THANKS_FETCH_TIMEOUT_MS, + "GitHub contributors request", + ); + if (!response.ok) { + throw new Error(`GitHub contributors request failed (${response.status})`); + } + + const payload = (await withTimeout( + response.json() as Promise, + THANKS_FETCH_TIMEOUT_MS, + "GitHub contributors response", + )) as GitHubContributorResponse[]; + if (!Array.isArray(payload)) { + throw new Error("GitHub contributors response was not an array"); + } + + const contributors = payload + .filter((contributor) => !shouldExcludeContributor(contributor)) + .map((contributor) => ({ + login: contributor.login!.trim(), + avatarUrl: contributor.avatar_url!, + profileUrl: contributor.html_url!, + contributions: + typeof contributor.contributions === "number" + ? contributor.contributions + : 0, + })) + .sort( + (a, b) => + b.contributions - a.contributions || a.login.localeCompare(b.login), + ); + return contributors; +} + +export function parseSupporterName(entryHtml: string): { + name: string; + isPrivate: boolean; +} { + const privateHrefMatch = entryHtml.match( + /href="https:\/\/docs\.github\.com\/sponsors\/sponsoring-open-source-contributors\/managing-your-sponsorship#managing-the-privacy-setting-for-your-sponsorship"/i, + ); + const privateTooltipMatch = entryHtml.match( + /]*>\s*Private Sponsor\s*<\/tool-tip>/i, + ); + const privateAriaMatch = entryHtml.match(/aria-label="Private Sponsor"/i); + if (privateHrefMatch || privateTooltipMatch || privateAriaMatch) { + return { name: "Private", isPrivate: true }; + } + + const altMatch = entryHtml.match(/]+alt="([^"]+)"/i); + const altText = altMatch ? stripHtml(altMatch[1]) : ""; + const normalizedAlt = altText.replace(/^@/, "").trim(); + if (normalizedAlt) { + return { name: normalizedAlt, isPrivate: false }; + } + + const ariaMatch = entryHtml.match(/aria-label="([^"]+)"/i); + const ariaText = ariaMatch ? stripHtml(ariaMatch[1]) : ""; + const normalizedAria = ariaText.replace(/^@/, "").trim(); + if (normalizedAria && !/private sponsor/i.test(normalizedAria)) { + return { name: normalizedAria, isPrivate: false }; + } + + const hrefMatch = entryHtml.match(/]+href="\/([^"/?#]+)"/i); + const normalizedHref = hrefMatch + ? decodeHtmlEntities(hrefMatch[1]).trim() + : ""; + if (normalizedHref && !/sponsors/i.test(normalizedHref)) { + return { name: normalizedHref.replace(/^@/, ""), isPrivate: false }; + } + + return { name: "Private", isPrivate: true }; +} + +export function parseSupportersFromHtml(html: string): ThankYouSupporter[] { + const sponsorsSectionMatch = html.match( + /
([\s\S]*?)<\/remote-pagination>/i, + ); + if (!sponsorsSectionMatch) { + return []; + } + + const listHtml = sponsorsSectionMatch[1]; + const entryMatches = + listHtml.match(/
]*>[\s\S]*?<\/div>/gi) ?? + []; + const supporters: ThankYouSupporter[] = []; + const seenKeys = new Set(); + + for (const entryHtml of entryMatches) { + const { name, isPrivate } = parseSupporterName(entryHtml); + const hrefMatch = entryHtml.match(/]+href="([^"]+)"/i); + const profileUrl = isPrivate ? undefined : normalizeUrl(hrefMatch?.[1]); + const avatarMatch = entryHtml.match(/]+src="([^"]+)"/i); + const avatarUrl = normalizeUrl(avatarMatch?.[1]); + const dedupeKey = `${name}|${profileUrl ?? ""}|${avatarUrl ?? ""}`; + if (seenKeys.has(dedupeKey)) continue; + seenKeys.add(dedupeKey); + supporters.push({ + name: name || "Private", + avatarUrl, + profileUrl, + isPrivate: isPrivate || !name, + source: isPrivate || !name ? "private" : "github", + }); + } + + return supporters; +} + +export function getSupporterDedupeKey(supporter: ThankYouSupporter): string { + const profileUrl = supporter.profileUrl?.trim().toLowerCase(); + if (profileUrl) return `profile:${profileUrl}`; + return `name:${supporter.name.trim().toLowerCase()}|private:${supporter.isPrivate}`; +} + +export function mergeThanksSupporters( + ...supporterGroups: readonly (readonly ThankYouSupporter[])[] +): ThankYouSupporter[] { + const supporters: ThankYouSupporter[] = []; + const seenKeys = new Set(); + + for (const group of supporterGroups) { + for (const supporter of group) { + const dedupeKey = getSupporterDedupeKey(supporter); + if (seenKeys.has(dedupeKey)) continue; + seenKeys.add(dedupeKey); + supporters.push({ ...supporter }); + } + } + + return supporters; +} + +export async function fetchThanksSupporters(): Promise { + const response = await fetchWithTimeout( + THANKS_SUPPORTERS_URL, + { + headers: { + ...THANKS_REQUEST_HEADERS, + Accept: "text/html,application/xhtml+xml", + }, + }, + THANKS_FETCH_TIMEOUT_MS, + "GitHub sponsors request", + ); + if (!response.ok) { + throw new Error(`GitHub sponsors page request failed (${response.status})`); + } + + const html = await withTimeout( + response.text(), + THANKS_FETCH_TIMEOUT_MS, + "GitHub sponsors response", + ); + const supporters = parseSupportersFromHtml(html); + return supporters; +} + +export async function fetchThanksData(): Promise { + const result: ThankYouDataResult = { + contributors: [], + supporters: [], + }; + + const [contributorsResult, supportersResult] = await Promise.allSettled([ + fetchThanksContributors(), + fetchThanksSupporters(), + ]); + + if (contributorsResult.status === "fulfilled") { + result.contributors = contributorsResult.value; + } else { + result.contributorsError = + contributorsResult.reason instanceof Error + ? contributorsResult.reason.message + : "Unable to load contributors right now."; + } + + if (supportersResult.status === "fulfilled") { + result.supporters = mergeThanksSupporters( + THANKS_CUSTOM_SUPPORTERS, + supportersResult.value, + ); + if (result.supporters.length === 0) { + result.supportersError = + "No public supporters were found on GitHub Sponsors."; + } + } else { + result.supporters = mergeThanksSupporters(THANKS_CUSTOM_SUPPORTERS); + result.supportersError = + supportersResult.reason instanceof Error + ? supportersResult.reason.message + : "Unable to load supporters right now."; + } + + return result; +} diff --git a/opennow-stable/src/main/updateChannel.test.ts b/opennow-stable/src/main/updateChannel.test.ts new file mode 100644 index 000000000..573ed752f --- /dev/null +++ b/opennow-stable/src/main/updateChannel.test.ts @@ -0,0 +1,45 @@ +import assert from "node:assert/strict"; +import test from "node:test"; + +import { normalizeUpdateChannel } from "@shared/gfn"; +import { applyUpdateChannel, getUpdateChannelPolicy } from "./updateChannel"; + +test("normalizes persisted update channels to the stable default", () => { + assert.equal(normalizeUpdateChannel("nightly"), "nightly"); + assert.equal(normalizeUpdateChannel("stable"), "stable"); + assert.equal(normalizeUpdateChannel("beta"), "stable"); + assert.equal(normalizeUpdateChannel(undefined), "stable"); +}); + +test("maps stable and nightly settings to explicit updater policies", () => { + assert.deepEqual(getUpdateChannelPolicy("stable"), { + feedChannel: "latest", + allowPrerelease: false, + }); + assert.deepEqual(getUpdateChannelPolicy("nightly"), { + feedChannel: "nightly", + allowPrerelease: true, + }); +}); + +test("applying a channel never enables update downgrades", () => { + const updater = { + channel: null as string | null, + allowPrerelease: false, + allowDowngrade: true, + }; + + applyUpdateChannel(updater, "nightly"); + assert.deepEqual(updater, { + channel: "nightly", + allowPrerelease: true, + allowDowngrade: false, + }); + + applyUpdateChannel(updater, "stable"); + assert.deepEqual(updater, { + channel: "latest", + allowPrerelease: false, + allowDowngrade: false, + }); +}); diff --git a/opennow-stable/src/main/updateChannel.ts b/opennow-stable/src/main/updateChannel.ts new file mode 100644 index 000000000..4a3420058 --- /dev/null +++ b/opennow-stable/src/main/updateChannel.ts @@ -0,0 +1,27 @@ +import type { UpdateChannel } from "@shared/gfn"; + +interface UpdaterChannelTarget { + channel: string | null; + allowPrerelease: boolean; + allowDowngrade: boolean; +} + +export interface UpdateChannelPolicy { + feedChannel: "latest" | "nightly"; + allowPrerelease: boolean; +} + +export function getUpdateChannelPolicy(channel: UpdateChannel): UpdateChannelPolicy { + return channel === "nightly" + ? { feedChannel: "nightly", allowPrerelease: true } + : { feedChannel: "latest", allowPrerelease: false }; +} + +export function applyUpdateChannel(target: UpdaterChannelTarget, channel: UpdateChannel): void { + const policy = getUpdateChannelPolicy(channel); + target.channel = policy.feedChannel; + target.allowPrerelease = policy.allowPrerelease; + // Changing electron-updater's channel enables downgrades. OpenNOW never needs + // that behavior: nightly versions target the next stable semver instead. + target.allowDowngrade = false; +} diff --git a/opennow-stable/src/main/updater.ts b/opennow-stable/src/main/updater.ts index bc6450e94..98959abb8 100644 --- a/opennow-stable/src/main/updater.ts +++ b/opennow-stable/src/main/updater.ts @@ -6,7 +6,8 @@ import { getAppBuildInfo } from "./appBuildInfo"; import { pickRuntimeGitHubToken } from "./githubRuntimeToken"; import { getLinuxUpdaterSupport } from "./linuxUpdaterSupport"; import { writeCacheEntry } from "./releaseHighlights"; -import type { AppUpdaterState } from "@shared/gfn"; +import { applyUpdateChannel } from "./updateChannel"; +import type { AppUpdaterState, UpdateChannel } from "@shared/gfn"; const { autoUpdater } = electronUpdater; @@ -18,6 +19,7 @@ export interface AppUpdaterController { dispose(): void; getState(): AppUpdaterState; setAutomaticChecksEnabled(enabled: boolean): AppUpdaterState; + setUpdateChannel(channel: UpdateChannel): AppUpdaterState; checkForUpdates(source?: "auto" | "manual"): Promise; downloadUpdate(): Promise; quitAndInstall(): Promise; @@ -26,14 +28,11 @@ export interface AppUpdaterController { interface AppUpdaterControllerOptions { onStateChanged: (state: AppUpdaterState) => void; automaticChecksEnabled: boolean; + updateChannel: UpdateChannel; onBeforeQuitAndInstall?: () => void; onQuitAndInstallError?: () => void; } -function isPrereleaseVersion(version: string): boolean { - return version.includes("-"); -} - function normalizeErrorMessage(error: unknown): string { if (!(error instanceof Error)) { return "Update check failed."; @@ -94,6 +93,9 @@ export function createAppUpdaterController(options: AppUpdaterControllerOptions) setAutomaticChecksEnabled() { return disabledState; }, + setUpdateChannel() { + return disabledState; + }, async checkForUpdates() { return disabledState; }, @@ -123,6 +125,9 @@ export function createAppUpdaterController(options: AppUpdaterControllerOptions) setAutomaticChecksEnabled() { return disabledState; }, + setUpdateChannel() { + return disabledState; + }, async checkForUpdates() { return disabledState; }, @@ -147,8 +152,7 @@ export function createAppUpdaterController(options: AppUpdaterControllerOptions) updater.autoDownload = false; updater.autoInstallOnAppQuit = false; updater.autoRunAppAfterInstall = true; - updater.allowPrerelease = isPrereleaseVersion(currentVersion); - updater.allowDowngrade = false; + applyUpdateChannel(updater, options.updateChannel); updater.fullChangelog = false; let disposed = false; @@ -157,6 +161,7 @@ export function createAppUpdaterController(options: AppUpdaterControllerOptions) let checkInFlight = false; let downloadInFlight = false; let automaticChecksEnabled = options.automaticChecksEnabled; + let updateChannel = options.updateChannel; let availableUpdateInfo: UpdateInfo | null = null; let downloadedUpdateInfo: UpdateInfo | null = null; @@ -336,6 +341,32 @@ export function createAppUpdaterController(options: AppUpdaterControllerOptions) scheduleAutomaticChecks(); return state; }, + setUpdateChannel(channel: UpdateChannel) { + if (channel === updateChannel) { + return state; + } + + updateChannel = channel; + applyUpdateChannel(updater, channel); + availableUpdateInfo = null; + downloadedUpdateInfo = null; + updateState({ + status: "idle", + availableVersion: undefined, + downloadedVersion: undefined, + progress: undefined, + message: channel === "nightly" + ? "Nightly channel selected. Checking for preview builds..." + : "Stable channel selected. Checking for regular releases...", + errorCode: undefined, + }); + setImmediate(() => { + if (!disposed) { + void controller.checkForUpdates("manual"); + } + }); + return state; + }, async checkForUpdates(source: "auto" | "manual" = "manual") { if (disposed || checkInFlight || downloadInFlight) { return state; diff --git a/opennow-stable/src/main/window/externalUrl.ts b/opennow-stable/src/main/window/externalUrl.ts new file mode 100644 index 000000000..4fded7a79 --- /dev/null +++ b/opennow-stable/src/main/window/externalUrl.ts @@ -0,0 +1,30 @@ +import { shell } from "electron"; +import { join } from "node:path"; +import { pathToFileURL } from "node:url"; + +export function parseExternalHttpUrl(url: string): URL { + const parsed = new URL(url); + if (parsed.protocol !== "https:" && parsed.protocol !== "http:") { + throw new Error("Only HTTP(S) external URLs can be opened."); + } + return parsed; +} + +export function isAppNavigationUrl(url: string, mainDir: string): boolean { + try { + const parsed = new URL(url); + if (process.env.ELECTRON_RENDERER_URL) { + return parsed.origin === new URL(process.env.ELECTRON_RENDERER_URL).origin; + } + return ( + parsed.toString() === + pathToFileURL(join(mainDir, "../../dist/index.html")).toString() + ); + } catch { + return false; + } +} + +export async function openExternalHttpUrl(url: string): Promise { + await shell.openExternal(parseExternalHttpUrl(url).toString()); +} diff --git a/opennow-stable/src/main/window/mainWindow.ts b/opennow-stable/src/main/window/mainWindow.ts new file mode 100644 index 000000000..79e1ed44a --- /dev/null +++ b/opennow-stable/src/main/window/mainWindow.ts @@ -0,0 +1,259 @@ +import { BrowserWindow, ipcMain } from "electron"; +import { existsSync } from "node:fs"; +import { join } from "node:path"; +import { IPC_CHANNELS } from "@shared/ipc"; +import type { DirectLaunchRequest } from "@shared/gfn"; +import type { SettingsManager } from "../settings"; +import { + ESCAPE_HOLD_TO_EXIT_FULLSCREEN_MS, + markEscapeHoldFired, + nextPointerLockEscapeCaptureUntilMs, + resolveEscapeHoldCaptureAction, + type EscapeHoldCaptureState, +} from "../escapeFullscreenGuard"; +import { captureMainException } from "../telemetry/posthog"; +import { isAppNavigationUrl, openExternalHttpUrl } from "./externalUrl"; + +export interface CreateMainWindowDeps { + mainDir: string; + settingsManager: SettingsManager; + getMainWindow(): BrowserWindow | null; + setMainWindow(window: BrowserWindow | null): void; + getRendererControlledFullscreen(): boolean; + setRendererControlledFullscreen(value: boolean): void; + getPendingDirectLaunchRequest(): DirectLaunchRequest | null; + emitDirectLaunchRequest(request: DirectLaunchRequest): void; + getPointerLockActive(): boolean; + setPointerLockActive(active: boolean): void; + getPointerLockEscapeCaptureUntilMs(): number; + setPointerLockEscapeCaptureUntilMs(value: number): void; + getStreamInputActive(): boolean; + setStreamInputActive(active: boolean): void; + getNativeRawInputOwnsEscape(): boolean; + setNativeRawInputOwnsEscape(ownsEscape: boolean): void; +} + +export async function createMainWindow( + deps: CreateMainWindowDeps, +): Promise { + const preloadMjsPath = join(deps.mainDir, "../preload/index.mjs"); + const preloadJsPath = join(deps.mainDir, "../preload/index.js"); + const preloadPath = existsSync(preloadMjsPath) + ? preloadMjsPath + : preloadJsPath; + + const settings = deps.settingsManager.getAll(); + let escapeHoldState: EscapeHoldCaptureState = { keyDownCaptured: false, holdFired: false }; + let escapeHoldTimer: NodeJS.Timeout | null = null; + const clearEscapeHoldTimer = (): void => { + if (escapeHoldTimer !== null) { + clearTimeout(escapeHoldTimer); + escapeHoldTimer = null; + } + }; + + // Console mode (big picture): mirror GeForce NOW's TV mode by launching + // fullscreen with the controller-oriented shell enabled. + if (settings.launchInConsoleMode && !settings.controllerMode) { + deps.settingsManager.set("controllerMode", true); + } + + // Direct-launch arguments always start fullscreen; the renderer applies the + // console shell for the run without persisting the Controller Mode setting. + const startFullscreen = + settings.launchInConsoleMode || + deps.getPendingDirectLaunchRequest() !== null; + + const window = new BrowserWindow({ + width: settings.windowWidth || 1400, + height: settings.windowHeight || 900, + minWidth: 1024, + minHeight: 680, + ...(startFullscreen ? { fullscreen: true } : {}), + autoHideMenuBar: true, + backgroundColor: "#0f172a", + webPreferences: { + preload: preloadPath, + contextIsolation: true, + nodeIntegration: false, + sandbox: false, + }, + }); + deps.setMainWindow(window); + + window.webContents.on("render-process-gone", (_event, details) => { + console.error("[Main] Renderer process gone:", details); + captureMainException(new Error(`Renderer process gone: ${details.reason}`), { + reason: details.reason, + exit_code: details.exitCode, + }); + }); + window.webContents.on("console-message", (_event, level, message, line, sourceId) => { + if (level < 2) return; + console.error(`[renderer:console:${level}]`, message, sourceId ? `(${sourceId}:${line})` : ""); + }); + + window.webContents.setWindowOpenHandler(({ url }) => { + void openExternalHttpUrl(url).catch((error) => { + console.warn( + "Blocked non-external window open:", + error instanceof Error ? error.message : error, + ); + }); + return { action: "deny" }; + }); + + window.webContents.on("will-navigate", (event, url) => { + if (isAppNavigationUrl(url, deps.mainDir)) { + return; + } + + event.preventDefault(); + void openExternalHttpUrl(url).catch((error) => { + console.warn( + "Blocked app window navigation:", + error instanceof Error ? error.message : error, + ); + }); + }); + + if (process.platform === "win32") { + // Keep native window fullscreen in sync with HTML fullscreen so Windows treats + // stream playback like a real fullscreen window instead of only DOM fullscreen. + window.webContents.on("enter-html-full-screen", () => { + const mainWindow = deps.getMainWindow(); + if ( + mainWindow && + !mainWindow.isDestroyed() && + !mainWindow.isFullScreen() + ) { + mainWindow.setFullScreen(true); + } + }); + + window.webContents.on("leave-html-full-screen", () => { + if (deps.getRendererControlledFullscreen()) { + return; + } + const mainWindow = deps.getMainWindow(); + if ( + mainWindow && + !mainWindow.isDestroyed() && + mainWindow.isFullScreen() + ) { + mainWindow.setFullScreen(false); + } + }); + } + + // Track pointer-lock state from renderer; used to decide whether to swallow + // Escape at the native level (before Chromium handles it). + ipcMain.on( + IPC_CHANNELS.POINTER_LOCK_CHANGE, + (_ev, active: boolean, suppressEscapeFullscreenGrace?: boolean) => { + const pointerLockActive = Boolean(active); + deps.setPointerLockActive(pointerLockActive); + deps.setPointerLockEscapeCaptureUntilMs( + nextPointerLockEscapeCaptureUntilMs( + pointerLockActive, + Boolean(suppressEscapeFullscreenGrace), + Date.now(), + ), + ); + }, + ); + + ipcMain.on( + IPC_CHANNELS.NATIVE_INPUT_MODE_CHANGE, + (_ev, active: boolean, rawInputOwnsEscape: boolean) => { + const streamInputActive = Boolean(active); + deps.setStreamInputActive(streamInputActive); + deps.setNativeRawInputOwnsEscape( + streamInputActive && Boolean(rawInputOwnsEscape), + ); + }, + ); + + // Intercept Escape early to avoid Chromium exiting fullscreen before the + // renderer can forward the key to the remote session. Keep a short fullscreen + // grace window after pointer lock drops so rapid repeated Escape presses cannot + // win the race before the renderer re-locks the pointer. + window.webContents.on("before-input-event", (event, input) => { + try { + const mainWindow = deps.getMainWindow(); + const resolved = resolveEscapeHoldCaptureAction( + input, + { + allowEscapeToExitFullscreen: Boolean( + deps.settingsManager?.get("allowEscapeToExitFullscreen"), + ), + streamInputActive: deps.getStreamInputActive(), + pointerLockActive: deps.getPointerLockActive(), + rendererControlledFullscreen: deps.getRendererControlledFullscreen(), + windowFullscreen: Boolean( + mainWindow && + !mainWindow.isDestroyed() && + mainWindow.isFullScreen(), + ), + pointerLockEscapeCaptureUntilMs: + deps.getPointerLockEscapeCaptureUntilMs(), + nowMs: Date.now(), + }, + escapeHoldState, + ); + escapeHoldState = resolved.nextHoldState; + + if (resolved.action === "ignore") return; + event.preventDefault(); + + if (resolved.action === "arm-hold") { + clearEscapeHoldTimer(); + escapeHoldTimer = setTimeout(() => { + escapeHoldTimer = null; + const activeWindow = deps.getMainWindow(); + if (!activeWindow || activeWindow.isDestroyed()) return; + if (!activeWindow.isFullScreen() && !deps.getRendererControlledFullscreen()) return; + escapeHoldState = markEscapeHoldFired(escapeHoldState); + activeWindow.webContents.send(IPC_CHANNELS.EXIT_FULLSCREEN); + }, ESCAPE_HOLD_TO_EXIT_FULLSCREEN_MS); + return; + } + + if (resolved.action === "tap") { + clearEscapeHoldTimer(); + // Windows internal native mode receives the same physical key through + // its persistent RawInput keyboard sink. Forward only when Electron is + // the input owner so the remote session sees exactly one Escape tap. + if (!deps.getNativeRawInputOwnsEscape()) { + console.log("[EscapeInput] Forwarding captured Escape tap to the stream session"); + mainWindow?.webContents.send(IPC_CHANNELS.EXTERNAL_ESCAPE); + } + } else if (resolved.action === "hold-consumed-keyup") { + clearEscapeHoldTimer(); + } + } catch { + // ignore errors - interception is best-effort + } + }); + + if (process.env.ELECTRON_RENDERER_URL) { + await window.loadURL(process.env.ELECTRON_RENDERER_URL); + } else { + await window.loadFile(join(deps.mainDir, "../../dist/index.html")); + } + const pendingDirectLaunchRequest = deps.getPendingDirectLaunchRequest(); + if (pendingDirectLaunchRequest) { + deps.emitDirectLaunchRequest(pendingDirectLaunchRequest); + } + + window.on("closed", () => { + clearEscapeHoldTimer(); + escapeHoldState = { keyDownCaptured: false, holdFired: false }; + deps.setMainWindow(null); + deps.setRendererControlledFullscreen(false); + deps.setPointerLockActive(false); + deps.setPointerLockEscapeCaptureUntilMs(0); + deps.setStreamInputActive(false); + deps.setNativeRawInputOwnsEscape(false); + }); +} diff --git a/opennow-stable/src/preload/index.ts b/opennow-stable/src/preload/index.ts index bc9954c45..dd7d9ff13 100644 --- a/opennow-stable/src/preload/index.ts +++ b/opennow-stable/src/preload/index.ts @@ -177,6 +177,13 @@ const api: OpenNowApi = { ipcRenderer.off("app:toggle-fullscreen", wrapped); }; }, + onExitFullscreen: (listener: () => void) => { + const wrapped = () => listener(); + ipcRenderer.on(IPC_CHANNELS.EXIT_FULLSCREEN, wrapped); + return () => { + ipcRenderer.off(IPC_CHANNELS.EXIT_FULLSCREEN, wrapped); + }; + }, quitApp: () => ipcRenderer.invoke(IPC_CHANNELS.QUIT_APP), getUpdaterState: (): Promise => ipcRenderer.invoke(IPC_CHANNELS.APP_UPDATER_GET_STATE), checkForUpdates: (): Promise => ipcRenderer.invoke(IPC_CHANNELS.APP_UPDATER_CHECK), @@ -205,6 +212,9 @@ const api: OpenNowApi = { notifyPointerLockChange: (active: boolean, suppressEscapeFullscreenGrace?: boolean) => { ipcRenderer.send(IPC_CHANNELS.POINTER_LOCK_CHANGE, active, suppressEscapeFullscreenGrace); }, + notifyNativeInputModeChange: (active: boolean, rawInputOwnsEscape: boolean) => { + ipcRenderer.send(IPC_CHANNELS.NATIVE_INPUT_MODE_CHANGE, active, rawInputOwnsEscape); + }, onExternalEscape: (listener: () => void) => { const wrapped = () => listener(); ipcRenderer.on(IPC_CHANNELS.EXTERNAL_ESCAPE, wrapped); diff --git a/opennow-stable/src/renderer/src/App.tsx b/opennow-stable/src/renderer/src/App.tsx index 77a78403b..ae11f0d90 100644 --- a/opennow-stable/src/renderer/src/App.tsx +++ b/opennow-stable/src/renderer/src/App.tsx @@ -1,102 +1,83 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import type { CSSProperties, JSX } from "react"; -import { createPortal } from "react-dom"; -import { AnimatePresence, m } from "motion/react"; +import { AnimatePresence, m, useReducedMotion } from "motion/react"; import type { ActiveSessionInfo, - AuthDeviceLoginChallenge, AuthSession, - CatalogBrowseResult, - CatalogFilterGroup, - CatalogSortOption, DirectLaunchRequest, - ExistingSessionStrategy, GameInfo, - GamePanelResult, - GameVariant, LoginProvider, - MainToRendererSignalingEvent, NativeStreamerShortcutAction, ReleaseHighlightsPayload, SessionInfo, SessionStopRequest, - SavedAccount, Settings, SubscriptionInfo, SignalingConnectRequest, StreamSettings, StreamRegion, - PrintedWasteQueueData, VideoShaderSettings, } from "@shared/gfn"; +import { discordGameImageUrl } from "@shared/discord"; import { buildNativeStreamerSessionContext, - DEFAULT_KEYBOARD_LAYOUT, - DEFAULT_VIDEO_SHADER_SETTINGS, - getDefaultStreamPreferences, - isGameInLibrary, - isSessionAdsRequired, + createDefaultSettings, + createPlatformShortcutDefaults, resolveEntitledStreamProfile, + resolveRuntimePlatform, SAFE_FALLBACK_STREAM_PROFILE, } from "@shared/gfn"; -import { GfnWebRtcClient } from "./gfn/webrtcClient"; import { formatShortcutForDisplay, isShortcutMatch, normalizeShortcut } from "./shortcuts"; import { dispatchStreamShortcutAction } from "./streamShortcutActions"; import { useElapsedSeconds } from "./utils/useElapsedSeconds"; +import { useAuthSession } from "./hooks/useAuthSession"; +import { useCatalogData } from "./hooks/useCatalogData"; +import { useActiveSessionActions } from "./hooks/streamSession/useActiveSessionActions"; +import { useGameLaunch } from "./hooks/streamSession/useGameLaunch"; +import { useSignalingEvents } from "./hooks/streamSession/useSignalingEvents"; +import { + RECOVERABLE_STREAM_STATUSES, + SIGNALING_RECOVERY_ATTEMPT_DELAYS_MS, + sendStreamClipboardPaste, + sleep, + useStreamSession, +} from "./hooks/useStreamSession"; import { useQueueAdRuntime } from "./hooks/useQueueAdRuntime"; import { usePlaytime } from "./utils/usePlaytime"; -import { createStreamDiagnosticsStore } from "./utils/streamDiagnosticsStore"; -import type { - LaunchErrorState, - LocalSessionTimerWarningState, - StreamLoadingStatus, - StreamStatus, - StreamWarningState, -} from "./lib/appTypes"; -import { loadCatalogPreferences, saveCatalogPreferences, VARIANT_SELECTION_LOCALSTORAGE_KEY } from "./lib/catalogPreferences"; -import { - buildCatalogQueryKey, - clearCatalogSnapshot, - loadCatalogSnapshot, - saveCatalogSnapshot, -} from "./lib/catalogSnapshot"; +import { createStreamDiagnosticsStore, useStreamDiagnosticsSelector } from "./utils/streamDiagnosticsStore"; +import type { StreamStatus } from "./lib/appTypes"; import { loadStoredCodecResults, saveStoredCodecResults, testCodecSupport, type CodecTestResult } from "./lib/codecDiagnostics"; import { - areStringArraysEqual, + createSyntheticDirectLaunchGame, + findDirectLaunchTarget, +} from "./lib/directLaunch"; +import { defaultVariantId, findSessionContextForAppId, getSelectedVariant, isNumericId, matchesGameSearch, - mergeVariantSelections, parseNumericId, sortLibraryGames, } from "./lib/gameCatalog"; -import { chooseAccountLinked, getEpicOwnershipLaunchError, resolveInstallToPlayStorageRegionUrl } from "./lib/launchOwnership"; +import { resolveInstallToPlayStorageRegionUrl } from "./lib/launchOwnership"; import { hasAnyEligiblePrintedWasteZone, isAllianceStreamingBaseUrl } from "./lib/printedWaste"; -import { - mergePolledSessionState, - normalizeMembershipTier, - shouldUseQueueAdPolling, -} from "./lib/queueAds"; -import { clearRuntimeSnapshot, loadRuntimeSnapshot, saveRuntimeSnapshot, type RuntimeSnapshot } from "./lib/runtimeSnapshot"; +import { normalizeMembershipTier } from "./lib/queueAds"; +import { clearRuntimeSnapshot, type RuntimeSnapshot } from "./lib/runtimeSnapshot"; +import { getEnabledSessionProxyUrl } from "./lib/sessionProxy"; import { getSessionLimitSecondsForTier, getLocalSessionTimerWarning, hasCrossedWarningThreshold, shouldShowFreeTierSessionWarnings, - warningMessage, - warningTone, } from "./lib/sessionWarnings"; import { - isSessionInQueue, - isSessionReadyForConnect, - streamStatusToLoadingStage, - toLaunchErrorState, + isStreamVideoReady, toLoadingStatus, } from "./lib/sessionState"; -import { defaultDiagnostics, mergeNativeStreamStats } from "./lib/streamDiagnostics"; +import { defaultDiagnostics } from "./lib/streamDiagnostics"; +import { selectRecoveryCandidate } from "./lib/streamRecoveryDecisions"; import { applyAccentColor, applyTheme, applyTranslucentUI } from "./lib/uiCustomization"; import { useTranslation } from "./i18n"; @@ -105,154 +86,34 @@ import { LoginScreen } from "./components/LoginScreen"; import { Navbar } from "./components/Navbar"; import { HomePage } from "./components/HomePage"; import { LibraryPage } from "./components/LibraryPage"; +import { PageErrorBoundary } from "./components/PageErrorBoundary"; import { SettingsPage } from "./components/SettingsPage"; import { SettingsModalHost } from "./components/SettingsModalHost"; import { StreamLoading } from "./components/StreamLoading"; import { StreamView } from "./components/StreamView"; import { QueueServerSelectModal } from "./components/QueueServerSelectModal"; import { ReleaseHighlightsModal } from "./components/ReleaseHighlightsModal"; -import { pageTransition } from "./components/MotionProvider"; - -const DEFAULT_STREAM_PREFERENCES = getDefaultStreamPreferences(); +import { ErrorReportingConsentModal } from "./components/ErrorReportingConsentModal"; +import { FeedbackModal } from "./components/FeedbackModal"; +import { ModalSurface } from "./components/ui/ModalSurface"; +import { overlayMotion, pageTransition, streamRevealTransition } from "./components/MotionProvider"; +import { LazyShaderAtmosphere } from "./components/LazyShaderAtmosphere"; +import { syncRendererTelemetry } from "./telemetry/posthog"; type AppStyle = CSSProperties & { "--game-poster-scale"?: string; }; -interface DirectLaunchTarget { - game: GameInfo; - variantId?: string; -} - function getAppStyle(posterSizeScale: number): AppStyle { return { "--game-poster-scale": String(posterSizeScale), }; } -function getEnabledSessionProxyUrl(settings: Pick): string | undefined { - const proxyUrl = settings.sessionProxyEnabled ? settings.sessionProxyUrl.trim() : ""; - return proxyUrl || undefined; -} - -function getSessionProxyUiScope(proxyUrl: string | undefined): string { - if (!proxyUrl) return "direct"; - const trimmed = proxyUrl.trim(); - const candidate = /^[a-z][a-z0-9+.-]*:\/\//i.test(trimmed) ? trimmed : `http://${trimmed}`; - - try { - const parsed = new URL(candidate); - return `${parsed.protocol}//${parsed.host}`; - } catch { - return "proxy"; - } -} - -function hasSessionProxyCredentials(proxyUrl: string | undefined): boolean { - if (!proxyUrl) return false; - const trimmed = proxyUrl.trim(); - const candidate = /^[a-z][a-z0-9+.-]*:\/\//i.test(trimmed) ? trimmed : `http://${trimmed}`; - - try { - const parsed = new URL(candidate); - return parsed.username.length > 0 || parsed.password.length > 0; - } catch { - return false; - } -} - -function buildProxyAwareCatalogQueryKey( - searchQuery: string, - filterIds: string[], - sortId: string, - proxyUrl: string | undefined, -): string { - return `${buildCatalogQueryKey(searchQuery, filterIds, sortId)}|${getSessionProxyUiScope(proxyUrl)}`; -} - -function normalizeDirectLaunchText(value: string | undefined): string { - return value?.trim().replace(/\s+/g, " ").toLowerCase() ?? ""; -} - -function directLaunchOwnershipScore(game: GameInfo): number { - return game.isInLibrary || isGameInLibrary(game) ? 10 : 0; -} - -function findDirectLaunchTargetByTitle(catalog: GameInfo[], title: string): DirectLaunchTarget | null { - const normalizedTitle = normalizeDirectLaunchText(title); - if (!normalizedTitle) return null; - - let best: { target: DirectLaunchTarget; score: number } | null = null; - for (const game of catalog) { - const gameTitle = normalizeDirectLaunchText(game.title); - const shortName = normalizeDirectLaunchText(game.shortName); - let score = 0; - if (gameTitle === normalizedTitle) { - score = 100; - } else if (shortName && shortName === normalizedTitle) { - score = 95; - } else if (gameTitle.startsWith(normalizedTitle)) { - score = 80; - } else if (matchesGameSearch(game, title)) { - score = 60; - } - - if (score === 0) continue; - score += directLaunchOwnershipScore(game); - if (!best || score > best.score) { - best = { target: { game }, score }; - } - } - - return best?.target ?? null; -} - -function createSyntheticDirectLaunchGame(request: DirectLaunchRequest, appId: string): GameInfo { - const title = request.title?.trim() || `GFN App ${appId}`; - return { - id: `direct-launch-${appId}`, - launchAppId: appId, - title, - searchText: normalizeDirectLaunchText(title), - isInLibrary: true, - selectedVariantIndex: 0, - variants: [ - { - id: appId, - store: "UNKNOWN", - supportedControls: [], - libraryStatus: "IN_LIBRARY", - }, - ], - }; -} - -function findDirectLaunchTarget( - request: DirectLaunchRequest, - catalog: GameInfo[], - variantByGameId: Record, -): DirectLaunchTarget | null { - const numericAppId = parseNumericId(request.appId); - if (numericAppId !== null) { - const matched = findSessionContextForAppId(catalog, variantByGameId, numericAppId); - if (matched) { - return { game: matched.game, variantId: matched.variant?.id }; - } - } - - if (request.title) { - return findDirectLaunchTargetByTitle(catalog, request.title); - } - - return null; -} - function isNvidiaProvider(provider: LoginProvider | null | undefined): boolean { return (provider?.code ?? "").trim().toUpperCase() === "NVIDIA"; } -const SESSION_READY_POLL_INTERVAL_MS = 2000; -const SESSION_AD_POLL_INTERVAL_MS = 30000; const PLAYTIME_RESYNC_INTERVAL_MS = 5 * 60 * 1000; const FREE_TIER_30_MIN_WARNING_SECONDS = 30 * 60; const FREE_TIER_15_MIN_WARNING_SECONDS = 15 * 60; @@ -261,377 +122,113 @@ const STREAM_WARNING_VISIBILITY_MS = 15 * 1000; type AppPage = "home" | "library" | "settings"; type ExitPromptState = { open: boolean; gameTitle: string }; -type SignalingRecoveryState = { - attemptCount: number; - inFlight: Promise | null; - explicitShutdown: boolean; - appId: number | null; - generation: number; -}; - -const RECOVERABLE_STREAM_STATUSES: readonly StreamStatus[] = ["streaming"]; -const SIGNALING_RECOVERY_ATTEMPT_DELAYS_MS = [0, 3000] as const; -const SIGNALING_RECOVERY_STABLE_RESET_DELAY_MS = 15000; -const SIGNALING_REMOTE_ICE_GRACE_MS = 5000; -const ICE_DISCONNECTED_RECOVERY_GRACE_MS = 7000; - -const isMac = navigator.platform.toLowerCase().includes("mac"); - -function isExpectedNativeSessionClose(reason: string): boolean { - const normalized = reason.trim().toLowerCase(); - return normalized === "bye" || - normalized === "peerremoved" || - normalized === "peer removed" || - normalized === "socket closed" || - normalized === "signaling disconnected: socket closed"; -} - -function gameIdentityMatches(left: GameInfo, right: GameInfo): boolean { - if (left.uuid && right.uuid && left.uuid === right.uuid) return true; - if (left.id && right.id && left.id === right.id) return true; - if (left.launchAppId && right.launchAppId && left.launchAppId === right.launchAppId) return true; - return left.title.trim().length > 0 && left.title.localeCompare(right.title, undefined, { sensitivity: "accent" }) === 0; -} - -function markVariantOwned(variant: GameVariant, selected: boolean): GameVariant { - return { - ...variant, - inLibrary: true, - librarySelected: selected, - libraryStatus: "MANUAL", - }; -} - -function markGameVariantOwned(game: GameInfo, variantId: string): GameInfo { - const selectedVariantIndex = game.variants.findIndex((variant) => variant.id === variantId); - if (selectedVariantIndex < 0) { - return game; - } - - return { - ...game, - isInLibrary: true, - selectedVariantIndex, - variants: game.variants.map((variant, index) => ( - index === selectedVariantIndex - ? markVariantOwned(variant, true) - : { ...variant, librarySelected: false } - )), - }; -} - -function markGameOwnedInList(games: GameInfo[], target: GameInfo, variantId: string): GameInfo[] { - let changed = false; - const next = games.map((game) => { - if (!gameIdentityMatches(game, target)) return game; - if (!game.variants.some((variant) => variant.id === variantId)) return game; - changed = true; - return markGameVariantOwned(game, variantId); - }); - return changed ? next : games; -} - -function upsertMarkedOwnedLibraryGame(games: GameInfo[], target: GameInfo, variantId: string): GameInfo[] { - let changed = false; - const next = games.map((game) => { - if (!gameIdentityMatches(game, target)) return game; - if (!game.variants.some((variant) => variant.id === variantId)) return game; - changed = true; - return markGameVariantOwned(game, variantId); - }); - return changed ? next : [markGameVariantOwned(target, variantId), ...games]; -} - -function markGameOwnedInPanels(panels: GamePanelResult[], target: GameInfo, variantId: string): GamePanelResult[] { - let changed = false; - const next = panels.map((panel) => ({ - ...panel, - sections: panel.sections.map((section) => ({ - ...section, - games: section.games.map((game) => { - if (!gameIdentityMatches(game, target)) return game; - if (!game.variants.some((variant) => variant.id === variantId)) return game; - changed = true; - return markGameVariantOwned(game, variantId); - }), - })), - })); - return changed ? next : panels; -} - -function getLibrarySelectedVariantId(storeGame: GameInfo, libraryGames: GameInfo[]): string | undefined { - const libraryGame = libraryGames.find((candidate) => gameIdentityMatches(storeGame, candidate)); - const libraryVariant = libraryGame?.variants.find((variant) => variant.librarySelected) - ?? libraryGame?.variants.find((variant) => variant.inLibrary) - ?? libraryGame?.variants[0]; - if (!libraryVariant) return undefined; - - const sameIdVariant = storeGame.variants.find((variant) => variant.id === libraryVariant.id); - if (sameIdVariant) return sameIdVariant.id; - - const sameStoreVariant = storeGame.variants.find((variant) => variant.store.localeCompare(libraryVariant.store, undefined, { sensitivity: "accent" }) === 0); - return sameStoreVariant?.id; -} - -function flattenStorePanelGames(panels: GamePanelResult[]): GameInfo[] { - return panels.flatMap((panel) => panel.sections.flatMap((section) => section.games)); -} - -const DEFAULT_SHORTCUTS = { - shortcutToggleStats: "F3", - shortcutTogglePointerLock: "F8", - shortcutToggleFullscreen: "F10", - shortcutStopStream: "Ctrl+Shift+Q", - shortcutToggleAntiAfk: "Ctrl+Shift+K", - shortcutToggleMicrophone: "Ctrl+Shift+M", - shortcutScreenshot: "F11", - shortcutToggleRecording: "F12", -} as const; - - -function sleep(ms: number): Promise { - return new Promise((resolve) => window.setTimeout(resolve, ms)); -} - -async function readStreamClipboardText(): Promise { - try { - const browserClipboard = navigator.clipboard; - if (browserClipboard?.readText) { - const text = await browserClipboard.readText(); - if (text) { - return text; - } - } - } catch { - // Electron main-process clipboard is the reliable fallback on Linux. - } - - return window.openNow.readClipboardText(); -} - -async function sendStreamClipboardPaste( - client: GfnWebRtcClient | null, -): Promise { - if (!client) { - return; - } - - const sentOfficialPaste = await client.pasteClipboardText(); - if (sentOfficialPaste) { - return; - } - - try { - const text = await readStreamClipboardText(); - if (text) { - client.sendText(text); - } - return; - } catch (error) { - console.warn("Clipboard read failed, falling back to paste shortcut:", error); - } - client.sendPasteShortcut(false); -} +const RUNTIME_PLATFORM = resolveRuntimePlatform(navigator.platform); +const isMac = RUNTIME_PLATFORM === "darwin"; +const DEFAULT_SHORTCUTS = createPlatformShortcutDefaults(RUNTIME_PLATFORM).bindings; export function App(): JSX.Element { const { locale, t } = useTranslation(); + const reducedMotion = useReducedMotion(); - // Auth State - const [authSession, setAuthSession] = useState(null); - const [savedAccounts, setSavedAccounts] = useState([]); - const [providers, setProviders] = useState([]); - const [providerIdpId, setProviderIdpId] = useState(""); - const [isLoggingIn, setIsLoggingIn] = useState(false); - const [activeLoginMode, setActiveLoginMode] = useState<"oauth" | "qr" | null>(null); - const [loginError, setLoginError] = useState(null); - const [qrLoginChallenge, setQrLoginChallenge] = useState(null); - const [isInitializing, setIsInitializing] = useState(true); - const [startupStatusMessage, setStartupStatusMessage] = useState(() => t("auth.status.restoringSavedSession")); - const [startupRefreshNotice, setStartupRefreshNotice] = useState<{ - tone: "success" | "warn"; - text: string; - } | null>(null); + // Navigation / settings / stream state below; auth + catalog come from hooks after deps are ready. // Navigation const [currentPage, setCurrentPage] = useState("home"); const [pageBeforeSettings, setPageBeforeSettings] = useState("home"); - const [settingsMounted, setSettingsMounted] = useState(false); const [sessionFullscreen, setSessionFullscreenState] = useState(false); - // Games State - const [games, setGames] = useState([]); - const [featuredGames, setFeaturedGames] = useState([]); - const [storePanels, setStorePanels] = useState([]); - const [libraryGames, setLibraryGames] = useState([]); - const [searchQuery, setSearchQuery] = useState(""); - const [selectedGameId, setSelectedGameId] = useState(""); - const [variantByGameId, setVariantByGameId] = useState>({}); - const [isLoadingCatalog, setIsLoadingCatalog] = useState(false); - const [isLoadingLibrary, setIsLoadingLibrary] = useState(false); - const [isLoadingStorePanels, setIsLoadingStorePanels] = useState(false); - const [catalogFilterGroups, setCatalogFilterGroups] = useState([]); - const [catalogSortOptions, setCatalogSortOptions] = useState([]); - const [catalogSelectedSortId, setCatalogSelectedSortId] = useState(() => loadCatalogPreferences().sortId); - const [catalogSelectedFilterIds, setCatalogSelectedFilterIds] = useState(() => loadCatalogPreferences().filterIds); - const [catalogTotalCount, setCatalogTotalCount] = useState(0); - const [catalogSupportedCount, setCatalogSupportedCount] = useState(0); - const catalogFilterKey = useMemo(() => catalogSelectedFilterIds.join("|"), [catalogSelectedFilterIds]); - const [markOwnedInFlightByVariantId, setMarkOwnedInFlightByVariantId] = useState>({}); - const [catalogActionNotice, setCatalogActionNotice] = useState<{ - tone: "success" | "warn"; - text: string; - } | null>(null); - // Settings State - const [settings, setSettings] = useState({ - resolution: "1920x1080", - aspectRatio: "16:9", - posterSizeScale: 1, - fps: 60, - maxBitrateMbps: 75, - recordingBitrateMbps: null, - streamClientMode: "web", - nativeStreamerBackend: "gstreamer", - nativeVideoBackend: "auto", - nativeStreamerExecutablePath: "", - nativeCloudGsyncMode: "auto", - nativeD3dFullscreenMode: "auto", - nativeExternalRenderer: true, - showNativeStreamerStats: false, - codec: DEFAULT_STREAM_PREFERENCES.codec, - decoderPreference: "auto", - encoderPreference: "auto", - colorQuality: DEFAULT_STREAM_PREFERENCES.colorQuality, - region: "", - sessionProxyEnabled: false, - sessionProxyUrl: "", - clipboardPaste: false, - enableGyroscopeControls: false, - steamControllerCompatibilityMode: false, - nativeCursorOverlay: true, - mouseSensitivity: 1, - mouseAcceleration: 1, - shortcutToggleStats: DEFAULT_SHORTCUTS.shortcutToggleStats, - shortcutTogglePointerLock: DEFAULT_SHORTCUTS.shortcutTogglePointerLock, - shortcutToggleFullscreen: DEFAULT_SHORTCUTS.shortcutToggleFullscreen, - shortcutStopStream: DEFAULT_SHORTCUTS.shortcutStopStream, - shortcutToggleAntiAfk: DEFAULT_SHORTCUTS.shortcutToggleAntiAfk, - shortcutToggleMicrophone: DEFAULT_SHORTCUTS.shortcutToggleMicrophone, - shortcutScreenshot: DEFAULT_SHORTCUTS.shortcutScreenshot, - shortcutToggleRecording: DEFAULT_SHORTCUTS.shortcutToggleRecording, - microphoneMode: "disabled", - microphoneDeviceId: "", - hideStreamButtons: false, - showAntiAfkIndicator: true, - showStatsOnLaunch: false, - hideServerSelector: false, - appAccentColor: "green", - appTheme: "auto", - translucentUI: false, - controllerMode: false, - launchInConsoleMode: false, - autoFullScreen: false, - favoriteGameIds: [], - sessionCounterEnabled: false, - showSessionTimeRemainingInStatsOverlay: false, - sessionClockShowEveryMinutes: 60, - sessionClockShowDurationSeconds: 30, - windowWidth: 1400, - windowHeight: 900, - keyboardLayout: DEFAULT_KEYBOARD_LAYOUT, - gameLanguage: "en_US", - enablePersistingInGameSettings: false, - enableL4S: false, - enableCloudGsync: false, - discordRichPresence: false, - autoCheckForUpdates: true, - lastSeenReleaseHighlightsVersion: "", - videoShader: { ...DEFAULT_VIDEO_SHADER_SETTINGS }, - }); + const [settings, setSettings] = useState(() => createDefaultSettings(RUNTIME_PLATFORM)); const [settingsLoaded, setSettingsLoaded] = useState(false); const [releaseHighlightsPayload, setReleaseHighlightsPayload] = useState(null); const [releaseHighlightsIsAuto, setReleaseHighlightsIsAuto] = useState(false); + const [feedbackOpen, setFeedbackOpen] = useState(false); + const [feedbackSurfacePresent, setFeedbackSurfacePresent] = useState(false); + const [consentSurfacePresent, setConsentSurfacePresent] = useState(false); const activeSessionProxyUrl = useMemo( () => getEnabledSessionProxyUrl(settings), [settings.sessionProxyEnabled, settings.sessionProxyUrl], ); const [codecResults, setCodecResults] = useState(() => loadStoredCodecResults()); const [codecTesting, setCodecTesting] = useState(false); - const [regions, setRegions] = useState([]); - const [subscriptionInfo, setSubscriptionInfo] = useState(null); const diagnosticsStoreRef = useRef | null>(null); const diagnosticsStore = diagnosticsStoreRef.current ?? (diagnosticsStoreRef.current = createStreamDiagnosticsStore(defaultDiagnostics())); + const diagnosticsVideoReady = useStreamDiagnosticsSelector( + diagnosticsStore, + (stats) => stats.nativeRendererActive || stats.framesDecoded > 0, + ); + + const { runtime: streamRuntime, recovery, snapshot } = useStreamSession(); + const { + session, setSession, + streamStatus, setStreamStatus, + showStatsOverlay, setShowStatsOverlay, + antiAfkEnabled, setAntiAfkEnabled, + antiAfkAckNonce, setAntiAfkAckNonce, + nativeInputCaptureActive, setNativeInputCaptureActive, + nativeInputBridgeReady, setNativeInputBridgeReady, + streamingGame, setStreamingGame, + streamingStore, setStreamingStore, + queuePosition, setQueuePosition, + navbarActiveSession, setNavbarActiveSession, + isResumingNavbarSession, setIsResumingNavbarSession, + isTerminatingNavbarSession, + launchError, setLaunchError, + pendingDirectLaunchRequest, setPendingDirectLaunchRequest, + directLaunchConsoleMode, setDirectLaunchConsoleMode, + queueModalGame, setQueueModalGame, + queueModalData, setQueueModalData, + sessionStartedAtMs, setSessionStartedAtMs, + remoteStreamWarning, setRemoteStreamWarning, + localSessionTimerWarning, setLocalSessionTimerWarning, + streamVolume, setStreamVolume, + videoElementHasFrame, setVideoElementHasFrame, + streamRevealPhase, setStreamRevealPhase, + videoRef, audioRef, clientRef, + previousFreeTierRemainingSecondsRef, + navbarSessionActionInFlightRef, + nativeStreamingRef, + handleStreamShortcutActionRef, + streamingGameRef, + isStreamingRef, + sessionRef, + launchInFlightRef, + directLaunchAttemptIdRef, + handledDirectLaunchIdsRef, + runtimeSnapshotRef, + claimResumePromisesRef, + launchAbortRef, + discordStreamingActivitySessionRef, + streamStatusRef, + nativeInputProtocolVersionRef, + awaitingRecoveryRemoteIceRef, + appUnloadingRef, + signalingRecoveryRef, + directLaunchSessionSeenRef, + } = streamRuntime; + const { + disconnectSignalingControlled, + isRecoveryGenerationCurrent, + markExplicitSignalingShutdown, + resetRecoveryConnectionState, + resetSignalingRecoveryState, + scheduleStableRecoveryReset, + } = recovery; + const { persistRuntimeSnapshotNow } = snapshot; - // Stream State - const [session, setSession] = useState(null); - const [streamStatus, setStreamStatus] = useState("idle"); - const [showStatsOverlay, setShowStatsOverlay] = useState(false); - const [antiAfkEnabled, setAntiAfkEnabled] = useState(false); - const [antiAfkAckNonce, setAntiAfkAckNonce] = useState(0); - const [nativeInputCaptureActive, setNativeInputCaptureActive] = useState(false); - const [nativeInputBridgeReady, setNativeInputBridgeReady] = useState(false); const [exitPrompt, setExitPrompt] = useState({ open: false, gameTitle: t("app.labels.game") }); - const [streamingGame, setStreamingGame] = useState(null); - const [streamingStore, setStreamingStore] = useState(null); - const [queuePosition, setQueuePosition] = useState(); - const [navbarActiveSession, setNavbarActiveSession] = useState(null); - const [isResumingNavbarSession, setIsResumingNavbarSession] = useState(false); - const [isTerminatingNavbarSession, setIsTerminatingNavbarSession] = useState(false); - const [accountToRemove, setAccountToRemove] = useState(null); - const [removeAccountConfirmOpen, setRemoveAccountConfirmOpen] = useState(false); - const [logoutConfirmOpen, setLogoutConfirmOpen] = useState(false); - const [launchError, setLaunchError] = useState(null); const [settingsFocusSection, setSettingsFocusSection] = useState<"account" | undefined>(); - const [pendingDirectLaunchRequest, setPendingDirectLaunchRequest] = useState(null); - // Argument-driven launches always use the console (big picture) experience for this run, - // without persisting the user's Controller Mode setting. - const [directLaunchConsoleMode, setDirectLaunchConsoleMode] = useState(false); - const [queueModalGame, setQueueModalGame] = useState(null); - const [queueModalData, setQueueModalData] = useState(null); - const [sessionStartedAtMs, setSessionStartedAtMs] = useState(null); - const [remoteStreamWarning, setRemoteStreamWarning] = useState(null); - const [localSessionTimerWarning, setLocalSessionTimerWarning] = useState(null); - const previousFreeTierRemainingSecondsRef = useRef(null); const { playtime, startSession: startPlaytimeSession, endSession: endPlaytimeSession } = usePlaytime(); const sessionElapsedSeconds = useElapsedSeconds(sessionStartedAtMs, streamStatus === "streaming"); const isStreaming = streamStatus === "streaming"; - const freeTierSessionWarningsActive = - isStreaming && sessionStartedAtMs !== null && shouldShowFreeTierSessionWarnings(subscriptionInfo); - const sessionLimitTier = useMemo(() => { - const subscriptionTier = normalizeMembershipTier(subscriptionInfo?.membershipTier); - const authTier = normalizeMembershipTier(authSession?.user.membershipTier); - return subscriptionTier ?? authTier; - }, [authSession?.user.membershipTier, subscriptionInfo?.membershipTier]); - const sessionLimitSeconds = getSessionLimitSecondsForTier(sessionLimitTier); - const sessionTimeRemainingSeconds = isStreaming && sessionStartedAtMs !== null && sessionLimitSeconds !== null - ? Math.max(0, sessionLimitSeconds - sessionElapsedSeconds) - : null; - const freeTierSessionRemainingSeconds = freeTierSessionWarningsActive - ? sessionTimeRemainingSeconds - : null; - const visibleLocalSessionTimerWarning = useMemo(() => { - if (localSessionTimerWarning === null || freeTierSessionRemainingSeconds === null) { - return null; - } + // freeTier/session-limit derived state is computed after auth/catalog hooks - return getLocalSessionTimerWarning(t, localSessionTimerWarning.stage, freeTierSessionRemainingSeconds); - }, [freeTierSessionRemainingSeconds, localSessionTimerWarning, locale, t]); - const streamWarning = useMemo(() => { - if (visibleLocalSessionTimerWarning?.tone === "critical") { - return visibleLocalSessionTimerWarning; - } - return remoteStreamWarning ?? visibleLocalSessionTimerWarning; - }, [remoteStreamWarning, visibleLocalSessionTimerWarning]); const codecTestPromiseRef = useRef | null>(null); const codecStartupTestAttemptedRef = useRef(false); - const navbarSessionActionInFlightRef = useRef<"resume" | "terminate" | null>(null); - const nativeStreamingRef = useRef(false); - const handleStreamShortcutActionRef = useRef<((action: NativeStreamerShortcutAction) => void) | null>(null); - const streamingGameRef = useRef(null); useEffect(() => { streamingGameRef.current = streamingGame; @@ -667,18 +264,91 @@ export function App(): JSX.Element { await testPromise; }, []); - const [streamVolume, setStreamVolume] = useState(1); - const [streamMicLevel, setStreamMicLevel] = useState(1); - // Refs - const videoRef = useRef(null); - const audioRef = useRef(null); - const clientRef = useRef(null); - const isStreamingRef = useRef(streamStatus === "streaming"); - + const accountConfirmRestoreFocusRef = useRef(null); + const logoutConfirmCancelRef = useRef(null); + const removeAccountConfirmCancelRef = useRef(null); + const [streamSurfacePresent, setStreamSurfacePresent] = useState(false); + const [launchSurfacePresent, setLaunchSurfacePresent] = useState(false); + const [settingsSurfacePresent, setSettingsSurfacePresent] = useState(false); + const [navbarOverlayBlocking, setNavbarOverlayBlocking] = useState(false); + const [logoutConfirmSurfacePresent, setLogoutConfirmSurfacePresent] = useState(false); + const [removeAccountConfirmSurfacePresent, setRemoveAccountConfirmSurfacePresent] = useState(false); + const [releaseHighlightsSurfacePresent, setReleaseHighlightsSurfacePresent] = useState(false); + const streamRevealComplete = streamRevealPhase === "revealed"; useEffect(() => { isStreamingRef.current = streamStatus === "streaming"; }, [streamStatus]); + useEffect(() => { + if (streamStatus !== "streaming") { + setVideoElementHasFrame(false); + setStreamRevealPhase("covered"); + } + + if (streamStatus === "idle") return undefined; + + const video = videoRef.current; + if (!video) return undefined; + + const syncVideoFrame = (): void => { + setVideoElementHasFrame(video.videoWidth > 0 && video.videoHeight > 0); + }; + + syncVideoFrame(); + video.addEventListener("loadeddata", syncVideoFrame); + video.addEventListener("playing", syncVideoFrame); + video.addEventListener("resize", syncVideoFrame); + return () => { + video.removeEventListener("loadeddata", syncVideoFrame); + video.removeEventListener("playing", syncVideoFrame); + video.removeEventListener("resize", syncVideoFrame); + }; + }, [streamStatus]); + + const streamVideoReady = isStreamVideoReady(streamStatus, diagnosticsVideoReady, videoElementHasFrame); + + useEffect(() => { + if (streamStatus === "idle" || !streamVideoReady || streamRevealPhase !== "covered") return; + setStreamRevealPhase(reducedMotion ? "revealed" : "revealing"); + }, [reducedMotion, streamRevealPhase, streamStatus, streamVideoReady]); + + useEffect(() => { + if (streamStatus !== "idle") setStreamSurfacePresent(true); + }, [streamStatus]); + + useEffect(() => { + if (streamStatus !== "idle" || launchError) setLaunchSurfacePresent(true); + }, [launchError, streamStatus]); + + useEffect(() => { + if (currentPage === "settings") setSettingsSurfacePresent(true); + }, [currentPage]); + + useEffect(() => { + if (releaseHighlightsPayload) setReleaseHighlightsSurfacePresent(true); + }, [releaseHighlightsPayload]); + + useEffect(() => { + if (feedbackOpen) setFeedbackSurfacePresent(true); + }, [feedbackOpen]); + + useEffect(() => { + if (settingsLoaded && settings.errorReportingConsent === "unset") { + setConsentSurfacePresent(true); + } + }, [settings.errorReportingConsent, settingsLoaded]); + + useEffect(() => { + if (!settingsLoaded) { + return; + } + void syncRendererTelemetry(settings).catch((error) => { + console.warn("[Telemetry] Failed to sync renderer PostHog:", error); + }); + // Only re-sync when consent or install identity changes — not on every settings keystroke. + // eslint-disable-next-line react-hooks/exhaustive-deps -- intentional narrow deps + }, [settings.errorReportingConsent, settings.telemetryInstallId, settingsLoaded]); + useEffect(() => { if (streamStatus === "streaming" && audioRef.current) { setStreamVolume(audioRef.current.volume); @@ -690,44 +360,185 @@ export function App(): JSX.Element { } clientRef.current?.setOutputVolume(streamVolume); }, [streamVolume]); - const sessionRef = useRef(null); - const hasInitializedRef = useRef(false); - const regionsRequestRef = useRef(0); - const launchInFlightRef = useRef(false); - const directLaunchAttemptIdRef = useRef(null); - const handledDirectLaunchIdsRef = useRef>(new Set()); - const runtimeSnapshotRef = useRef(loadRuntimeSnapshot()); - /** Joins concurrent claim/resume calls for the same Cloud session id (single CloudMatch RESUME + signaling). */ - const claimResumePromisesRef = useRef>>(new Map()); - const launchAbortRef = useRef(false); - const discordStreamingActivitySessionRef = useRef(null); - const streamStatusRef = useRef(streamStatus); - const nativeInputProtocolVersionRef = useRef(null); - const stableRecoveryResetTimerRef = useRef(null); - const remoteIceGraceTimerRef = useRef(null); - const remoteIceSeenForSessionRef = useRef(null); - const remoteIceRecoveryGenerationRef = useRef(null); - const awaitingRecoveryRemoteIceRef = useRef(false); - const appUnloadingRef = useRef(false); - const hasConfirmedRemoteIceRef = useRef(false); - const latestIceConnectionStateRef = useRef("new"); - const iceDisconnectedRecoveryTimerRef = useRef(null); - const pendingControlledDisconnectsRef = useRef(0); - const storePanelsLoadedContextRef = useRef(""); - const storePanelsLoadIdRef = useRef(0); - const runtimeDataLoadIdRef = useRef(0); - const lastCatalogQueryRef = useRef(null); - const lastCatalogProxyUrlRef = useRef(undefined); - const signalingRecoveryRef = useRef({ - attemptCount: 0, - inFlight: null, - explicitShutdown: false, - appId: null, - generation: 0, - }); const exitPromptResolverRef = useRef<((confirmed: boolean) => void) | null>(null); + + type CatalogOps = { + hydrateCatalogSnapshot: (session: AuthSession, proxyUrl?: string) => string | null; + loadSessionRuntimeData: (session: AuthSession, options?: { background?: boolean; proxyUrl?: string }) => Promise; + clearSessionCatalog: (mode: "logout" | "no-session", options?: { clearFeatured?: boolean }) => void; + resetStorePanels: () => void; + setVariantByGameId: (value: Record | ((prev: Record) => Record)) => void; + }; + const catalogOpsRef = useRef({ + hydrateCatalogSnapshot: () => null, + loadSessionRuntimeData: async () => {}, + clearSessionCatalog: () => {}, + resetStorePanels: () => {}, + setVariantByGameId: (() => {}) as CatalogOps['setVariantByGameId'], + }); + const resetLaunchRuntimeRef = useRef<(options?: { keepLaunchError?: boolean; keepStreamingContext?: boolean }) => void>(() => {}); + const refreshNavbarActiveSessionRef = useRef<(sessionOverride?: AuthSession) => Promise>(async () => {}); + + const onBootstrapSettings = useCallback((loadedSettings: Settings, _sessionProxyUrl: string | undefined) => { + setSettings(loadedSettings); + setShowStatsOverlay(loadedSettings.showStatsOnLaunch); + setSettingsLoaded(true); + }, []); + + const onBootstrapVariantSelections = useCallback((selections: Record) => { + catalogOpsRef.current.setVariantByGameId(selections); + }, []); + + const onBootstrapRuntimeSnapshot = useCallback((snapshot: RuntimeSnapshot | null) => { + runtimeSnapshotRef.current = snapshot; + if (snapshot?.recoveryAppId !== null && snapshot?.recoveryAppId !== undefined) { + signalingRecoveryRef.current.appId = snapshot.recoveryAppId; + } + }, []); + + const { + authSession, + savedAccounts, + providers, + providerIdpId, + setProviderIdpId, + isLoggingIn, + activeLoginMode, + loginError, + setLoginError, + qrLoginChallenge, + isInitializing, + startupStatusMessage, + startupRefreshNotice, + removeAccountConfirmOpen, + setRemoveAccountConfirmOpen, + logoutConfirmOpen, + setLogoutConfirmOpen, + selectedProvider, + handleLogin, + handleQrLogin, + handleCancelQrLogin, + handleSwitchAccount, + handleRemoveAccount, + confirmRemoveAccount, + handleAddAccount, + confirmLogout, + handleLogout, + accountToRemoveDisplayName, + setAccountToRemove, + } = useAuthSession({ + t, + loadSessionRuntimeData: (session, options) => catalogOpsRef.current.loadSessionRuntimeData(session, options), + hydrateCatalogSnapshot: (session, proxyUrl) => catalogOpsRef.current.hydrateCatalogSnapshot(session, proxyUrl), + clearSessionCatalog: (mode, options) => catalogOpsRef.current.clearSessionCatalog(mode, options), + resetLaunchRuntime: (options) => resetLaunchRuntimeRef.current(options), + refreshNavbarActiveSession: (sessionOverride) => refreshNavbarActiveSessionRef.current(sessionOverride), + onBootstrapSettings, + onBootstrapVariantSelections, + onBootstrapRuntimeSnapshot, + setCurrentPage, + setNavbarActiveSession, + setIsResumingNavbarSession, + }); + + useEffect(() => { + if (logoutConfirmOpen) setLogoutConfirmSurfacePresent(true); + }, [logoutConfirmOpen]); + + useEffect(() => { + if (removeAccountConfirmOpen) setRemoveAccountConfirmSurfacePresent(true); + }, [removeAccountConfirmOpen]); + + const effectiveControllerModeForCatalog = settings.controllerMode || directLaunchConsoleMode; + const effectiveStreamingBaseUrlForCatalog = settings.region.trim() + ? settings.region + : (selectedProvider?.streamingServiceUrl ?? ""); + + const { + games, + featuredGames, + storePanels, + libraryGames, + searchQuery, + setSearchQuery, + selectedGameId, + setSelectedGameId, + variantByGameId, + setVariantByGameId, + isLoadingCatalog, + isLoadingLibrary, + isLoadingStorePanels, + catalogFilterGroups, + catalogSortOptions, + catalogSelectedSortId, + setCatalogSelectedSortId, + catalogSelectedFilterIds, + catalogTotalCount, + catalogSupportedCount, + markOwnedInFlightByVariantId, + catalogActionNotice, + regions, + setRegions, + subscriptionInfo, + setSubscriptionInfo, + storePanelGames, + allKnownGames, + resetStorePanels, + hydrateCatalogSnapshot, + loadSessionRuntimeData, + clearSessionCatalog, + handleMarkGameOwned, + handleSelectGameVariant, + handleToggleCatalogFilter, + loadSubscriptionInfo, + } = useCatalogData({ + authSession, + activeSessionProxyUrl, + effectiveStreamingBaseUrl: effectiveStreamingBaseUrlForCatalog, + currentPage, + effectiveControllerMode: effectiveControllerModeForCatalog, + isInitializing, + t, + }); + + catalogOpsRef.current = { + hydrateCatalogSnapshot, + loadSessionRuntimeData, + clearSessionCatalog, + resetStorePanels, + setVariantByGameId, + }; + + const freeTierSessionWarningsActive = + isStreaming && sessionStartedAtMs !== null && shouldShowFreeTierSessionWarnings(subscriptionInfo); + const sessionLimitTier = useMemo(() => { + const subscriptionTier = normalizeMembershipTier(subscriptionInfo?.membershipTier); + const authTier = normalizeMembershipTier(authSession?.user.membershipTier); + return subscriptionTier ?? authTier; + }, [authSession?.user.membershipTier, subscriptionInfo?.membershipTier]); + const sessionLimitSeconds = getSessionLimitSecondsForTier(sessionLimitTier); + const sessionTimeRemainingSeconds = isStreaming && sessionStartedAtMs !== null && sessionLimitSeconds !== null + ? Math.max(0, sessionLimitSeconds - sessionElapsedSeconds) + : null; + const freeTierSessionRemainingSeconds = freeTierSessionWarningsActive + ? sessionTimeRemainingSeconds + : null; + const visibleLocalSessionTimerWarning = useMemo(() => { + if (localSessionTimerWarning === null || freeTierSessionRemainingSeconds === null) { + return null; + } + + return getLocalSessionTimerWarning(t, localSessionTimerWarning.stage, freeTierSessionRemainingSeconds); + }, [freeTierSessionRemainingSeconds, localSessionTimerWarning, locale, t]); + const streamWarning = useMemo(() => { + if (visibleLocalSessionTimerWarning?.tone === "critical") { + return visibleLocalSessionTimerWarning; + } + return remoteStreamWarning ?? visibleLocalSessionTimerWarning; + }, [remoteStreamWarning, visibleLocalSessionTimerWarning]); + const queueDirectLaunchRequest = useCallback((request: DirectLaunchRequest | null): void => { if (!request || handledDirectLaunchIdsRef.current.has(request.id)) return; setDirectLaunchConsoleMode(true); @@ -753,40 +564,11 @@ export function App(): JSX.Element { return unsubscribe; }, []); - const resetStorePanels = useCallback((): void => { - storePanelsLoadIdRef.current += 1; - storePanelsLoadedContextRef.current = ""; - setStorePanels([]); - setIsLoadingStorePanels(false); - }, []); - - - const applyVariantSelections = useCallback((catalog: GameInfo[]): void => { - setVariantByGameId((prev) => mergeVariantSelections(prev, catalog)); - }, []); - const resetLaunchRuntime = useCallback((options?: { keepLaunchError?: boolean; keepStreamingContext?: boolean; }): void => { - if (stableRecoveryResetTimerRef.current !== null) { - window.clearTimeout(stableRecoveryResetTimerRef.current); - stableRecoveryResetTimerRef.current = null; - } - if (remoteIceGraceTimerRef.current !== null) { - window.clearTimeout(remoteIceGraceTimerRef.current); - remoteIceGraceTimerRef.current = null; - } - if (iceDisconnectedRecoveryTimerRef.current !== null) { - window.clearTimeout(iceDisconnectedRecoveryTimerRef.current); - iceDisconnectedRecoveryTimerRef.current = null; - } - remoteIceSeenForSessionRef.current = null; - remoteIceRecoveryGenerationRef.current = null; - awaitingRecoveryRemoteIceRef.current = false; - hasConfirmedRemoteIceRef.current = false; - latestIceConnectionStateRef.current = "new"; - pendingControlledDisconnectsRef.current = 0; + resetRecoveryConnectionState(); discordStreamingActivitySessionRef.current = null; signalingRecoveryRef.current.attemptCount = 0; signalingRecoveryRef.current.inFlight = null; @@ -799,6 +581,7 @@ export function App(): JSX.Element { setLocalSessionTimerWarning(null); resetStatsOverlayToPreference(); nativeStreamingRef.current = false; + window.openNow.notifyNativeInputModeChange(false, false); diagnosticsStore.set(defaultDiagnostics()); if (!options?.keepStreamingContext) { @@ -816,7 +599,9 @@ export function App(): JSX.Element { } runtimeSnapshotRef.current = null; clearRuntimeSnapshot(); - }, [diagnosticsStore, resetStatsOverlayToPreference, settings.discordRichPresence]); + }, [diagnosticsStore, resetRecoveryConnectionState, resetStatsOverlayToPreference, settings.discordRichPresence]); + + resetLaunchRuntimeRef.current = resetLaunchRuntime; const markDiscordStreamStarted = useCallback((): void => { if (!settings.discordRichPresence) { @@ -829,9 +614,13 @@ export function App(): JSX.Element { } const gameName = (streamingGameRef.current?.title || activeSession.appId || "Game").trim(); + const gameImageUrl = streamingGameRef.current + ? discordGameImageUrl(streamingGameRef.current) + : undefined; discordStreamingActivitySessionRef.current = activeSession.sessionId; void window.openNow.setDiscordActivity({ gameName, + gameImageUrl, kind: "streaming", appId: activeSession.appId, startTimestampMs: Date.now(), @@ -862,6 +651,7 @@ export function App(): JSX.Element { enableCloudGsync: settings.enableCloudGsync, clientMode: settings.streamClientMode, nativeStreamerBackend: "gstreamer", + transportMode: "webrtc", nativeCloudGsyncMode: settings.nativeCloudGsyncMode, nativeTransitionDiagnostics: settings.nativeTransitionDiagnostics, appLaunchMode: @@ -906,238 +696,11 @@ export function App(): JSX.Element { }); }, [settings.streamClientMode]); - const resetSignalingRecoveryState = useCallback((options?: { - keepExplicitShutdown?: boolean; - }): void => { - if (stableRecoveryResetTimerRef.current !== null) { - window.clearTimeout(stableRecoveryResetTimerRef.current); - stableRecoveryResetTimerRef.current = null; - } - if (remoteIceGraceTimerRef.current !== null) { - window.clearTimeout(remoteIceGraceTimerRef.current); - remoteIceGraceTimerRef.current = null; - } - if (iceDisconnectedRecoveryTimerRef.current !== null) { - window.clearTimeout(iceDisconnectedRecoveryTimerRef.current); - iceDisconnectedRecoveryTimerRef.current = null; - } - remoteIceSeenForSessionRef.current = null; - remoteIceRecoveryGenerationRef.current = null; - awaitingRecoveryRemoteIceRef.current = false; - hasConfirmedRemoteIceRef.current = false; - latestIceConnectionStateRef.current = "new"; - pendingControlledDisconnectsRef.current = 0; - signalingRecoveryRef.current.generation += 1; - signalingRecoveryRef.current.attemptCount = 0; - signalingRecoveryRef.current.inFlight = null; - signalingRecoveryRef.current.appId = null; - if (!options?.keepExplicitShutdown) { - signalingRecoveryRef.current.explicitShutdown = false; - } - }, []); + // Derived state - const markExplicitSignalingShutdown = useCallback((): void => { - if (stableRecoveryResetTimerRef.current !== null) { - window.clearTimeout(stableRecoveryResetTimerRef.current); - stableRecoveryResetTimerRef.current = null; - } - if (remoteIceGraceTimerRef.current !== null) { - window.clearTimeout(remoteIceGraceTimerRef.current); - remoteIceGraceTimerRef.current = null; - } - if (iceDisconnectedRecoveryTimerRef.current !== null) { - window.clearTimeout(iceDisconnectedRecoveryTimerRef.current); - iceDisconnectedRecoveryTimerRef.current = null; - } - remoteIceSeenForSessionRef.current = null; - remoteIceRecoveryGenerationRef.current = null; - awaitingRecoveryRemoteIceRef.current = false; - hasConfirmedRemoteIceRef.current = false; - latestIceConnectionStateRef.current = "new"; - pendingControlledDisconnectsRef.current = 0; - signalingRecoveryRef.current.generation += 1; - signalingRecoveryRef.current.explicitShutdown = true; - signalingRecoveryRef.current.inFlight = null; - }, []); - - const isRecoveryGenerationCurrent = useCallback((generation: number): boolean => { - const state = signalingRecoveryRef.current; - return state.generation === generation && !state.explicitShutdown; - }, []); - - const scheduleStableRecoveryReset = useCallback((sessionId: string): void => { - if (stableRecoveryResetTimerRef.current !== null) { - window.clearTimeout(stableRecoveryResetTimerRef.current); - stableRecoveryResetTimerRef.current = null; - } - - stableRecoveryResetTimerRef.current = window.setTimeout(() => { - stableRecoveryResetTimerRef.current = null; - const activeSessionId = sessionRef.current?.sessionId; - if ( - streamStatusRef.current !== "streaming" - || !activeSessionId - || activeSessionId !== sessionId - ) { - return; - } - console.log( - `[Recovery] Stream remained stable for ${SIGNALING_RECOVERY_STABLE_RESET_DELAY_MS}ms; resetting recovery budget`, - ); - resetSignalingRecoveryState({ keepExplicitShutdown: true }); - }, SIGNALING_RECOVERY_STABLE_RESET_DELAY_MS); - }, [resetSignalingRecoveryState]); - - const disconnectSignalingControlled = useCallback(async (): Promise => { - pendingControlledDisconnectsRef.current += 1; - await window.openNow.disconnectSignaling().catch(() => {}); - }, []); - - // Session ref sync - useEffect(() => { - sessionRef.current = session; - }, [session]); - - useEffect(() => { - const streamIsActive = streamStatus !== "idle" || session !== null || navbarActiveSession !== null; - if (!streamIsActive) { - runtimeSnapshotRef.current = null; - clearRuntimeSnapshot(); - return; - } - - const snapshot: RuntimeSnapshot = { - version: 1, - updatedAt: Date.now(), - streamStatus, - sessionId: session?.sessionId ?? navbarActiveSession?.sessionId ?? null, - sessionAppId: - (Number.isFinite(signalingRecoveryRef.current.appId ?? NaN) ? signalingRecoveryRef.current.appId : null) ?? - (navbarActiveSession ? navbarActiveSession.appId : null), - streamingGameId: streamingGame?.id ?? null, - streamingStore: streamingStore ?? null, - recoveryAppId: signalingRecoveryRef.current.appId, - resumeContext: session - ? { - sessionId: session.sessionId, - serverIp: session.serverIp, - streamingBaseUrl: session.streamingBaseUrl, - signalingServer: session.signalingServer, - signalingUrl: session.signalingUrl, - appId: Number.isFinite(signalingRecoveryRef.current.appId ?? NaN) ? signalingRecoveryRef.current.appId ?? undefined : undefined, - appLaunchMode: session.appLaunchMode, - enablePersistingInGameSettings: session.enablePersistingInGameSettings, - clientId: session.clientId, - deviceId: session.deviceId, - } - : (navbarActiveSession?.sessionId && navbarActiveSession.serverIp) - ? { - sessionId: navbarActiveSession.sessionId, - serverIp: navbarActiveSession.serverIp, - streamingBaseUrl: navbarActiveSession.streamingBaseUrl, - signalingUrl: navbarActiveSession.signalingUrl, - appId: Number.isFinite(navbarActiveSession.appId) ? navbarActiveSession.appId : undefined, - appLaunchMode: navbarActiveSession.appLaunchMode, - enablePersistingInGameSettings: navbarActiveSession.enablePersistingInGameSettings, - } - : null, - }; - - runtimeSnapshotRef.current = snapshot; - saveRuntimeSnapshot(snapshot); - }, [navbarActiveSession, session, streamStatus, streamingGame?.id, streamingStore]); - - const persistRuntimeSnapshotNow = useCallback((): void => { - const latestSession = sessionRef.current; - const latestNavbarSession = navbarActiveSession; - const hasActiveContext = - streamStatusRef.current !== "idle" || latestSession !== null || latestNavbarSession !== null; - if (!hasActiveContext) { - runtimeSnapshotRef.current = null; - clearRuntimeSnapshot(); - return; - } - - const snapshot: RuntimeSnapshot = { - version: 1, - updatedAt: Date.now(), - streamStatus: streamStatusRef.current, - sessionId: latestSession?.sessionId ?? latestNavbarSession?.sessionId ?? null, - sessionAppId: - (Number.isFinite(signalingRecoveryRef.current.appId ?? NaN) ? signalingRecoveryRef.current.appId : null) ?? - (latestNavbarSession ? latestNavbarSession.appId : null), - streamingGameId: streamingGame?.id ?? null, - streamingStore: streamingStore ?? null, - recoveryAppId: signalingRecoveryRef.current.appId, - resumeContext: latestSession - ? { - sessionId: latestSession.sessionId, - serverIp: latestSession.serverIp, - streamingBaseUrl: latestSession.streamingBaseUrl, - signalingServer: latestSession.signalingServer, - signalingUrl: latestSession.signalingUrl, - appId: Number.isFinite(signalingRecoveryRef.current.appId ?? NaN) ? signalingRecoveryRef.current.appId ?? undefined : undefined, - appLaunchMode: latestSession.appLaunchMode, - enablePersistingInGameSettings: latestSession.enablePersistingInGameSettings, - clientId: latestSession.clientId, - deviceId: latestSession.deviceId, - } - : (latestNavbarSession?.sessionId && latestNavbarSession.serverIp) - ? { - sessionId: latestNavbarSession.sessionId, - serverIp: latestNavbarSession.serverIp, - streamingBaseUrl: latestNavbarSession.streamingBaseUrl, - signalingUrl: latestNavbarSession.signalingUrl, - appId: Number.isFinite(latestNavbarSession.appId) ? latestNavbarSession.appId : undefined, - appLaunchMode: latestNavbarSession.appLaunchMode, - enablePersistingInGameSettings: latestNavbarSession.enablePersistingInGameSettings, - } - : null, - }; - - runtimeSnapshotRef.current = snapshot; - saveRuntimeSnapshot(snapshot); - }, [navbarActiveSession, streamingGame?.id, streamingStore]); - - useEffect(() => { - const onBeforeUnload = (): void => { - appUnloadingRef.current = true; - persistRuntimeSnapshotNow(); - }; - window.addEventListener("beforeunload", onBeforeUnload); - return () => window.removeEventListener("beforeunload", onBeforeUnload); - }, [persistRuntimeSnapshotNow]); - - // Keep a ref copy of `streamStatus` so async callbacks can observe latest value - useEffect(() => { - streamStatusRef.current = streamStatus; - }, [streamStatus]); - - // Broadcast minimal session/loading state for UI listeners. - useEffect(() => { - const detail = { - status: streamStatus, - queuePosition, - launchError: launchError ? { title: launchError.title, description: launchError.description, stage: launchError.stage, codeLabel: launchError.codeLabel } : null, - gameTitle: streamingGame?.title ?? null, - gameCover: streamingGame?.imageUrl ?? null, - platformStore: streamingStore ?? null, - }; - try { - window.dispatchEvent(new CustomEvent("opennow:session-update", { detail })); - } catch { - // ignore - } - }, [streamStatus, queuePosition, launchError, streamingGame, streamingStore]); - - // Derived state - const selectedProvider = useMemo(() => { - return providers.find((p) => p.idpId === providerIdpId) ?? authSession?.provider ?? null; - }, [providers, providerIdpId, authSession]); - - const effectiveStreamingBaseUrl = useMemo(() => { - if (settings.region.trim()) { - return settings.region; + const effectiveStreamingBaseUrl = useMemo(() => { + if (settings.region.trim()) { + return settings.region; } return selectedProvider?.streamingServiceUrl ?? ""; }, [selectedProvider, settings.region]); @@ -1217,24 +780,6 @@ export function App(): JSX.Element { t, }); - const loadSubscriptionInfo = useCallback( - async (session: AuthSession): Promise => { - const token = session.tokens.idToken ?? session.tokens.accessToken; - const subscription = await window.openNow.fetchSubscription({ - token, - providerStreamingBaseUrl: session.provider.streamingServiceUrl, - userId: session.user.userId, - }); - setSubscriptionInfo(subscription); - }, - [], - ); - - const refreshSavedAccounts = useCallback(async (): Promise => { - const accounts = await window.openNow.getSavedAccounts(); - setSavedAccounts(accounts); - return accounts; - }, []); const refreshNavbarActiveSession = useCallback(async ( sessionOverride?: AuthSession, @@ -1271,8 +816,7 @@ export function App(): JSX.Element { } }, [authSession, effectiveStreamingBaseUrl, settings.region]); - const storePanelGames = useMemo(() => flattenStorePanelGames(storePanels), [storePanels]); - const allKnownGames = useMemo(() => [...games, ...libraryGames, ...storePanelGames], [games, libraryGames, storePanelGames]); + refreshNavbarActiveSessionRef.current = refreshNavbarActiveSession; const gameTitleByAppId = useMemo(() => { const titles = new Map(); @@ -1326,12 +870,6 @@ export function App(): JSX.Element { return true; }, [authSession]); - useEffect(() => { - if (!startupRefreshNotice) return; - const timer = window.setTimeout(() => setStartupRefreshNotice(null), 7000); - return () => window.clearTimeout(timer); - }, [startupRefreshNotice]); - useEffect(() => { if (!authSession || streamStatus !== "idle") { return; @@ -1347,9 +885,6 @@ export function App(): JSX.Element { saveStoredCodecResults(codecResults); }, [codecResults]); - useEffect(() => { - saveCatalogPreferences({ sortId: catalogSelectedSortId, filterIds: catalogSelectedFilterIds }); - }, [catalogSelectedSortId, catalogSelectedFilterIds]); useEffect(() => { if (codecResults || codecTesting || codecStartupTestAttemptedRef.current) { @@ -1421,18 +956,8 @@ export function App(): JSX.Element { if (canUseNativeFullscreen) { try { - if (nextFullscreen) { - if (!document.fullscreenElement) { - await document.documentElement.requestFullscreen(); - } - } else if (document.fullscreenElement) { - await document.exitFullscreen(); - } - } catch (error) { - console.warn(`Failed to set DOM fullscreen state (${nextFullscreen ? "enter" : "exit"}):`, error); - } - - try { + // Electron owns desktop fullscreen. Stacking HTML fullscreen on top lets + // Chromium force-exit the DOM layer on Escape before stream input runs. await window.openNow.setFullscreen(nextFullscreen); setSessionFullscreenState(nextFullscreen); } catch (error) { @@ -1554,6 +1079,14 @@ export function App(): JSX.Element { return () => unsubscribe(); }, [toggleSessionFullscreen]); + // Escape-hold (and other explicit exit requests) from main — never toggle back on. + useEffect(() => { + const unsubscribe = window.openNow.onExitFullscreen(() => { + void setSessionFullscreen(false); + }); + return () => unsubscribe(); + }, [setSessionFullscreen]); + const autoFullscreenRequestedRef = useRef(false); useEffect(() => { @@ -1713,21 +1246,11 @@ export function App(): JSX.Element { applyTranslucentUI(settings.translucentUI); }, [settings.translucentUI]); - useEffect(() => { - if (!catalogActionNotice) return; - const timer = window.setTimeout(() => { - setCatalogActionNotice((current) => (current === catalogActionNotice ? null : current)); - }, 4500); - return () => window.clearTimeout(timer); - }, [catalogActionNotice]); - // Save settings when changed - const updateSetting = useCallback(async (key: K, value: Settings[K]) => { - setSettings((prev) => ({ ...prev, [key]: value })); - if (settingsLoaded) { - await window.openNow.setSetting(key, value); - } - // If a running client exists, push certain settings live + const previewSetting = useCallback((key: K, value: Settings[K]): void => { + setSettings((prev) => (Object.is(prev[key], value) ? prev : { ...prev, [key]: value })); + + // If a running client exists, push supported settings live while controls move. if (key === "mouseSensitivity") { try { (clientRef.current as any)?.setMouseSensitivity?.(value as number); @@ -1770,7 +1293,21 @@ export function App(): JSX.Element { // ignore } } - }, [settingsLoaded]); + }, []); + + const updateSetting = useCallback(async (key: K, value: Settings[K]): Promise => { + previewSetting(key, value); + if (settingsLoaded) { + await window.openNow.setSetting(key, value); + } + if (key === "identifyAsSteamDeck" && authSession) { + try { + await loadSubscriptionInfo(authSession); + } catch (error) { + console.warn("Failed to refresh subscription after Steam Deck identity change:", error); + } + } + }, [authSession, loadSubscriptionInfo, previewSetting, settingsLoaded]); useEffect(() => { if (!settingsLoaded || !subscriptionInfo) { @@ -1825,7 +1362,6 @@ export function App(): JSX.Element { // Argument-driven (direct) launches behave like a console frontend session: // when the streamed session ends cleanly, close OpenNOW and return to the caller. - const directLaunchSessionSeenRef = useRef(false); useEffect(() => { if (!directLaunchConsoleMode) return; if (streamStatus !== "idle") { @@ -1845,894 +1381,148 @@ export function App(): JSX.Element { }); }, [updateSetting]); - const applyCatalogBrowseResult = useCallback((catalogResult: CatalogBrowseResult): void => { - setGames(catalogResult.games); - setCatalogFilterGroups(catalogResult.filterGroups); - setCatalogSortOptions(catalogResult.sortOptions); - setCatalogSelectedSortId((previous) => previous === catalogResult.selectedSortId ? previous : catalogResult.selectedSortId); - setCatalogSelectedFilterIds((previous) => areStringArraysEqual(previous, catalogResult.selectedFilterIds) ? previous : catalogResult.selectedFilterIds); - setCatalogTotalCount(catalogResult.totalCount); - setCatalogSupportedCount(catalogResult.numberSupported); - setSelectedGameId((previous) => catalogResult.games.some((game) => game.id === previous) ? previous : (catalogResult.games[0]?.id ?? "")); - applyVariantSelections(catalogResult.games); - }, [applyVariantSelections]); - - const persistCatalogSnapshot = useCallback(( - session: AuthSession, - catalogResult: CatalogBrowseResult, - library: GameInfo[], - queryKey: string, - proxyUrl?: string, - ): void => { - if (hasSessionProxyCredentials(proxyUrl)) { - clearCatalogSnapshot(); - return; + const resolveSessionClaimAppId = useCallback((existingSession: ActiveSessionInfo): string => { + const trackedAppId = signalingRecoveryRef.current.appId; + const persistedAppId = runtimeSnapshotRef.current?.sessionAppId ?? runtimeSnapshotRef.current?.recoveryAppId; + if (Number.isFinite(existingSession.appId) && existingSession.appId > 0) { + return String(existingSession.appId); } - - saveCatalogSnapshot({ - version: 1, - userId: session.user.userId, - streamingBaseUrl: session.provider.streamingServiceUrl, - queryKey, - games: catalogResult.games, - libraryGames: library, - filterGroups: catalogResult.filterGroups, - sortOptions: catalogResult.sortOptions, - totalCount: catalogResult.totalCount, - supportedCount: catalogResult.numberSupported, - savedAt: Date.now(), - }); + if (trackedAppId && Number.isFinite(trackedAppId)) { + return String(trackedAppId); + } + if (persistedAppId && Number.isFinite(persistedAppId)) { + return String(persistedAppId); + } + throw new Error("Active session is missing app metadata required for resume."); }, []); - const hydrateCatalogSnapshot = useCallback((session: AuthSession, proxyUrl: string | undefined = activeSessionProxyUrl): string | null => { - if (hasSessionProxyCredentials(proxyUrl)) { - clearCatalogSnapshot(); - return null; + const resolveResumeIdentity = useCallback((sessionId: string): { clientId?: string; deviceId?: string } => { + const liveSession = sessionRef.current; + if (liveSession?.sessionId === sessionId) { + return { + clientId: liveSession.clientId, + deviceId: liveSession.deviceId, + }; } - - const queryKey = buildProxyAwareCatalogQueryKey("", catalogSelectedFilterIds, catalogSelectedSortId, proxyUrl); - const snapshot = loadCatalogSnapshot( - session.user.userId, - session.provider.streamingServiceUrl, - queryKey, - ); - if (!snapshot) { - return null; + const persisted = runtimeSnapshotRef.current?.resumeContext; + if (persisted?.sessionId === sessionId) { + return { + clientId: persisted.clientId, + deviceId: persisted.deviceId, + }; } + return {}; + }, []); - setGames(snapshot.games); - setLibraryGames(snapshot.libraryGames); - setCatalogFilterGroups(snapshot.filterGroups); - setCatalogSortOptions(snapshot.sortOptions); - setCatalogTotalCount(snapshot.totalCount); - setCatalogSupportedCount(snapshot.supportedCount); - setSelectedGameId((previous) => ( - snapshot.games.some((game) => game.id === previous) ? previous : (snapshot.games[0]?.id ?? "") - )); - applyVariantSelections([...snapshot.games, ...snapshot.libraryGames]); - lastCatalogQueryRef.current = queryKey; - lastCatalogProxyUrlRef.current = proxyUrl; - return queryKey; - }, [activeSessionProxyUrl, applyVariantSelections, catalogSelectedFilterIds, catalogSelectedSortId]); - - const loadSessionRuntimeData = useCallback(async ( - session: AuthSession, - options?: { background?: boolean; proxyUrl?: string }, + const applyClaimedSessionAndConnect = useCallback(async ( + claimed: SessionInfo, + expectedRecoveryGeneration?: number, ): Promise => { - const token = session.tokens.idToken ?? session.tokens.accessToken; - const streamingBaseUrl = session.provider.streamingServiceUrl; - const userId = session.user.userId; - const loadId = ++runtimeDataLoadIdRef.current; - const isCurrentLoad = (): boolean => runtimeDataLoadIdRef.current === loadId; - const background = options?.background === true; - const proxyUrl = options?.proxyUrl ?? activeSessionProxyUrl; - const catalogQueryKey = buildProxyAwareCatalogQueryKey("", catalogSelectedFilterIds, catalogSelectedSortId, proxyUrl); - - if (!background) { - lastCatalogQueryRef.current = null; - lastCatalogProxyUrlRef.current = proxyUrl; - setIsLoadingCatalog(true); - setIsLoadingLibrary(true); - } + const canProceedWithClaimedReconnect = (): boolean => { + if ( + expectedRecoveryGeneration !== undefined + && !isRecoveryGenerationCurrent(expectedRecoveryGeneration) + ) { + return false; + } + if (signalingRecoveryRef.current.explicitShutdown) { + return false; + } + return true; + }; - void window.openNow.getRegions({ token }).then((discovered) => { - if (isCurrentLoad()) setRegions(discovered); - }).catch((error) => { - console.warn("Failed to load regions:", error); - if (isCurrentLoad()) setRegions([]); - }); + if ( + expectedRecoveryGeneration !== undefined + && !isRecoveryGenerationCurrent(expectedRecoveryGeneration) + ) { + console.log("[Recovery] Skipping claimed session apply due to stale recovery generation"); + return; + } - void window.openNow.fetchSubscription({ - token, - providerStreamingBaseUrl: streamingBaseUrl, - userId: session.user.userId, - }).then((subscription) => { - if (isCurrentLoad()) setSubscriptionInfo(subscription); - }).catch((error) => { - console.warn("Failed to load subscription info:", error); - if (isCurrentLoad()) setSubscriptionInfo(null); + console.log("Claimed session:", { + sessionId: claimed.sessionId, + signalingServer: claimed.signalingServer, + signalingUrl: claimed.signalingUrl, + status: claimed.status, }); - let latestCatalogResult: CatalogBrowseResult | null = null; - let latestLibraryGames: GameInfo[] | null = null; - - void window.openNow.browseCatalog({ - token, - userId, - providerStreamingBaseUrl: streamingBaseUrl, - proxyUrl, - searchQuery: "", - sortId: catalogSelectedSortId, - filterIds: catalogSelectedFilterIds, - }).then((catalogResult) => { - if (!isCurrentLoad()) return; - latestCatalogResult = catalogResult; - applyCatalogBrowseResult(catalogResult); - lastCatalogQueryRef.current = catalogQueryKey; - lastCatalogProxyUrlRef.current = proxyUrl; - if (latestLibraryGames) { - persistCatalogSnapshot(session, catalogResult, latestLibraryGames, catalogQueryKey, proxyUrl); - } - }).catch((error) => { - console.error("Catalog load failed:", error); - if (!isCurrentLoad() || background) return; - setGames([]); - setCatalogFilterGroups([]); - setCatalogSortOptions([]); - setCatalogTotalCount(0); - setCatalogSupportedCount(0); - }).finally(() => { - if (isCurrentLoad() && !background) setIsLoadingCatalog(false); - }); + await sleep(1000); + if ( + expectedRecoveryGeneration !== undefined + && !isRecoveryGenerationCurrent(expectedRecoveryGeneration) + ) { + console.log("[Recovery] Skipping reconnect due to stale recovery generation after delay"); + return; + } + if (!canProceedWithClaimedReconnect()) { + console.log("[Recovery] Skipping claimed session apply due to explicit shutdown"); + return; + } - void window.openNow.fetchLibraryGames({ - token, - userId, - providerStreamingBaseUrl: streamingBaseUrl, - proxyUrl, - }).then((libGames) => { - if (!isCurrentLoad()) return; - latestLibraryGames = libGames; - setLibraryGames(libGames); - applyVariantSelections(libGames); - if (latestCatalogResult) { - persistCatalogSnapshot(session, latestCatalogResult, libGames, catalogQueryKey, proxyUrl); - } - }).catch((error) => { - console.error("Library load failed:", error); - if (!isCurrentLoad() || background) return; - setLibraryGames([]); - }).finally(() => { - if (isCurrentLoad() && !background) setIsLoadingLibrary(false); + // Mirror attemptSessionRecovery: tear down WebRTC + signaling before connecting to a new edge. + // Avoids stale PeerConnection/video vs migrated CloudMatch connectionInfo (intermittent black screen on resume). + const reconnectSource = expectedRecoveryGeneration !== undefined ? "recovery" : "resume"; + console.log(`[Stream] ${reconnectSource}: teardown WebRTC + signaling before reconnect`, { + sessionId: claimed.sessionId, + signalingServer: claimed.signalingServer, + signalingUrl: claimed.signalingUrl, + mediaConnectionInfo: claimed.mediaConnectionInfo, }); + clientRef.current?.dispose(); + clientRef.current = null; + await disconnectSignalingControlled(); + awaitingRecoveryRemoteIceRef.current = expectedRecoveryGeneration !== undefined; - void window.openNow.fetchFeaturedGames({ - token, - userId, - providerStreamingBaseUrl: streamingBaseUrl, - proxyUrl, - }).then((featured) => { - if (isCurrentLoad()) setFeaturedGames(featured); - }).catch((error) => { - console.warn("Featured games load failed:", error); - if (isCurrentLoad()) setFeaturedGames([]); - }); - }, [ - activeSessionProxyUrl, - applyCatalogBrowseResult, - applyVariantSelections, - catalogSelectedFilterIds, - catalogSelectedSortId, - persistCatalogSnapshot, - ]); + setSession(claimed); + sessionRef.current = claimed; + nativeInputProtocolVersionRef.current = null; + setNativeInputBridgeReady(false); + setNativeInputCaptureActive(false); + try { + window.openNow.notifyPointerLockChange(false, true); + } catch { + /* best-effort */ + } + setQueuePosition(undefined); + setLaunchError(null); + setStreamStatus("connecting"); + await window.openNow.connectSignaling(buildSignalingConnectRequest(claimed)); + }, [buildSignalingConnectRequest, disconnectSignalingControlled, isRecoveryGenerationCurrent]); - // Initialize app - useEffect(() => { - if (hasInitializedRef.current) return; - hasInitializedRef.current = true; + const claimAndConnectSession = useCallback(async (existingSession: ActiveSessionInfo): Promise => { + const sid = existingSession.sessionId; + const inflight = claimResumePromisesRef.current.get(sid); + if (inflight) { + console.log("[Resume] claimAndConnectSession: deduped — joining in-flight claim for session", sid); + await inflight; + return; + } - const initialize = async () => { + const resumePromiseHolder: { promise?: Promise } = {}; + resumePromiseHolder.promise = (async (): Promise => { try { - // Load settings first - const loadedSettings = await window.openNow.getSettings(); - setSettings(loadedSettings); - setShowStatsOverlay(loadedSettings.showStatsOnLaunch); - setSettingsLoaded(true); - const loadedSessionProxyUrl = getEnabledSessionProxyUrl(loadedSettings); - - // Load providers and session (refresh only if token is near expiry) - setStartupStatusMessage(t("auth.status.restoringSavedSession")); - const [providerList, sessionResult] = await Promise.all([ - window.openNow.getLoginProviders(), - window.openNow.getAuthSession(), - ]); - const accounts = await window.openNow.getSavedAccounts(); - const persistedSession = sessionResult.session; - - if (sessionResult.refresh.outcome === "refreshed") { - setStartupRefreshNotice({ - tone: "success", - text: t("auth.status.sessionRestoredTokenRefreshed"), - }); - setStartupStatusMessage(t("auth.status.tokenRefreshedLoadingAccount")); - } else if (sessionResult.refresh.outcome === "failed") { - setStartupRefreshNotice({ - tone: "warn", - text: t("auth.status.tokenRefreshFailedUsingSaved"), - }); - setStartupStatusMessage(t("auth.status.tokenRefreshFailedContinuing")); - } else if (sessionResult.refresh.outcome === "missing_refresh_token") { - setStartupStatusMessage(t("auth.status.missingRefreshTokenContinuing")); - } else if (persistedSession) { - setStartupStatusMessage(t("auth.status.sessionRestored")); - } else { - setStartupStatusMessage(t("auth.status.noSavedSessionFound")); - } - - // Load persisted variant selections from localStorage before applying defaults - try { - const raw = localStorage.getItem(VARIANT_SELECTION_LOCALSTORAGE_KEY); - if (raw) { - const parsed = JSON.parse(raw); - if (parsed && typeof parsed === "object") { - setVariantByGameId(parsed as Record); - } - } - } catch (e) { - // ignore parse/storage errors + const token = authSession?.tokens.idToken ?? authSession?.tokens.accessToken; + if (!token) { + throw new Error("Missing token for session resume"); } - - const persistedRuntimeSnapshot = loadRuntimeSnapshot(); - runtimeSnapshotRef.current = persistedRuntimeSnapshot; - if (persistedRuntimeSnapshot?.recoveryAppId !== null && persistedRuntimeSnapshot?.recoveryAppId !== undefined) { - signalingRecoveryRef.current.appId = persistedRuntimeSnapshot.recoveryAppId; + if (!existingSession.serverIp) { + throw new Error("Active session is missing server address. Start the game again to create a new session."); } + warmNativeStreamerForLaunch(); - setProviders(providerList); - setAuthSession(persistedSession); - setSavedAccounts(accounts); - - const activeProviderId = persistedSession?.provider?.idpId ?? providerList[0]?.idpId ?? ""; - setProviderIdpId(activeProviderId); + console.log("[Resume] claimAndConnectSession: invoking claimSession", { + sessionId: existingSession.sessionId, + serverIp: existingSession.serverIp, + status: existingSession.status, + appId: existingSession.appId, + }); - if (persistedSession) { - const hydrated = hydrateCatalogSnapshot(persistedSession, loadedSessionProxyUrl); - void loadSessionRuntimeData(persistedSession, { background: hydrated !== null, proxyUrl: loadedSessionProxyUrl }); + const matchedContext = findGameContextForSession(existingSession); + if (matchedContext) { + setStreamingGame(matchedContext.game); + setStreamingStore(matchedContext.variant?.store ?? null); } else { - runtimeDataLoadIdRef.current += 1; - resetStorePanels(); - setRegions([]); - setGames([]); - setLibraryGames([]); - setSubscriptionInfo(null); - setCatalogFilterGroups([]); - setCatalogSortOptions([]); - setCatalogTotalCount(0); - setCatalogSupportedCount(0); - setIsLoadingCatalog(false); - setIsLoadingLibrary(false); - } - - setIsInitializing(false); - } catch (error) { - console.error("Initialization failed:", error); - setStartupStatusMessage(t("auth.status.sessionRestoreFailed")); - // Always set isInitializing to false even on error - setIsInitializing(false); - } - }; - - void initialize(); - }, [hydrateCatalogSnapshot, loadSessionRuntimeData, resetStorePanels, t]); - - // Login handler - const handleLogin = useCallback(async () => { - setIsLoggingIn(true); - setActiveLoginMode("oauth"); - setLoginError(null); - if (qrLoginChallenge) { - void window.openNow.cancelDeviceLogin({ attemptId: qrLoginChallenge.attemptId }); - } - setQrLoginChallenge(null); - try { - const session = await window.openNow.login({ providerIdpId: providerIdpId || undefined }); - setAuthSession(session); - setProviderIdpId(session.provider.idpId); - await refreshSavedAccounts(); - await loadSessionRuntimeData(session); - } catch (error) { - setLoginError(error instanceof Error ? error.message : t("errors.loginFailed")); - } finally { - setIsLoggingIn(false); - setActiveLoginMode(null); - } - }, [loadSessionRuntimeData, providerIdpId, qrLoginChallenge, refreshSavedAccounts, t]); - - const qrLoginAttemptRef = useRef(0); - const completingQrLoginRef = useRef(false); - - const handleCancelQrLogin = useCallback(() => { - if (completingQrLoginRef.current) { - return; - } - qrLoginAttemptRef.current += 1; - if (qrLoginChallenge) { - void window.openNow.cancelDeviceLogin({ attemptId: qrLoginChallenge.attemptId }); - } - setQrLoginChallenge(null); - setIsLoggingIn(false); - setActiveLoginMode(null); - setLoginError(null); - }, [qrLoginChallenge]); - - const handleQrLogin = useCallback(async () => { - const attemptId = qrLoginAttemptRef.current + 1; - qrLoginAttemptRef.current = attemptId; - completingQrLoginRef.current = false; - setIsLoggingIn(true); - setActiveLoginMode("qr"); - setLoginError(null); - if (qrLoginChallenge) { - void window.openNow.cancelDeviceLogin({ attemptId: qrLoginChallenge.attemptId }); - } - setQrLoginChallenge(null); - - try { - const challenge = await window.openNow.startDeviceLogin({ providerIdpId: providerIdpId || undefined }); - if (qrLoginAttemptRef.current !== attemptId) { - void window.openNow.cancelDeviceLogin({ attemptId: challenge.attemptId }); - return; - } - - setQrLoginChallenge(challenge); - let intervalSeconds = Math.max(1, challenge.intervalSeconds); - - while (Date.now() < challenge.expiresAt) { - await sleep(intervalSeconds * 1000); - if (qrLoginAttemptRef.current !== attemptId) { - return; - } - - const result = await window.openNow.pollDeviceLogin({ - attemptId: challenge.attemptId, - deviceCode: challenge.deviceCode, - }); - if (qrLoginAttemptRef.current !== attemptId) { - return; - } - - if (result.status === "authorized") { - completingQrLoginRef.current = true; - setQrLoginChallenge(null); - setActiveLoginMode(null); - const session = await window.openNow.completeDeviceLogin({ attemptId: challenge.attemptId }); - if (qrLoginAttemptRef.current !== attemptId) { - return; - } - setAuthSession(session); - setProviderIdpId(session.provider.idpId); - await refreshSavedAccounts(); - await loadSessionRuntimeData(session); - return; - } - - if (result.status === "pending") { - continue; - } - - if (result.status === "slow_down") { - intervalSeconds += 5; - continue; - } - - throw new Error(result.error ?? t("errors.loginFailed")); - } - - throw new Error(t("auth.qr.expired")); - } catch (error) { - if (qrLoginAttemptRef.current === attemptId) { - setLoginError(error instanceof Error ? error.message : t("errors.loginFailed")); - } - } finally { - if (qrLoginAttemptRef.current === attemptId) { - setQrLoginChallenge(null); - setIsLoggingIn(false); - setActiveLoginMode(null); - completingQrLoginRef.current = false; - } - } - }, [loadSessionRuntimeData, providerIdpId, qrLoginChallenge, refreshSavedAccounts, t]); - - const handleSwitchAccount = useCallback(async (userId: string) => { - try { - const session = await window.openNow.switchAccount(userId); - setAuthSession(session); - setProviderIdpId(session.provider.idpId); - await refreshSavedAccounts(); - await loadSessionRuntimeData(session); - await refreshNavbarActiveSession(session); - } catch (error) { - console.warn("Failed to switch account:", error); - setLoginError(error instanceof Error ? error.message : t("errors.switchAccountFailed")); - try { - await refreshSavedAccounts(); - const sessionResult = await window.openNow.getAuthSession(); - setAuthSession(sessionResult.session); - if (sessionResult.session) { - setProviderIdpId(sessionResult.session.provider.idpId); - await loadSessionRuntimeData(sessionResult.session); - await refreshNavbarActiveSession(sessionResult.session); - } else { - runtimeDataLoadIdRef.current += 1; - resetStorePanels(); - setRegions([]); - setGames([]); - setLibraryGames([]); - setSubscriptionInfo(null); - setNavbarActiveSession(null); - setCatalogFilterGroups([]); - setCatalogSortOptions([]); - setCatalogTotalCount(0); - setCatalogSupportedCount(0); - setIsLoadingCatalog(false); - setIsLoadingLibrary(false); - } - } catch (recoveryError) { - console.warn("Failed to recover account state after switch failure:", recoveryError); - } - } - }, [loadSessionRuntimeData, refreshNavbarActiveSession, refreshSavedAccounts, resetStorePanels, t]); - - const handleRemoveAccount = useCallback((userId: string) => { - setAccountToRemove(userId); - setRemoveAccountConfirmOpen(true); - }, []); - - const confirmRemoveAccount = useCallback(async () => { - if (!accountToRemove) return; - const targetUserId = accountToRemove; - setRemoveAccountConfirmOpen(false); - setAccountToRemove(null); - - await window.openNow.removeAccount(targetUserId); - const [accounts, sessionResult] = await Promise.all([ - window.openNow.getSavedAccounts(), - window.openNow.getAuthSession(), - ]); - setSavedAccounts(accounts); - setAuthSession(sessionResult.session); - if (sessionResult.session) { - setProviderIdpId(sessionResult.session.provider.idpId); - await loadSessionRuntimeData(sessionResult.session); - await refreshNavbarActiveSession(sessionResult.session); - return; - } - runtimeDataLoadIdRef.current += 1; - resetStorePanels(); - setRegions([]); - setGames([]); - setFeaturedGames([]); - setLibraryGames([]); - setSubscriptionInfo(null); - setNavbarActiveSession(null); - setCatalogFilterGroups([]); - setCatalogSortOptions([]); - setCatalogTotalCount(0); - setCatalogSupportedCount(0); - setIsLoadingCatalog(false); - setIsLoadingLibrary(false); - }, [accountToRemove, loadSessionRuntimeData, refreshNavbarActiveSession, resetStorePanels]); - - const handleAddAccount = useCallback(() => { - setAuthSession(null); - setLoginError(null); - }, []); - - const confirmLogout = useCallback(async () => { - setLogoutConfirmOpen(false); - runtimeDataLoadIdRef.current += 1; - resetStorePanels(); - clearCatalogSnapshot(); - await window.openNow.logoutAll(); - setAuthSession(null); - setSavedAccounts([]); - setGames([]); - setLibraryGames([]); - setVariantByGameId({}); - resetLaunchRuntime(); - setNavbarActiveSession(null); - setIsResumingNavbarSession(false); - setSubscriptionInfo(null); - setCurrentPage("home"); - setCatalogFilterGroups([]); - setCatalogSortOptions([]); - setCatalogSelectedSortId("relevance"); - setCatalogSelectedFilterIds([]); - setCatalogTotalCount(0); - setCatalogSupportedCount(0); - setSelectedGameId(""); - setIsLoadingCatalog(false); - setIsLoadingLibrary(false); - }, [resetLaunchRuntime, resetStorePanels]); - - // Logout handler - const handleLogout = useCallback(() => { - setLogoutConfirmOpen(true); - }, []); - - // Load games handler - const loadGames = useCallback(async ( - targetSource: "main" | "library", - options?: { background?: boolean }, - ) => { - const setLoading = targetSource === "main" ? setIsLoadingCatalog : setIsLoadingLibrary; - if (!options?.background) { - setLoading(true); - } - try { - const token = authSession?.tokens.idToken ?? authSession?.tokens.accessToken; - const userId = authSession?.user.userId; - const baseUrl = effectiveStreamingBaseUrl; - const proxyUrl = activeSessionProxyUrl; - if (!token || !userId) { - return; - } - - if (targetSource === "main") { - const catalogResult = await window.openNow.browseCatalog({ - token, - userId, - providerStreamingBaseUrl: baseUrl, - proxyUrl, - searchQuery, - sortId: catalogSelectedSortId, - filterIds: catalogSelectedFilterIds, - }); - applyCatalogBrowseResult(catalogResult); - if (featuredGames.length === 0) { - void window.openNow.fetchFeaturedGames({ token, userId, providerStreamingBaseUrl: baseUrl, proxyUrl }).then((featured) => { - if (featured.length > 0) setFeaturedGames(featured); - }).catch((error) => { - console.warn("Featured games refresh failed:", error); - }); - } - return; - } - - const result = await window.openNow.fetchLibraryGames({ token, userId, providerStreamingBaseUrl: baseUrl, proxyUrl }); - setLibraryGames(result); - setSelectedGameId((previous) => result.some((game) => game.id === previous) ? previous : (result[0]?.id ?? "")); - applyVariantSelections(result); - } catch (error) { - console.error("Failed to load games:", error); - } finally { - if (!options?.background) { - setLoading(false); - } - } - }, [activeSessionProxyUrl, applyCatalogBrowseResult, applyVariantSelections, authSession, effectiveStreamingBaseUrl, featuredGames.length, searchQuery, catalogFilterKey, catalogSelectedSortId]); - - const loadStorePanels = useCallback(async (options?: { force?: boolean; background?: boolean }) => { - const session = authSession; - if (!session) return; - - const token = session.tokens.idToken ?? session.tokens.accessToken; - if (!token) return; - - const contextKey = `${session.user.userId}\0${effectiveStreamingBaseUrl}\0${getSessionProxyUiScope(activeSessionProxyUrl)}`; - if (!options?.force && storePanelsLoadedContextRef.current === contextKey) return; - - const loadId = ++storePanelsLoadIdRef.current; - const isCurrentLoad = (): boolean => storePanelsLoadIdRef.current === loadId; - if (!options?.background) setIsLoadingStorePanels(true); - try { - const panels = await window.openNow.fetchStorePanels({ - token, - providerStreamingBaseUrl: effectiveStreamingBaseUrl, - proxyUrl: activeSessionProxyUrl, - }); - if (!isCurrentLoad()) return; - const panelGames = flattenStorePanelGames(panels); - storePanelsLoadedContextRef.current = contextKey; - setStorePanels(panels); - setSelectedGameId((previous) => panelGames.some((game) => game.id === previous) ? previous : (panelGames[0]?.id ?? "")); - setVariantByGameId((previous) => { - const next = { ...previous }; - for (const game of panelGames) { - next[game.id] = defaultVariantId(game); - } - return next; - }); - } catch (error) { - if (!isCurrentLoad()) return; - console.error("Failed to load Store panels:", error); - storePanelsLoadedContextRef.current = ""; - setStorePanels([]); - } finally { - if (isCurrentLoad() && !options?.background) setIsLoadingStorePanels(false); - } - }, [activeSessionProxyUrl, authSession, effectiveStreamingBaseUrl]); - - const handleMarkGameOwned = useCallback(async (game: GameInfo, selectedVariantId?: string): Promise => { - const session = authSession; - const token = session?.tokens.idToken ?? session?.tokens.accessToken; - const userId = session?.user.userId; - if (!token || !userId) { - setCatalogActionNotice({ tone: "warn", text: t("errors.markOwnedSignInRequired") }); - return; - } - - const selectedVariant = getSelectedVariant(game, selectedVariantId ?? variantByGameId[game.id] ?? defaultVariantId(game)); - const variantId = selectedVariant?.id ?? selectedVariantId; - if (!variantId) { - setCatalogActionNotice({ tone: "warn", text: t("errors.markOwnedMissingVariant") }); - return; - } - if (markOwnedInFlightByVariantId[variantId]) { - return; - } - - setMarkOwnedInFlightByVariantId((previous) => ({ ...previous, [variantId]: true })); - try { - await window.openNow.markGameOwned({ - token, - userId, - providerStreamingBaseUrl: effectiveStreamingBaseUrl, - proxyUrl: activeSessionProxyUrl, - variantId, - }); - - setVariantByGameId((previous) => ({ ...previous, [game.id]: variantId })); - setGames((previous) => markGameOwnedInList(previous, game, variantId)); - setFeaturedGames((previous) => markGameOwnedInList(previous, game, variantId)); - setLibraryGames((previous) => upsertMarkedOwnedLibraryGame(previous, game, variantId)); - setStorePanels((previous) => markGameOwnedInPanels(previous, game, variantId)); - setCatalogActionNotice({ tone: "success", text: t("games.markOwned.success", { title: game.title }) }); - - void loadGames("main", { background: true }); - void loadGames("library", { background: true }); - void loadStorePanels({ force: true, background: true }); - } catch (error) { - console.error("Failed to mark game as owned:", error); - setCatalogActionNotice({ - tone: "warn", - text: error instanceof Error && error.message - ? t("errors.markOwnedFailedWithReason", { reason: error.message }) - : t("errors.markOwnedFailed"), - }); - } finally { - setMarkOwnedInFlightByVariantId((previous) => { - const next = { ...previous }; - delete next[variantId]; - return next; - }); - } - }, [ - activeSessionProxyUrl, - authSession, - effectiveStreamingBaseUrl, - loadGames, - loadStorePanels, - markOwnedInFlightByVariantId, - t, - variantByGameId, - ]); - - useEffect(() => { - if (storePanelGames.length === 0 || libraryGames.length === 0) return; - setVariantByGameId((previous) => { - let changed = false; - const next = { ...previous }; - for (const game of storePanelGames) { - const libraryVariantId = getLibrarySelectedVariantId(game, libraryGames); - if (libraryVariantId && next[game.id] !== libraryVariantId) { - next[game.id] = libraryVariantId; - changed = true; - } - } - return changed ? next : previous; - }); - }, [libraryGames, storePanelGames]); - - useEffect(() => { - if (!authSession || currentPage !== "home" || effectiveControllerMode || isInitializing) { - return; - } - const queryKey = buildProxyAwareCatalogQueryKey(searchQuery, catalogSelectedFilterIds, catalogSelectedSortId, activeSessionProxyUrl); - if ( - lastCatalogQueryRef.current === queryKey - && lastCatalogProxyUrlRef.current === activeSessionProxyUrl - && games.length > 0 - ) { - return; - } - lastCatalogQueryRef.current = queryKey; - lastCatalogProxyUrlRef.current = activeSessionProxyUrl; - - const handle = window.setTimeout(() => { - void loadGames("main", { background: games.length > 0 }); - }, searchQuery.trim() ? 220 : 0); - return () => window.clearTimeout(handle); - }, [ - authSession, - currentPage, - games.length, - isInitializing, - loadGames, - searchQuery, - activeSessionProxyUrl, - catalogFilterKey, - catalogSelectedSortId, - effectiveControllerMode, - ]); - - useEffect(() => { - if (!authSession || currentPage !== "home" || !effectiveControllerMode) { - return; - } - void loadStorePanels(); - }, [authSession, currentPage, loadStorePanels, effectiveControllerMode]); - - const handleSelectGameVariant = useCallback((gameId: string, variantId: string): void => { - setVariantByGameId((prev) => { - if (prev[gameId] === variantId) { - return prev; - } - const next = { ...prev, [gameId]: variantId }; - try { - localStorage.setItem(VARIANT_SELECTION_LOCALSTORAGE_KEY, JSON.stringify(next)); - } catch (e) { - // ignore storage errors - } - return next; - }); - }, []); - - const handleToggleCatalogFilter = useCallback((filterId: string): void => { - setCatalogSelectedFilterIds((previous) => ( - previous.includes(filterId) - ? previous.filter((value) => value !== filterId) - : [...previous, filterId] - )); - }, []); - - const resolveSessionClaimAppId = useCallback((existingSession: ActiveSessionInfo): string => { - const trackedAppId = signalingRecoveryRef.current.appId; - const persistedAppId = runtimeSnapshotRef.current?.sessionAppId ?? runtimeSnapshotRef.current?.recoveryAppId; - if (Number.isFinite(existingSession.appId) && existingSession.appId > 0) { - return String(existingSession.appId); - } - if (trackedAppId && Number.isFinite(trackedAppId)) { - return String(trackedAppId); - } - if (persistedAppId && Number.isFinite(persistedAppId)) { - return String(persistedAppId); - } - throw new Error("Active session is missing app metadata required for resume."); - }, []); - - const resolveResumeIdentity = useCallback((sessionId: string): { clientId?: string; deviceId?: string } => { - const liveSession = sessionRef.current; - if (liveSession?.sessionId === sessionId) { - return { - clientId: liveSession.clientId, - deviceId: liveSession.deviceId, - }; - } - const persisted = runtimeSnapshotRef.current?.resumeContext; - if (persisted?.sessionId === sessionId) { - return { - clientId: persisted.clientId, - deviceId: persisted.deviceId, - }; - } - return {}; - }, []); - - const applyClaimedSessionAndConnect = useCallback(async ( - claimed: SessionInfo, - expectedRecoveryGeneration?: number, - ): Promise => { - const canProceedWithClaimedReconnect = (): boolean => { - if ( - expectedRecoveryGeneration !== undefined - && !isRecoveryGenerationCurrent(expectedRecoveryGeneration) - ) { - return false; - } - if (signalingRecoveryRef.current.explicitShutdown) { - return false; - } - return true; - }; - - if ( - expectedRecoveryGeneration !== undefined - && !isRecoveryGenerationCurrent(expectedRecoveryGeneration) - ) { - console.log("[Recovery] Skipping claimed session apply due to stale recovery generation"); - return; - } - - console.log("Claimed session:", { - sessionId: claimed.sessionId, - signalingServer: claimed.signalingServer, - signalingUrl: claimed.signalingUrl, - status: claimed.status, - }); - - await sleep(1000); - if ( - expectedRecoveryGeneration !== undefined - && !isRecoveryGenerationCurrent(expectedRecoveryGeneration) - ) { - console.log("[Recovery] Skipping reconnect due to stale recovery generation after delay"); - return; - } - if (!canProceedWithClaimedReconnect()) { - console.log("[Recovery] Skipping claimed session apply due to explicit shutdown"); - return; - } - - // Mirror attemptSessionRecovery: tear down WebRTC + signaling before connecting to a new edge. - // Avoids stale PeerConnection/video vs migrated CloudMatch connectionInfo (intermittent black screen on resume). - const reconnectSource = expectedRecoveryGeneration !== undefined ? "recovery" : "resume"; - console.log(`[Stream] ${reconnectSource}: teardown WebRTC + signaling before reconnect`, { - sessionId: claimed.sessionId, - signalingServer: claimed.signalingServer, - signalingUrl: claimed.signalingUrl, - mediaConnectionInfo: claimed.mediaConnectionInfo, - }); - clientRef.current?.dispose(); - clientRef.current = null; - await disconnectSignalingControlled(); - awaitingRecoveryRemoteIceRef.current = expectedRecoveryGeneration !== undefined; - - setSession(claimed); - sessionRef.current = claimed; - nativeInputProtocolVersionRef.current = null; - setNativeInputBridgeReady(false); - setNativeInputCaptureActive(false); - setQueuePosition(undefined); - setLaunchError(null); - setStreamStatus("connecting"); - await window.openNow.connectSignaling(buildSignalingConnectRequest(claimed)); - }, [buildSignalingConnectRequest, disconnectSignalingControlled, isRecoveryGenerationCurrent]); - - const claimAndConnectSession = useCallback(async (existingSession: ActiveSessionInfo): Promise => { - const sid = existingSession.sessionId; - const inflight = claimResumePromisesRef.current.get(sid); - if (inflight) { - console.log("[Resume] claimAndConnectSession: deduped — joining in-flight claim for session", sid); - await inflight; - return; - } - - const resumePromiseHolder: { promise?: Promise } = {}; - resumePromiseHolder.promise = (async (): Promise => { - try { - const token = authSession?.tokens.idToken ?? authSession?.tokens.accessToken; - if (!token) { - throw new Error("Missing token for session resume"); - } - if (!existingSession.serverIp) { - throw new Error("Active session is missing server address. Start the game again to create a new session."); - } - warmNativeStreamerForLaunch(); - - console.log("[Resume] claimAndConnectSession: invoking claimSession", { - sessionId: existingSession.sessionId, - serverIp: existingSession.serverIp, - status: existingSession.status, - appId: existingSession.appId, - }); - - const matchedContext = findGameContextForSession(existingSession); - if (matchedContext) { - setStreamingGame(matchedContext.game); - setStreamingStore(matchedContext.variant?.store ?? null); - } else { - setStreamingStore(null); + setStreamingStore(null); } const launchSubscription = await resolveSubscriptionInfoForLaunch(); @@ -2829,58 +1619,24 @@ export function App(): JSX.Element { } const previousAppId = recoveryState.appId; const currentSessionId = currentSession.sessionId; - const sameSessionCandidate = - activeSessions.find((entry) => entry.sessionId === currentSessionId && entry.serverIp && isSessionReadyForConnect(entry.status)) ?? - null; - - let candidate = sameSessionCandidate; - if (!candidate && previousAppId !== null) { - candidate = - activeSessions.find((entry) => ( - entry.appId === previousAppId && - entry.serverIp && - isSessionReadyForConnect(entry.status) && - entry.sessionId === currentSessionId - )) ?? - activeSessions.find((entry) => ( - entry.appId === previousAppId && - entry.serverIp && - isSessionReadyForConnect(entry.status) - )) ?? - null; - } - - if (!candidate) { - const persisted = runtimeSnapshotRef.current?.resumeContext; - if ( - persisted && - persisted.sessionId === currentSessionId && - persisted.serverIp - ) { - candidate = { - sessionId: persisted.sessionId, - appId: - Number.isFinite(persisted.appId ?? NaN) - ? (persisted.appId as number) - : (previousAppId ?? 0), - appLaunchMode: persisted.appLaunchMode, - enablePersistingInGameSettings: persisted.enablePersistingInGameSettings, - status: 2, - serverIp: persisted.serverIp, - streamingBaseUrl: persisted.streamingBaseUrl, - signalingUrl: persisted.signalingUrl, - }; - console.log("[Recovery] Falling back to persisted resume context", { - sessionId: persisted.sessionId, - serverIp: persisted.serverIp, - appId: persisted.appId ?? previousAppId ?? null, - }); - } + const persisted = runtimeSnapshotRef.current?.resumeContext ?? null; + const recoveryCandidate = selectRecoveryCandidate( + activeSessions, + currentSessionId, + previousAppId, + persisted, + ); + const candidate = recoveryCandidate.candidate; + if (recoveryCandidate.source === "persisted-resume-context" && persisted) { + console.log("[Recovery] Falling back to persisted resume context", { + sessionId: persisted.sessionId, + serverIp: persisted.serverIp, + appId: persisted.appId ?? previousAppId ?? null, + }); } if (!candidate) { - const hasQueueOnlyMatch = activeSessions.some((entry) => entry.sessionId === currentSessionId && entry.status === 1); - if (hasQueueOnlyMatch) { + if (recoveryCandidate.hasQueueOnlyMatch) { throw new Error("The session is still queued and cannot be reclaimed until the server marks it ready again."); } throw new Error("The running session could not be found anymore, so resume was not possible."); @@ -2919,765 +1675,95 @@ export function App(): JSX.Element { await applyClaimedSessionAndConnect(claimed, recoveryGeneration); if (!isRecoveryGenerationCurrent(recoveryGeneration)) { - console.log("[Recovery] Recovery generation changed before connect completed"); - return false; - } - return true; - } catch (error) { - lastError = error instanceof Error ? error : new Error(String(error)); - console.warn(`[Recovery] Attempt ${attemptNumber} failed:`, lastError.message); - } - } - - if (lastError) { - throw lastError; - } - return false; - })(); - - recoveryState.inFlight = attemptPromise; - try { - return await attemptPromise; - } finally { - if (signalingRecoveryRef.current.inFlight === attemptPromise) { - signalingRecoveryRef.current.inFlight = null; - } - } - }, [ - applyClaimedSessionAndConnect, - authSession, - disconnectSignalingControlled, - effectiveStreamingBaseUrl, - findGameContextForSession, - isRecoveryGenerationCurrent, - resolveResumeIdentity, - resolveSessionClaimAppId, - buildCurrentStreamSettings, - resolveSubscriptionInfoForLaunch, - ]); - - const handleExpectedNativeSessionClose = useCallback((reason: string): void => { - console.log("[Recovery] Treating signaling close as ended session:", reason); - const activeGameId = streamingGameRef.current?.id; - if (activeGameId) { - endPlaytimeSession(activeGameId); - } - markExplicitSignalingShutdown(); - clientRef.current?.dispose(); - clientRef.current = null; - launchInFlightRef.current = false; - resetLaunchRuntime(); - void refreshNavbarActiveSession(); - }, [endPlaytimeSession, markExplicitSignalingShutdown, refreshNavbarActiveSession, resetLaunchRuntime]); - - // Signaling events - useEffect(() => { - const ensureWebRtcClient = (): GfnWebRtcClient | null => { - if (clientRef.current) { - return clientRef.current; - } - if (!videoRef.current || !audioRef.current) { - return null; - } - - clientRef.current = new GfnWebRtcClient({ - videoElement: videoRef.current, - audioElement: audioRef.current, - autoFullScreen: settings.autoFullScreen, - microphoneMode: settings.microphoneMode, - microphoneDeviceId: settings.microphoneDeviceId || undefined, - nativeCursorOverlay: settings.nativeCursorOverlay, - mouseSensitivity: settings.mouseSensitivity, - mouseAcceleration: settings.mouseAcceleration, - keyboardLayout: settings.keyboardLayout, - clipboardPaste: settings.clipboardPaste, - readClipboardText: readStreamClipboardText, - onLog: (line: string) => console.log(`[WebRTC] ${line}`), - onStats: (stats) => diagnosticsStore.set(stats), - onTimeWarning: (warning) => { - setRemoteStreamWarning({ - code: warning.code, - message: warningMessage(t, warning.code), - tone: warningTone(warning.code), - secondsLeft: warning.secondsLeft, - }); - }, - onMicStateChange: (state) => { - console.log(`[App] Mic state: ${state.state}${state.deviceLabel ? ` (${state.deviceLabel})` : ""}`); - }, - onControllerMetaPress: () => { - if (streamStatusRef.current === "streaming") { - dispatchStreamShortcutAction("toggleSidebar"); - } - }, - onIceConnectionStateChange: (iceState) => { - latestIceConnectionStateRef.current = iceState; - if (iceDisconnectedRecoveryTimerRef.current !== null) { - window.clearTimeout(iceDisconnectedRecoveryTimerRef.current); - iceDisconnectedRecoveryTimerRef.current = null; - } - if (appUnloadingRef.current) { - return; - } - if (streamStatusRef.current !== "streaming") { - return; - } - if (iceState === "failed") { - console.warn("[Recovery] ICE failed; attempting targeted recovery"); - void attemptSessionRecovery("ICE failed").catch((error) => { - console.error("[Recovery] ICE-failed recovery failed:", error); - }); - return; - } - if (iceState === "disconnected") { - iceDisconnectedRecoveryTimerRef.current = window.setTimeout(() => { - iceDisconnectedRecoveryTimerRef.current = null; - if (appUnloadingRef.current || streamStatusRef.current !== "streaming") { - return; - } - if (latestIceConnectionStateRef.current !== "disconnected") { - return; - } - console.warn("[Recovery] ICE remained disconnected; attempting targeted recovery"); - void attemptSessionRecovery("ICE disconnected timeout").catch((error) => { - console.error("[Recovery] ICE-disconnected recovery failed:", error); - }); - }, ICE_DISCONNECTED_RECOVERY_GRACE_MS); - } - }, - }); - clientRef.current.setOutputVolume(streamVolume); - clientRef.current.setMicrophoneLevel(streamMicLevel); - if (settings.microphoneMode !== "disabled") { - void clientRef.current.startMicrophone(); - } - return clientRef.current; - }; - - const activateNativeInputForCurrentSession = (protocolVersion?: number): void => { - const activeSession = sessionRef.current; - if (!activeSession) { - console.warn("[App] Received native stream event but no active session in sessionRef!"); - return; - } - const client = ensureWebRtcClient(); - if (!client) { - console.warn("[App] Native stream event received before media elements were ready"); - return; - } - - nativeStreamingRef.current = true; - pendingControlledDisconnectsRef.current = 0; - client.activateNativeInput(protocolVersion, { - codec: settings.codec, - colorQuality: settings.colorQuality, - resolution: settings.resolution, - fps: settings.fps, - maxBitrateKbps: settings.maxBitrateMbps * 1000, - }); - setLaunchError(null); - setStreamStatus("streaming"); - markDiscordStreamStarted(); - scheduleStableRecoveryReset(activeSession.sessionId); - }; - - const unsubscribe = window.openNow.onSignalingEvent(async (event: MainToRendererSignalingEvent) => { - console.log(`[App] Signaling event: ${event.type}`, event.type === "offer" ? `(SDP ${event.sdp.length} chars)` : "", event.type === "remote-ice" ? event.candidate : ""); - try { - if (event.type === "offer") { - pendingControlledDisconnectsRef.current = 0; - const activeSession = sessionRef.current; - if (!activeSession) { - console.warn("[App] Received offer but no active session in sessionRef!"); - return; - } - const shouldEnforceRemoteIceGrace = awaitingRecoveryRemoteIceRef.current; - remoteIceSeenForSessionRef.current = null; - hasConfirmedRemoteIceRef.current = false; - if (remoteIceGraceTimerRef.current !== null) { - window.clearTimeout(remoteIceGraceTimerRef.current); - remoteIceGraceTimerRef.current = null; - } - const expectedSessionId = activeSession.sessionId; - const recoveryGenerationAtOffer = signalingRecoveryRef.current.generation; - if (shouldEnforceRemoteIceGrace) { - remoteIceGraceTimerRef.current = window.setTimeout(() => { - remoteIceGraceTimerRef.current = null; - if (sessionRef.current?.sessionId !== expectedSessionId) { - return; - } - if (remoteIceSeenForSessionRef.current === expectedSessionId) { - return; - } - if (remoteIceRecoveryGenerationRef.current === recoveryGenerationAtOffer) { - return; - } - if (!RECOVERABLE_STREAM_STATUSES.includes(streamStatusRef.current)) { - return; - } - awaitingRecoveryRemoteIceRef.current = false; - remoteIceRecoveryGenerationRef.current = recoveryGenerationAtOffer; - console.warn( - `[Recovery] No remote ICE received within ${SIGNALING_REMOTE_ICE_GRACE_MS}ms after offer; forcing targeted recovery`, - ); - void attemptSessionRecovery("No remote ICE received after offer").catch((error) => { - console.error("[Recovery] ICE-timeout recovery failed:", error); - }); - }, SIGNALING_REMOTE_ICE_GRACE_MS); - } - console.log("[App] Active session for offer:", JSON.stringify({ - sessionId: activeSession.sessionId, - serverIp: activeSession.serverIp, - signalingServer: activeSession.signalingServer, - mediaConnectionInfo: activeSession.mediaConnectionInfo, - iceServersCount: activeSession.iceServers?.length, - })); - - const client = ensureWebRtcClient(); - - if (client) { - await client.handleOffer(event.sdp, activeSession, { - codec: settings.codec, - colorQuality: settings.colorQuality, - resolution: settings.resolution, - fps: settings.fps, - maxBitrateKbps: settings.maxBitrateMbps * 1000, - nativeTransitionDiagnostics: settings.nativeTransitionDiagnostics, - }); - setLaunchError(null); - setStreamStatus("streaming"); - markDiscordStreamStarted(); - scheduleStableRecoveryReset(activeSession.sessionId); - console.log( - "[Stream] Offer applied; use [WebRTC] logs for ICE/video dimensions. signalingServer=%s media=%s", - activeSession.signalingServer, - activeSession.mediaConnectionInfo - ? `${activeSession.mediaConnectionInfo.ip}:${activeSession.mediaConnectionInfo.port}` - : "n/a", - ); - } - } else if (event.type === "native-stream-started") { - console.log("[App] Native streamer started:", event.message ?? ""); - activateNativeInputForCurrentSession(nativeInputProtocolVersionRef.current ?? undefined); - } else if (event.type === "native-input-ready") { - console.log("[App] Native input protocol ready:", event.protocolVersion); - nativeInputProtocolVersionRef.current = event.protocolVersion; - setNativeInputBridgeReady(true); - clientRef.current?.setNativeInputProtocolVersion(event.protocolVersion); - if (nativeStreamingRef.current || sessionRef.current) { - activateNativeInputForCurrentSession(event.protocolVersion); - } - } else if (event.type === "native-shortcut") { - handleStreamShortcutActionRef.current?.(event.action); - } else if (event.type === "native-clipboard-paste") { - if (settings.clipboardPaste && (!nativeStreamingRef.current || nativeInputBridgeReady)) { - void sendStreamClipboardPaste(clientRef.current); - } - } else if (event.type === "native-input-capture-changed") { - setNativeInputCaptureActive(event.captured); - } else if (event.type === "native-stream-stats") { - diagnosticsStore.set(mergeNativeStreamStats( - diagnosticsStore.getSnapshot(), - event.stats, - )); - } else if (event.type === "native-stream-transition") { - diagnosticsStore.set({ - ...diagnosticsStore.getSnapshot(), - nativeRendererActive: true, - nativeTransitionSummary: event.transition.summary, - nativeRequestedFps: event.transition.requestedFps, - nativeCapsFramerate: event.transition.capsFramerate, - nativeQueueMode: event.transition.queueMode, - lagReasonDetail: event.transition.summary ?? "Native video transition detected", - }); - } else if (event.type === "native-stream-stopped") { - const reason = event.reason ?? "Native streamer stopped"; - console.warn("[App] Native streamer stopped:", reason); - nativeStreamingRef.current = false; - nativeInputProtocolVersionRef.current = null; - setNativeInputBridgeReady(false); - setNativeInputCaptureActive(false); - clientRef.current?.dispose(); - clientRef.current = null; - launchInFlightRef.current = false; - - if (appUnloadingRef.current) { - console.log("[Recovery] Ignoring native streamer stop during app shutdown"); - return; - } - if (streamStatusRef.current === "streaming" && isExpectedNativeSessionClose(reason)) { - handleExpectedNativeSessionClose(reason); - return; - } - if ( - signalingRecoveryRef.current.explicitShutdown - || !RECOVERABLE_STREAM_STATUSES.includes(streamStatusRef.current) - ) { - console.log("[Recovery] Ignoring native streamer stop after explicit shutdown or non-recoverable status"); - return; - } - - const recovered = await attemptSessionRecovery(reason).catch((error) => { - console.error("[Recovery] Native streamer recovery failed:", error); - return false; - }); - if (!recovered) { - if ( - signalingRecoveryRef.current.explicitShutdown - || !RECOVERABLE_STREAM_STATUSES.includes(streamStatusRef.current) - ) { - console.log("[Recovery] Ignoring native streamer stop after explicit shutdown or non-recoverable status"); - return; - } - setLaunchError({ - stage: streamStatusToLoadingStage(streamStatusRef.current), - title: t("errors.nativeStreamerStoppedTitle"), - description: t("errors.nativeStreamerStoppedDescription"), - }); - resetLaunchRuntime({ keepLaunchError: true, keepStreamingContext: true }); - void refreshNavbarActiveSession(); - launchInFlightRef.current = false; - } - } else if (event.type === "remote-ice") { - remoteIceSeenForSessionRef.current = sessionRef.current?.sessionId ?? null; - hasConfirmedRemoteIceRef.current = true; - awaitingRecoveryRemoteIceRef.current = false; - if (remoteIceGraceTimerRef.current !== null) { - window.clearTimeout(remoteIceGraceTimerRef.current); - remoteIceGraceTimerRef.current = null; - } - await clientRef.current?.addRemoteCandidate(event.candidate); - } else if (event.type === "disconnected") { - if (appUnloadingRef.current) { - console.log("[Recovery] Ignoring signaling disconnect during app shutdown"); - return; - } - if (streamStatusRef.current !== "idle" && isExpectedNativeSessionClose(event.reason)) { - handleExpectedNativeSessionClose(event.reason); - return; - } - if ( - nativeStreamingRef.current - && streamStatusRef.current === "streaming" - && isExpectedNativeSessionClose(event.reason) - ) { - handleExpectedNativeSessionClose(event.reason); - return; - } - const iceState = latestIceConnectionStateRef.current; - if ( - (hasConfirmedRemoteIceRef.current && iceState === "new") || - iceState === "connected" || - iceState === "completed" || - iceState === "checking" - ) { - console.log(`[Recovery] Ignoring signaling disconnect while ICE state is ${iceState}`); - return; - } - // Official-style behavior: if the attach never reached a confirmed remote ICE - // handshake, do not auto-recover. Fail this attempt and require explicit resume. - if (!hasConfirmedRemoteIceRef.current) { - console.warn("[Recovery] Skipping auto-recovery: disconnected before remote ICE handshake"); - clientRef.current?.dispose(); - clientRef.current = null; - setLaunchError({ - stage: streamStatusToLoadingStage(streamStatusRef.current), - title: t("errors.sessionConnectionLostTitle"), - description: t("errors.resumeAttachFailedDescription"), - }); - resetLaunchRuntime({ keepLaunchError: true, keepStreamingContext: true }); - void refreshNavbarActiveSession(); - launchInFlightRef.current = false; - return; - } - if (remoteIceGraceTimerRef.current !== null) { - window.clearTimeout(remoteIceGraceTimerRef.current); - remoteIceGraceTimerRef.current = null; - } - remoteIceSeenForSessionRef.current = null; - awaitingRecoveryRemoteIceRef.current = false; - if (pendingControlledDisconnectsRef.current > 0) { - pendingControlledDisconnectsRef.current -= 1; - console.log("[Recovery] Ignoring controlled signaling disconnect"); - return; - } - console.warn("Signaling disconnected:", event.reason); - const recovered = await attemptSessionRecovery(event.reason).catch((error) => { - console.error("[Recovery] Signaling recovery failed:", error); - throw error; - }); - if (!recovered) { - if ( - signalingRecoveryRef.current.explicitShutdown - || !RECOVERABLE_STREAM_STATUSES.includes(streamStatusRef.current) - ) { - console.log("[Recovery] Ignoring disconnect after explicit shutdown or non-recoverable status"); - return; - } - clientRef.current?.dispose(); - clientRef.current = null; - setLaunchError({ - stage: streamStatusToLoadingStage(streamStatusRef.current), - title: t("errors.sessionConnectionLostTitle"), - description: t("errors.sessionConnectionLostDescription"), - }); - resetLaunchRuntime({ keepLaunchError: true, keepStreamingContext: true }); - void refreshNavbarActiveSession(); - launchInFlightRef.current = false; - } - } else if (event.type === "error") { - console.error("Signaling error:", event.message); - } - } catch (error) { - if (appUnloadingRef.current) { - console.log("[Recovery] Suppressing signaling handler errors during app shutdown"); - return; - } - if ( - signalingRecoveryRef.current.explicitShutdown - || !RECOVERABLE_STREAM_STATUSES.includes(streamStatusRef.current) - ) { - console.log("[Recovery] Suppressing signaling error after explicit shutdown or non-recoverable status"); - return; - } - console.error("Signaling event error:", error); - clientRef.current?.dispose(); - clientRef.current = null; - const message = error instanceof Error ? error.message : t("errors.sessionResumeFailedDescription"); - setLaunchError({ - stage: streamStatusToLoadingStage(streamStatusRef.current), - title: t("errors.sessionConnectionLostTitle"), - description: message, - }); - resetLaunchRuntime({ keepLaunchError: true, keepStreamingContext: true }); - void refreshNavbarActiveSession(); - launchInFlightRef.current = false; - } - }); - - return () => unsubscribe(); - }, [attemptSessionRecovery, diagnosticsStore, handleExpectedNativeSessionClose, markDiscordStreamStarted, nativeInputBridgeReady, refreshNavbarActiveSession, resetLaunchRuntime, scheduleStableRecoveryReset, settings, streamMicLevel, streamVolume, t]); - - // Play game handler - const handlePlayGame = useCallback(async (game: GameInfo, options?: { bypassGuards?: boolean; streamingBaseUrl?: string; variantId?: string }) => { - if (!selectedProvider) return; - - console.log("handlePlayGame entry", { - title: game.title, - launchInFlight: launchInFlightRef.current, - streamStatus, - bypass: options?.bypassGuards ?? false, - }); - - if (!options?.bypassGuards && (launchInFlightRef.current || streamStatus !== "idle" || navbarSessionActionInFlightRef.current)) { - console.warn("Ignoring play request: launch already in progress or stream not idle", { - inFlight: launchInFlightRef.current, - streamStatus, - navbarSessionAction: navbarSessionActionInFlightRef.current, - }); - return; - } - - const selectedVariantId = options?.variantId ?? variantByGameId[game.id] ?? defaultVariantId(game); - const selectedVariant = getSelectedVariant(game, selectedVariantId); - const epicOwnershipError = getEpicOwnershipLaunchError(selectedVariant); - if (epicOwnershipError) { - setStreamingGame(game); - setStreamingStore(selectedVariant?.store ?? null); - setLaunchError({ - stage: "queue", - title: epicOwnershipError.title, - description: epicOwnershipError.description, - }); - return; - } - - launchInFlightRef.current = true; - launchAbortRef.current = false; - resetSignalingRecoveryState(); - let loadingStep: StreamLoadingStatus = "queue"; - const updateLoadingStep = (next: StreamLoadingStatus): void => { - loadingStep = next; - setStreamStatus(next); - }; - - setSessionStartedAtMs(null); - setRemoteStreamWarning(null); - setLocalSessionTimerWarning(null); - setLaunchError(null); - resetStatsOverlayToPreference(); - startPlaytimeSession(game.id); - updateLoadingStep("queue"); - setQueuePosition(undefined); - warmNativeStreamerForLaunch(); - let launchGameContext: GameInfo = game; - - try { - const token = authSession?.tokens.idToken ?? authSession?.tokens.accessToken; - - // Resolve appId - let appId: string | null = null; - if (isNumericId(selectedVariantId)) { - appId = selectedVariantId; - } else if (isNumericId(game.launchAppId)) { - appId = game.launchAppId; - } - - if (!appId && token) { - try { - const resolved = await window.openNow.resolveLaunchAppId({ - token, - providerStreamingBaseUrl: effectiveStreamingBaseUrl, - proxyUrl: activeSessionProxyUrl, - appIdOrUuid: game.uuid ?? selectedVariantId, - }); - if (resolved && isNumericId(resolved)) { - appId = resolved; - } - } catch { - // Ignore resolution errors - } - } - - if (!appId) { - throw new Error("Could not resolve numeric appId for this game"); - } - - const numericAppId = Number(appId); - signalingRecoveryRef.current.appId = numericAppId; - const matchedGameContext = findSessionContextForAppId(allKnownGames, variantByGameId, numericAppId) ?? { - game, - variant: selectedVariant, - }; - const launchVariant = matchedGameContext.variant ?? selectedVariant; - launchGameContext = matchedGameContext.game; - setStreamingGame(matchedGameContext.game); - setStreamingStore(launchVariant?.store ?? null); - - const launchSubscription = await resolveSubscriptionInfoForLaunch(); - const streamSettings = buildCurrentStreamSettings(launchSubscription); - const i2pStorageRegionBaseUrl = await resolveInstallToPlayStreamingBaseUrl( - matchedGameContext.game, - launchSubscription, - token || undefined, - ); - const launchStreamingBaseUrl = i2pStorageRegionBaseUrl ?? options?.streamingBaseUrl ?? effectiveStreamingBaseUrl; - let existingSessionStrategy: ExistingSessionStrategy | undefined; - - // Check for active sessions first - if (token) { - try { - const activeSessions = await window.openNow.getActiveSessions(token, launchStreamingBaseUrl); - if (activeSessions.length > 0) { - // Only claim sessions that are already paused/ready (status 2 or 3). - // Status=1 sessions are still in queue/setup; sending a RESUME claim - // skips the queue/ad phase entirely. Let them fall through to - // createSession so the polling loop handles queue position and ads. - const matchingSession = activeSessions.find((entry) => entry.appId === numericAppId && (entry.status === 2 || entry.status === 3)) ?? null; - const otherSession = activeSessions.find((s) => s.status === 2 || s.status === 3) ?? null; - - if (matchingSession) { - await claimAndConnectSession(matchingSession); - setNavbarActiveSession(null); - return; - } - - if (otherSession) { - const choice = await window.openNow.showSessionConflictDialog(); - if (choice === "cancel") { - resetLaunchRuntime(); - return; - } - if (choice === "resume") { - await claimAndConnectSession(otherSession); - setNavbarActiveSession(null); - return; - } - if (choice === "new") { - existingSessionStrategy = "force-new"; - } - } - } - } catch (error) { - console.error("Failed to claim/resume session:", error); - // Continue to create new session - } - } - - const sessionProxyUrl = activeSessionProxyUrl; - - // Create new session - const newSession = await window.openNow.createSession({ - token: token || undefined, - streamingBaseUrl: launchStreamingBaseUrl, - appId, - internalTitle: game.title, - accountLinked: chooseAccountLinked(game, selectedVariant), - enablePersistingInGameSettings: settings.enablePersistingInGameSettings, - supportsInGameSettingsPersistence: launchVariant?.supportsInGameSettingsPersistence === true, - existingSessionStrategy, - proxyUrl: sessionProxyUrl, - zone: "prod", - settings: streamSettings, - }); - - setSession(newSession); - setQueuePosition(newSession.queuePosition); - - // Poll for readiness. - // Queue and setup/starting modes wait indefinitely until the session becomes ready - // or the launch is explicitly aborted. Some rigs take much longer than 180s. - let finalSession: SessionInfo | null = null; - let latestSession = newSession; - let isInQueueMode = isSessionInQueue(newSession); - let attempt = 0; - - while (true) { - attempt++; - - const pollIntervalMs = shouldUseQueueAdPolling(latestSession, subscriptionInfo, authSession) - ? SESSION_AD_POLL_INTERVAL_MS - : SESSION_READY_POLL_INTERVAL_MS; - - // Sleep in small ticks during ad-polling intervals so the loop can react - // quickly when reportSessionAd clears isAdsRequired (which only updates - // sessionRef, not the local latestSession variable). Standard 2 s intervals - // are kept as a single sleep since they're already short. - if (pollIntervalMs > SESSION_READY_POLL_INTERVAL_MS) { - const tickMs = 500; - let elapsed = 0; - while (elapsed < pollIntervalMs) { - await sleep(tickMs); - elapsed += tickMs; - if (launchAbortRef.current) return; - // Sync ad-action responses from sessionRef into the local tracking variable - // so shouldUseQueueAdPolling sees the updated adState immediately. - const refSession = sessionRef.current; - if (refSession && refSession.sessionId === latestSession.sessionId) { - latestSession = mergePolledSessionState(latestSession, refSession); - } - // Break out of the sleep early when ads are no longer required. - if (!shouldUseQueueAdPolling(latestSession, subscriptionInfo, authSession)) { - break; - } - } - } else { - await sleep(pollIntervalMs); - } - - if (shouldUseQueueAdPolling(latestSession, subscriptionInfo, authSession) && queueAdPlaybackRef.current) { - const graceDeadline = Date.now() + 5000; - while (queueAdPlaybackRef.current && Date.now() < graceDeadline) { - await sleep(200); - if (launchAbortRef.current) { - return; - } - } - } - - if (launchAbortRef.current) { - return; - } - - if (launchAbortRef.current) { - return; - } - - const polled = await window.openNow.pollSession({ - token: token || undefined, - streamingBaseUrl: newSession.streamingBaseUrl ?? effectiveStreamingBaseUrl, - serverIp: newSession.serverIp, - zone: newSession.zone, - sessionId: newSession.sessionId, - clientId: newSession.clientId, - deviceId: newSession.deviceId, - proxyUrl: sessionProxyUrl, - }); - - if (launchAbortRef.current) { - return; - } - - const mergedSession = mergePolledSessionState(latestSession, polled); - latestSession = mergedSession; - - setSession(mergedSession); - setQueuePosition(mergedSession.queuePosition); - - // Check if queue just cleared so the loading UI can transition to setup mode. - isInQueueMode = isSessionInQueue(mergedSession); - - console.log( - `Poll attempt ${attempt}: status=${mergedSession.status}, seatSetupStep=${mergedSession.seatSetupStep ?? "n/a"}, queuePosition=${mergedSession.queuePosition ?? "n/a"}, serverIp=${mergedSession.serverIp}, queueMode=${isInQueueMode}, adsRequired=${isSessionAdsRequired(mergedSession.adState)}`, - ); - - if (isSessionReadyForConnect(mergedSession.status)) { - finalSession = mergedSession; - break; - } - - // Update status based on session state - if (isInQueueMode) { - updateLoadingStep("queue"); - } else if (mergedSession.status === 1) { - updateLoadingStep("setup"); + console.log("[Recovery] Recovery generation changed before connect completed"); + return false; + } + return true; + } catch (error) { + lastError = error instanceof Error ? error : new Error(String(error)); + console.warn(`[Recovery] Attempt ${attemptNumber} failed:`, lastError.message); } - } - // finalSession is guaranteed to be set here (we only exit the loop via break when session is ready) - - setQueuePosition(undefined); - updateLoadingStep("connecting"); - - // Use finalSession (the status=2 poll result) as the authoritative source for - // signaling coordinates — it carries the real server IP resolved at the moment - // the rig became ready. sessionRef.current may still hold stale zone-LB data - // from a prior React render cycle. - const sessionToConnect = finalSession ?? sessionRef.current ?? newSession; - console.log("Connecting signaling with:", { - sessionId: sessionToConnect.sessionId, - signalingServer: sessionToConnect.signalingServer, - signalingUrl: sessionToConnect.signalingUrl, - status: sessionToConnect.status, - }); - - await window.openNow.connectSignaling(buildSignalingConnectRequest(sessionToConnect)); - } catch (error) { - if (launchAbortRef.current) { - return; + if (lastError) { + throw lastError; } - console.error("Launch failed:", error); - setLaunchError(toLaunchErrorState(t, error, loadingStep, launchGameContext)); - await disconnectSignalingControlled(); - clientRef.current?.dispose(); - clientRef.current = null; - resetLaunchRuntime({ keepLaunchError: true, keepStreamingContext: true }); - void refreshNavbarActiveSession(); + return false; + })(); + + recoveryState.inFlight = attemptPromise; + try { + return await attemptPromise; } finally { - launchInFlightRef.current = false; + if (signalingRecoveryRef.current.inFlight === attemptPromise) { + signalingRecoveryRef.current.inFlight = null; + } } }, [ + applyClaimedSessionAndConnect, authSession, + disconnectSignalingControlled, + effectiveStreamingBaseUrl, + findGameContextForSession, + isRecoveryGenerationCurrent, + resolveResumeIdentity, + resolveSessionClaimAppId, + buildCurrentStreamSettings, + resolveSubscriptionInfoForLaunch, + ]); + + const handleExpectedNativeSessionClose = useCallback((reason: string): void => { + console.log("[Recovery] Treating signaling close as ended session:", reason); + const activeGameId = streamingGameRef.current?.id; + if (activeGameId) { + endPlaytimeSession(activeGameId); + } + markExplicitSignalingShutdown(); + clientRef.current?.dispose(); + clientRef.current = null; + launchInFlightRef.current = false; + resetLaunchRuntime(); + void refreshNavbarActiveSession(); + }, [endPlaytimeSession, markExplicitSignalingShutdown, refreshNavbarActiveSession, resetLaunchRuntime]); + + useSignalingEvents({ + runtime: streamRuntime, + attemptSessionRecovery, + diagnosticsStore, + handleExpectedNativeSessionClose, + markDiscordStreamStarted, + refreshNavbarActiveSession, + resetLaunchRuntime, + scheduleStableRecoveryReset, + settings, + t, + }); + + const { handlePlayGame } = useGameLaunch({ + runtime: streamRuntime, activeSessionProxyUrl, allKnownGames, + authSession, buildCurrentStreamSettings, buildSignalingConnectRequest, + canLaunch: Boolean(selectedProvider), claimAndConnectSession, + disconnectSignalingControlled, effectiveStreamingBaseUrl, + queueAdPlaybackRef, refreshNavbarActiveSession, - resetSignalingRecoveryState, resetLaunchRuntime, + resetSignalingRecoveryState, resetStatsOverlayToPreference, resolveInstallToPlayStreamingBaseUrl, resolveSubscriptionInfoForLaunch, - selectedProvider, - settings.enablePersistingInGameSettings, - streamStatus, + settings, + startPlaytimeSession, + subscriptionInfo, t, variantByGameId, warmNativeStreamerForLaunch, - ]); + }); useEffect(() => { const request = pendingDirectLaunchRequest; @@ -3892,257 +1978,126 @@ export function App(): JSX.Element { }); }, [activeSessionProxyUrl, authSession, effectiveStreamingBaseUrl, handleOpenStoreUrl]); - useEffect(() => { - if (!logoutConfirmOpen && !removeAccountConfirmOpen) return; - - const handleKeyDown = (event: KeyboardEvent) => { - if (event.key === "Escape") { - if (removeAccountConfirmOpen) { - setRemoveAccountConfirmOpen(false); - setAccountToRemove(null); - } else if (logoutConfirmOpen) { - setLogoutConfirmOpen(false); - } - } - if (event.key === "Enter") { - event.preventDefault(); - if (removeAccountConfirmOpen) { - void confirmRemoveAccount(); - } else if (logoutConfirmOpen) { - void confirmLogout(); - } - } - }; - - window.addEventListener("keydown", handleKeyDown); - const previousOverflow = document.body.style.overflow; - document.body.style.overflow = "hidden"; - - return () => { - window.removeEventListener("keydown", handleKeyDown); - document.body.style.overflow = previousOverflow; - }; - }, [confirmLogout, confirmRemoveAccount, logoutConfirmOpen, removeAccountConfirmOpen]); - - const accountToRemoveDisplayName = useMemo(() => ( - savedAccounts.find((account) => account.userId === accountToRemove)?.displayName ?? t("auth.accounts.thisAccount") - ), [accountToRemove, savedAccounts, locale, t]); - - const logoutConfirmModal = logoutConfirmOpen && typeof document !== "undefined" - ? createPortal( -
- - -
-
- Enter {t("app.actions.confirm")} · Esc {t("app.actions.cancel")} -
-
-
, - document.body, - ) - : null; - - const removeAccountConfirmModal = removeAccountConfirmOpen && typeof document !== "undefined" - ? createPortal( -
- - -
-
- Enter {t("app.actions.confirm")} · Esc {t("app.actions.cancel")} -
-
-
, - document.body, - ) - : null; - - const handleResumeFromNavbar = useCallback(async () => { - if ( - !selectedProvider - || !navbarActiveSession - || isResumingNavbarSession - || isTerminatingNavbarSession - || navbarSessionActionInFlightRef.current - ) { - return; - } - if (launchInFlightRef.current || streamStatus !== "idle") { - return; - } + const closeRemoveAccountConfirm = (): void => { + setRemoveAccountConfirmOpen(false); + setAccountToRemove(null); + }; - navbarSessionActionInFlightRef.current = "resume"; - launchInFlightRef.current = true; - resetSignalingRecoveryState(); - setIsResumingNavbarSession(true); - let loadingStep: StreamLoadingStatus = "setup"; - const updateLoadingStep = (next: StreamLoadingStatus): void => { - loadingStep = next; - setStreamStatus(next); - }; + const logoutConfirmModal = ( + setLogoutConfirmOpen(false)} + onConfirm={() => { + void confirmLogout(); + }} + onExitComplete={() => setLogoutConfirmSurfacePresent(false)} + motion="compact" + overlayClassName="logout-confirm" + backdropClassName="logout-confirm-backdrop" + panelClassName="logout-confirm-card" + ariaLabel={t("auth.accounts.logOutConfirmation")} + backdropLabel={t("auth.accounts.cancelLogOut")} + initialFocusRef={logoutConfirmCancelRef} + restoreFocusRef={accountConfirmRestoreFocusRef} + > +
{t("auth.accounts.kicker")}
+

{t("auth.accounts.signOutAllTitle")}

+

+ {t("auth.accounts.signOutAllDescription")} +

+

+ {t("auth.accounts.signOutAllSubtext")} +

+
+ + +
+
+ Enter {t("app.actions.confirm")} · Esc {t("app.actions.cancel")} +
+
+ ); - setLaunchError(null); - setQueuePosition(undefined); - setSessionStartedAtMs(null); - setRemoteStreamWarning(null); - setLocalSessionTimerWarning(null); - resetStatsOverlayToPreference(); - const matchedContext = findGameContextForSession(navbarActiveSession); - let resumeGameContext: GameInfo | null = null; - if (matchedContext) { - resumeGameContext = matchedContext.game; - setStreamingGame(matchedContext.game); - setStreamingStore(matchedContext.variant?.store ?? null); - } else { - setStreamingStore(null); - } - updateLoadingStep("setup"); + const removeAccountConfirmModal = ( + { + void confirmRemoveAccount(); + }} + onExitComplete={() => setRemoveAccountConfirmSurfacePresent(false)} + motion="compact" + overlayClassName="logout-confirm" + backdropClassName="logout-confirm-backdrop" + panelClassName="logout-confirm-card" + ariaLabel={t("auth.accounts.removeAccountConfirmation")} + backdropLabel={t("auth.accounts.cancelAccountRemoval")} + initialFocusRef={removeAccountConfirmCancelRef} + restoreFocusRef={accountConfirmRestoreFocusRef} + > +
{t("auth.accounts.kicker")}
+

{t("auth.accounts.removeAccountTitle")}

+

+ {t("auth.accounts.removeAccountDescription", { name: accountToRemoveDisplayName })} +

+

+ {t("auth.accounts.removeAccountSubtext")} +

+
+ + +
+
+ Enter {t("app.actions.confirm")} · Esc {t("app.actions.cancel")} +
+
+ ); - try { - signalingRecoveryRef.current.appId = navbarActiveSession.appId; - await claimAndConnectSession(navbarActiveSession); - setNavbarActiveSession(null); - } catch (error) { - console.error("Navbar resume failed:", error); - setLaunchError(toLaunchErrorState(t, error, loadingStep, resumeGameContext)); - await disconnectSignalingControlled(); - clientRef.current?.dispose(); - clientRef.current = null; - resetLaunchRuntime({ keepLaunchError: true }); - void refreshNavbarActiveSession(); - } finally { - navbarSessionActionInFlightRef.current = null; - launchInFlightRef.current = false; - setIsResumingNavbarSession(false); - } - }, [ + const { handleResumeFromNavbar, handleTerminateNavbarSession } = useActiveSessionActions({ + runtime: streamRuntime, + canResume: Boolean(selectedProvider), claimAndConnectSession, - isTerminatingNavbarSession, - isResumingNavbarSession, - navbarActiveSession, + disconnectSignalingControlled, + effectiveStreamingBaseUrl, findGameContextForSession, + gameTitleByAppId, refreshNavbarActiveSession, - resetSignalingRecoveryState, resetLaunchRuntime, + resetSignalingRecoveryState, resetStatsOverlayToPreference, - selectedProvider, - streamStatus, - t, - ]); - - const handleTerminateNavbarSession = useCallback(async () => { - if ( - !navbarActiveSession - || isResumingNavbarSession - || isTerminatingNavbarSession - || navbarSessionActionInFlightRef.current - ) { - return; - } - if (launchInFlightRef.current || streamStatus !== "idle") { - return; - } - - const activeSessionTitle = gameTitleByAppId.get(navbarActiveSession.appId)?.trim() || t("session.thisSession"); - if (!window.confirm(t("session.terminateConfirmation", { title: activeSessionTitle }))) { - return; - } - - navbarSessionActionInFlightRef.current = "terminate"; - setIsTerminatingNavbarSession(true); - try { - await stopSessionByTarget({ - sessionId: navbarActiveSession.sessionId, - zone: "", - streamingBaseUrl: navbarActiveSession.streamingBaseUrl ?? (effectiveStreamingBaseUrl || undefined), - serverIp: navbarActiveSession.serverIp, - }); - setNavbarActiveSession(null); - } catch (error) { - console.error("Navbar terminate failed:", error); - } finally { - navbarSessionActionInFlightRef.current = null; - setIsTerminatingNavbarSession(false); - void refreshNavbarActiveSession(); - } - }, [ - effectiveStreamingBaseUrl, - gameTitleByAppId, - isResumingNavbarSession, - isTerminatingNavbarSession, - navbarActiveSession, - refreshNavbarActiveSession, stopSessionByTarget, - streamStatus, t, - ]); + }); // Stop stream handler const handleStopStream = useCallback(async () => { @@ -4475,15 +2430,21 @@ export function App(): JSX.Element { setReleaseHighlightsIsAuto(false); }, [releaseHighlightsIsAuto]); - const handleSettingsExitComplete = useCallback((): void => { - setSettingsMounted(false); - }, []); - - useEffect(() => { - if (currentPage === "settings") { - setSettingsMounted(true); + const handleErrorReportingConsent = useCallback(async (granted: boolean): Promise => { + await updateSetting("errorReportingConsent", granted ? "granted" : "denied"); + try { + const refreshed = await window.openNow.getSettings(); + setSettings((prev) => ({ + ...prev, + errorReportingConsent: refreshed.errorReportingConsent, + telemetryInstallId: refreshed.telemetryInstallId, + })); + } catch (error) { + console.warn("[Telemetry] Failed to refresh settings after consent change:", error); } - }, [currentPage]); + }, [updateSetting]); + + const showErrorReportingConsent = settingsLoaded && settings.errorReportingConsent === "unset"; const mainPage: AppPage = currentPage === "settings" ? pageBeforeSettings : currentPage; @@ -4505,13 +2466,27 @@ export function App(): JSX.Element { qrLoginChallenge={qrLoginChallenge} isQrLoginPending={activeLoginMode === "qr" && !qrLoginChallenge} /> - {releaseHighlightsPayload && ( - - )} + setReleaseHighlightsSurfacePresent(false)} + /> + { + void handleErrorReportingConsent(true); + }} + onDecline={() => { + void handleErrorReportingConsent(false); + }} + onExitComplete={() => setConsentSurfacePresent(false)} + /> + setFeedbackOpen(false)} + onExitComplete={() => setFeedbackSurfacePresent(false)} + /> ); } @@ -4519,137 +2494,69 @@ export function App(): JSX.Element { const showLaunchOverlay = streamStatus !== "idle" || launchError !== null; const hasActiveStreamView = streamStatus !== "idle"; const showLaunchErrorOverlay = launchError !== null; - const showDesktopLaunchLoading = showLaunchErrorOverlay || (streamStatus !== "idle" && streamStatus !== "streaming"); - // Show stream lifecycle (waiting/connecting/streaming/failure) - if (showLaunchOverlay) { - const loadingStatus = launchError ? launchError.stage : toLoadingStatus(streamStatus); - return ( - <> - {hasActiveStreamView && ( - { - void toggleSessionFullscreen(); - }} - onConfirmExit={handleExitPromptConfirm} - onCancelExit={handleExitPromptCancel} - onEndSession={() => { - void handlePromptedStopStream(); - }} - onToggleMicrophone={() => { - clientRef.current?.toggleMicrophone(); - }} - mouseSensitivity={settings.mouseSensitivity} - onMouseSensitivityChange={handleMouseSensitivityChange} - mouseAcceleration={settings.mouseAcceleration} - onMouseAccelerationChange={handleMouseAccelerationChange} - microphoneMode={settings.microphoneMode} - onMicrophoneModeChange={handleMicrophoneModeChange} - onScreenshotShortcutChange={(value) => { - void updateSetting("shortcutScreenshot", value); - }} - onRecordingShortcutChange={(value) => { - void updateSetting("shortcutToggleRecording", value); - }} - onShowSessionTimeRemainingInStatsOverlayChange={(value) => { - void updateSetting("showSessionTimeRemainingInStatsOverlay", value); - }} - subscriptionInfo={subscriptionInfo} - micTrack={clientRef.current?.getMicTrack() ?? null} - onRequestPointerLock={handleRequestPointerLock} - onReleasePointerLock={() => { - void releasePointerLockIfNeeded(); - }} - onNativeInputPaused={setNativeInputPaused} - allowEscapeToExitFullscreen={settings.allowEscapeToExitFullscreen} - videoShader={settings.videoShader} - onVideoShaderChange={handleVideoShaderChange} - /> - )} - {showDesktopLaunchLoading && ( - { - if (launchError) { - void handleDismissLaunchError(); - return; - } - void handlePromptedStopStream(); - }} - /> - )} - - ); - } + const showDesktopLaunchLoading = showLaunchErrorOverlay + || (streamStatus !== "idle" && streamRevealPhase !== "revealed"); + const loadingStatus = launchError + ? launchError.stage + : streamStatus === "streaming" + ? "connecting" + : toLoadingStatus(streamStatus); + const showCatalogAtmosphere = mainPage === "home" || mainPage === "library"; + const shellBlocked = showLaunchOverlay + || streamSurfacePresent + || launchSurfacePresent + || currentPage === "settings" + || settingsSurfacePresent + || navbarOverlayBlocking + || queueModalGame !== null + || releaseHighlightsPayload !== null + || releaseHighlightsSurfacePresent + || showErrorReportingConsent + || consentSurfacePresent + || feedbackOpen + || feedbackSurfacePresent + || logoutConfirmOpen + || logoutConfirmSurfacePresent + || removeAccountConfirmOpen + || removeAccountConfirmSurfacePresent; + const catalogSurfaceActive = !shellBlocked; - // Main app layout return ( -
- {startupRefreshNotice && ( -
- {startupRefreshNotice.text} -
- )} - {catalogActionNotice && ( -
- {catalogActionNotice.text} -
+
+
+ {showCatalogAtmosphere && ( + )} + + {startupRefreshNotice && ( + + {startupRefreshNotice.text} + + )} + + + {catalogActionNotice && ( + + {catalogActionNotice.text} + + )} + { + onRemoveAccount={(userId, restoreFocusTarget) => { + accountConfirmRestoreFocusRef.current = restoreFocusTarget ?? null; void handleRemoveAccount(userId); }} onAddAccount={handleAddAccount} - onLogoutAll={handleLogout} + onLogoutAll={(restoreFocusTarget) => { + accountConfirmRestoreFocusRef.current = restoreFocusTarget ?? null; + handleLogout(); + }} + onOpenFeedback={() => setFeedbackOpen(true)} + onBlockingOverlayChange={setNavbarOverlayBlocking} controllerMode={effectiveControllerMode} />
+ option.id !== "relevance")} - selectedSortId={catalogSelectedSortId === "relevance" ? "last_played" : catalogSelectedSortId} - onSortChange={setCatalogSelectedSortId} - controllerMode={effectiveControllerMode} - featuredGames={featuredGames.length > 0 ? featuredGames : games} - activeSessionAppIds={activeSessionAppIds} - onBuyGame={handleBuyGame} - onPreviousControllerPage={() => navigateControllerPage(-1)} - onNextControllerPage={() => navigateControllerPage(1)} - /> + + option.id !== "relevance")} + selectedSortId={catalogSelectedSortId === "relevance" ? "last_played" : catalogSelectedSortId} + onSortChange={setCatalogSelectedSortId} + controllerMode={effectiveControllerMode} + surfaceActive={catalogSurfaceActive} + featuredGames={featuredGames.length > 0 ? featuredGames : games} + activeSessionAppIds={activeSessionAppIds} + onBuyGame={handleBuyGame} + onPreviousControllerPage={() => navigateControllerPage(-1)} + onNextControllerPage={() => navigateControllerPage(1)} + /> + )} +
+
+ + setStreamSurfacePresent(false)} + > + {hasActiveStreamView && ( + + { + void toggleSessionFullscreen(); + }} + onConfirmExit={handleExitPromptConfirm} + onCancelExit={handleExitPromptCancel} + onEndSession={() => { + void handlePromptedStopStream(); + }} + onToggleMicrophone={() => { + clientRef.current?.toggleMicrophone(); + }} + mouseSensitivity={settings.mouseSensitivity} + onMouseSensitivityChange={handleMouseSensitivityChange} + mouseAcceleration={settings.mouseAcceleration} + onMouseAccelerationChange={handleMouseAccelerationChange} + microphoneMode={settings.microphoneMode} + onMicrophoneModeChange={handleMicrophoneModeChange} + onScreenshotShortcutChange={(value) => { + void updateSetting("shortcutScreenshot", value); + }} + onRecordingShortcutChange={(value) => { + void updateSetting("shortcutToggleRecording", value); + }} + onShowSessionTimeRemainingInStatsOverlayChange={(value) => { + void updateSetting("showSessionTimeRemainingInStatsOverlay", value); + }} + subscriptionInfo={subscriptionInfo} + micTrack={clientRef.current?.getMicTrack() ?? null} + onRequestPointerLock={handleRequestPointerLock} + onReleasePointerLock={() => { + void releasePointerLockIfNeeded(); + }} + onNativeInputPaused={setNativeInputPaused} + allowEscapeToExitFullscreen={settings.allowEscapeToExitFullscreen} + videoShader={settings.videoShader} + onVideoShaderChange={handleVideoShaderChange} + /> + + )} + + + setLaunchSurfacePresent(false)} + > + {showDesktopLaunchLoading && ( + { + if (streamRevealPhase === "revealing" && !launchError) { + setStreamRevealPhase("revealed"); + } + }} + > + { + if (launchError) { + void handleDismissLaunchError(); + return; + } + void handlePromptedStopStream(); + }} + /> + + )} + + setSettingsSurfacePresent(false)} > - {settingsMounted && ( - - )} + setFeedbackOpen(true)} + /> {logoutConfirmModal} {removeAccountConfirmModal} @@ -4773,13 +2839,27 @@ export function App(): JSX.Element { onCancel={handleQueueModalCancel} /> )} - {releaseHighlightsPayload && ( - - )} + setReleaseHighlightsSurfacePresent(false)} + /> + { + void handleErrorReportingConsent(true); + }} + onDecline={() => { + void handleErrorReportingConsent(false); + }} + onExitComplete={() => setConsentSurfacePresent(false)} + /> + setFeedbackOpen(false)} + onExitComplete={() => setFeedbackSurfacePresent(false)} + />
); } diff --git a/opennow-stable/src/renderer/src/components/ErrorReportingConsentModal.tsx b/opennow-stable/src/renderer/src/components/ErrorReportingConsentModal.tsx new file mode 100644 index 000000000..2e741804d --- /dev/null +++ b/opennow-stable/src/renderer/src/components/ErrorReportingConsentModal.tsx @@ -0,0 +1,68 @@ +import { useRef, type JSX } from "react"; +import { useTranslation } from "../i18n"; +import { ModalSurface } from "./ui/ModalSurface"; + +export interface ErrorReportingConsentModalProps { + open: boolean; + onAccept: () => void; + onDecline: () => void; + onExitComplete?: () => void; +} + +export function ErrorReportingConsentModal({ + open, + onAccept, + onDecline, + onExitComplete, +}: ErrorReportingConsentModalProps): JSX.Element { + const { t } = useTranslation(); + const primaryActionRef = useRef(null); + + return ( + +
+
{t("telemetryConsent.kicker")}
+

{t("telemetryConsent.title")}

+
+ +
+

{t("telemetryConsent.body")}

+
    +
  • {t("telemetryConsent.sendsErrors")}
  • +
  • {t("telemetryConsent.sendsAppOpens")}
  • +
  • {t("telemetryConsent.sendsAnonymousId")}
  • +
  • {t("telemetryConsent.neverSendsAccount")}
  • +
  • {t("telemetryConsent.neverSendsGameplay")}
  • +
+

{t("telemetryConsent.changeLater")}

+
+ +
+ + +
+
+ ); +} diff --git a/opennow-stable/src/renderer/src/components/FeedbackModal.tsx b/opennow-stable/src/renderer/src/components/FeedbackModal.tsx new file mode 100644 index 000000000..e278f4f5d --- /dev/null +++ b/opennow-stable/src/renderer/src/components/FeedbackModal.tsx @@ -0,0 +1,189 @@ +import { MessageSquareText, X } from "lucide-react"; +import { useEffect, useRef, useState, type FormEvent, type JSX } from "react"; +import type { Settings } from "@shared/gfn"; +import { FEEDBACK_CATEGORIES, type FeedbackCategory } from "@shared/telemetry"; +import { useTranslation } from "../i18n"; +import { captureFeedback, isTelemetryConfigured } from "../telemetry/posthog"; +import { ModalSurface } from "./ui/ModalSurface"; +import { SelectDropdown } from "./ui/SelectDropdown"; + +export interface FeedbackModalProps { + open: boolean; + settings: Settings; + onClose: () => void; + onExitComplete?: () => void; +} + +export function FeedbackModal({ + open, + settings, + onClose, + onExitComplete, +}: FeedbackModalProps): JSX.Element { + const { t } = useTranslation(); + const messageRef = useRef(null); + const [category, setCategory] = useState("bug"); + const [message, setMessage] = useState(""); + const [includeSystemInfo, setIncludeSystemInfo] = useState(true); + const [includeLogs, setIncludeLogs] = useState(true); + const [submitting, setSubmitting] = useState(false); + const [error, setError] = useState(null); + const [submitted, setSubmitted] = useState(false); + + useEffect(() => { + if (!open) { + return; + } + setCategory("bug"); + setMessage(""); + setIncludeSystemInfo(true); + setIncludeLogs(true); + setSubmitting(false); + setError(null); + setSubmitted(false); + }, [open]); + + const handleSubmit = async (event: FormEvent): Promise => { + event.preventDefault(); + const trimmed = message.trim(); + if (!trimmed) { + setError(t("feedback.errors.empty")); + return; + } + if (!isTelemetryConfigured()) { + setError(t("feedback.errors.notConfigured")); + return; + } + + setSubmitting(true); + setError(null); + try { + const ok = await captureFeedback(settings, { + category, + message: trimmed, + includeSystemInfo, + includeLogs, + }); + if (!ok) { + setError(t("feedback.errors.sendFailed")); + return; + } + setSubmitted(true); + } catch (submitError) { + console.error("[Feedback] Failed to send feedback:", submitError); + setError(t("feedback.errors.sendFailed")); + } finally { + setSubmitting(false); + } + }; + + return ( + +
+
{t("feedback.kicker")}
+

{t("feedback.title")}

+ +
+ + {submitted ? ( + <> +
+

{t("feedback.success")}

+
+
+ +
+ + ) : ( +
void handleSubmit(event)} + > +
+ + ({ + value, + label: t(`feedback.categories.${value}`), + }))} + onChange={(value) => setCategory(value as FeedbackCategory)} + ariaLabel={t("feedback.category")} + /> + + +