From efda8eb98fa317fda7f0231b45cc00692a8e6184 Mon Sep 17 00:00:00 2001 From: dokril Date: Thu, 10 Sep 2026 20:59:52 +0300 Subject: [PATCH] Release v2.0.0 --- .agent/skills/repository-orientation/SKILL.md | 22 +- .agent/skills/testing-ci-release/SKILL.md | 117 +- .../windows-services-powershell/SKILL.md | 112 +- .github/workflows/ci.yml | 23 +- .gitignore | 2 + AGENTS.md | 49 +- CONTRIBUTING.md | 9 +- README.md | 312 +- THIRD_PARTY_NOTICES.md | 41 + package-lock.json | 4 +- package.json | 4 +- pnpm-lock.yaml | 2356 ------ release.cmd | 8 + scripts/audit-windows-smoke.ps1 | 39 +- scripts/check-runtime-powershell-boundary.ps1 | 253 + scripts/install-control-app.ps1 | 79 - scripts/install-proxyfier.ps1 | 96 - scripts/install-singbox.ps1 | 270 - scripts/prepare-release.check.mjs | 262 + scripts/prepare-release.ps1 | 322 +- scripts/update-component-bundle.ps1 | 1917 +++++ scripts/update-proxifyre-bundle.ps1 | 143 - src-tauri/Cargo.lock | 46 +- src-tauri/Cargo.toml | 22 +- .../cleanup/uninstall-managed-components.ps1 | 250 - src-tauri/bundled/components/catalog.json | 130 + .../bundled/components/proxifyre/LICENSE | 661 ++ .../ProxiFyre-v2.4.0-x64-signed.zip} | Bin 1372483 -> 1519694 bytes src-tauri/bundled/components/sing-box/LICENSE | 17 + .../sing-box-1.13.19-windows-amd64.zip | Bin 0 -> 21046252 bytes .../components/vc-runtime/LICENSE.docx | Bin 0 -> 39553 bytes .../vc-runtime/VC_redist.x64.exe} | Bin .../components/windows-packet-filter/LICENSE | 21 + .../Windows.Packet.Filter.3.6.2.1.x64.msi | Bin .../bundled/components/winsw/LICENSE.txt | 21 + .../bundled/components/winsw/WinSW.NET461.exe | Bin 0 -> 655872 bytes .../installer-hooks/installer-template.nsi | 809 +++ .../installer-hooks/proxywarden-hooks.nsh | 37 +- src-tauri/bundled/proxifyre/manifest.json | 31 - src-tauri/capabilities/default.json | 2 +- src-tauri/src/adapters/proxifyre.rs | 6 +- src-tauri/src/adapters/singbox.rs | 202 +- src-tauri/src/admin.rs | 76 +- src-tauri/src/apply_flow.rs | 431 +- src-tauri/src/command_dto.rs | 311 +- src-tauri/src/commands.rs | 1775 ++++- src-tauri/src/component_catalog.rs | 869 +++ src-tauri/src/component_cutover.rs | 6454 +++++++++++++++++ src-tauri/src/component_detection.rs | 1452 +++- src-tauri/src/component_inventory.rs | 924 +++ src-tauri/src/component_packages.rs | 2940 ++++++++ src-tauri/src/component_status.rs | 59 +- src-tauri/src/configuration_transaction.rs | 295 + src-tauri/src/configuration_use_case.rs | 492 +- src-tauri/src/elevated_scripts.rs | 19 - src-tauri/src/helper.rs | 184 - src-tauri/src/lib.rs | 131 +- src-tauri/src/main.rs | 4 + src-tauri/src/migration.rs | 1630 +++++ src-tauri/src/models.rs | 49 +- src-tauri/src/nsis_runtime.rs | 1080 +++ src-tauri/src/nsis_runtime_tests.rs | 993 +++ src-tauri/src/powershell.rs | 120 - src-tauri/src/privileged_jobs.rs | 3489 +++++++++ src-tauri/src/privileged_runtime.rs | 2928 ++++++++ src-tauri/src/process.rs | 3942 +++++++++- src-tauri/src/proxifyre_ownership.rs | 67 +- src-tauri/src/proxifyre_runtime.rs | 5591 +++++++++++--- .../proxifyre_runtime/cutover_scm_tests.rs | 921 +++ .../src/proxifyre_runtime/native_tests.rs | 1324 ++++ src-tauri/src/proxifyre_scripts.rs | 687 -- src-tauri/src/proxy_apply.rs | 124 +- src-tauri/src/route_state.rs | 274 + src-tauri/src/safe_fs.rs | 4732 +++++++++++- src-tauri/src/singbox_config.rs | 29 +- src-tauri/src/singbox_runtime.rs | 1797 +++-- src-tauri/src/singbox_runtime/native_tests.rs | 1072 +++ src-tauri/src/singbox_runtime/system.rs | 1978 +++++ src-tauri/src/singbox_service.rs | 273 +- src-tauri/src/singbox_subscription.rs | 185 +- src-tauri/src/storage.rs | 173 +- src-tauri/src/subscription.rs | 11 +- src-tauri/tauri.conf.json | 12 +- src-tauri/tests/apply_flow_tests.rs | 69 + src-tauri/tests/authenticode_tests.rs | 81 + src-tauri/tests/command_tests.rs | 543 +- src-tauri/tests/component_catalog_tests.rs | 640 ++ src-tauri/tests/component_cutover_tests.rs | 2312 ++++++ src-tauri/tests/component_detection_tests.rs | 498 +- src-tauri/tests/component_inventory_tests.rs | 666 ++ .../tests/component_package_update_tests.rs | 1219 ++++ src-tauri/tests/component_packages_tests.rs | 423 ++ .../tests/configuration_transaction_tests.rs | 177 + src-tauri/tests/fixture_contract_tests.rs | 938 +++ src-tauri/tests/fixtures/legacy/contract.json | 445 ++ .../markers/install-proxyfier.marker.json | 6 + .../legacy/markers/proxywarden-component.json | 7 + .../pre-1.2-split/config/components.json | 24 + .../pre-1.2-split/config/local-singbox.json | 9 + .../legacy/pre-1.2-split/config/profiles.json | 29 + .../legacy/pre-1.2-split/config/targets.json | 11 + .../proxifyre-generated/app-config.json | 18 + .../proxifyre-real-sanitized/app-config.json | 23 + .../proxifyre-unsupported/app-config.json | 25 + src-tauri/tests/helper_tests.rs | 134 - src-tauri/tests/lifecycle_preflight_tests.rs | 136 + src-tauri/tests/migration_tests.rs | 1132 +++ src-tauri/tests/privileged_jobs_tests.rs | 1825 +++++ .../tests/privileged_package_trust_tests.rs | 346 + src-tauri/tests/privileged_runtime_tests.rs | 1325 ++++ src-tauri/tests/singbox_adapter_tests.rs | 43 + src-tauri/tests/singbox_command_tests.rs | 112 + src-tauri/tests/singbox_service_tests.rs | 99 +- src-tauri/tests/storage_tests.rs | 66 +- src/api/tauriCommands.ts | 125 +- src/app/App.tsx | 1391 ++-- src/app/components/AppList.test.ts | 39 +- src/app/components/AppList.tsx | 408 +- .../ComponentPackageStatus.test.tsx | 296 + src/app/components/ComponentPackageStatus.tsx | 389 + src/app/components/MigrationNotice.test.tsx | 174 + src/app/components/MigrationNotice.tsx | 404 ++ src/app/components/ProxiFyreSetupStrip.tsx | 143 - .../components/SummaryStatusControl.test.tsx | 51 + src/app/components/SummaryStatusControl.tsx | 40 +- src/app/hooks/useApplyFlow.test.ts | 35 + src/app/hooks/useApplyFlow.ts | 37 + src/app/hooks/useComponentPackages.test.ts | 172 + src/app/hooks/useComponentPackages.ts | 364 + src/app/hooks/useConfigurationDraft.test.ts | 114 + src/app/hooks/useConfigurationDraft.ts | 99 + src/app/hooks/useSubscription.ts | 28 + src/app/lib/parseProxy.ts | 9 + src/app/lib/snapshots.test.ts | 4 +- src/app/lib/snapshots.ts | 28 +- src/app/readiness.test.ts | 105 +- src/app/readiness.ts | 103 +- src/app/viewModel.ts | 62 +- src/domain/types.ts | 122 + src/main.tsx | 1 + src/styles/app.css | 591 +- src/styles/proxifyre.css | 334 + 142 files changed, 68308 insertions(+), 9333 deletions(-) create mode 100644 THIRD_PARTY_NOTICES.md delete mode 100644 pnpm-lock.yaml create mode 100644 release.cmd create mode 100644 scripts/check-runtime-powershell-boundary.ps1 delete mode 100644 scripts/install-control-app.ps1 delete mode 100644 scripts/install-proxyfier.ps1 delete mode 100644 scripts/install-singbox.ps1 create mode 100644 scripts/prepare-release.check.mjs create mode 100644 scripts/update-component-bundle.ps1 delete mode 100644 scripts/update-proxifyre-bundle.ps1 delete mode 100644 src-tauri/bundled/cleanup/uninstall-managed-components.ps1 create mode 100644 src-tauri/bundled/components/catalog.json create mode 100644 src-tauri/bundled/components/proxifyre/LICENSE rename src-tauri/bundled/{proxifyre/ProxiFyre-v2.2.1-x64-signed.zip => components/proxifyre/ProxiFyre-v2.4.0-x64-signed.zip} (62%) create mode 100644 src-tauri/bundled/components/sing-box/LICENSE create mode 100644 src-tauri/bundled/components/sing-box/sing-box-1.13.19-windows-amd64.zip create mode 100644 src-tauri/bundled/components/vc-runtime/LICENSE.docx rename src-tauri/bundled/{proxifyre/vc_redist.x64.exe => components/vc-runtime/VC_redist.x64.exe} (100%) create mode 100644 src-tauri/bundled/components/windows-packet-filter/LICENSE rename src-tauri/bundled/{proxifyre => components/windows-packet-filter}/Windows.Packet.Filter.3.6.2.1.x64.msi (100%) create mode 100644 src-tauri/bundled/components/winsw/LICENSE.txt create mode 100644 src-tauri/bundled/components/winsw/WinSW.NET461.exe create mode 100644 src-tauri/bundled/installer-hooks/installer-template.nsi delete mode 100644 src-tauri/bundled/proxifyre/manifest.json create mode 100644 src-tauri/src/component_catalog.rs create mode 100644 src-tauri/src/component_cutover.rs create mode 100644 src-tauri/src/component_inventory.rs create mode 100644 src-tauri/src/component_packages.rs create mode 100644 src-tauri/src/configuration_transaction.rs delete mode 100644 src-tauri/src/elevated_scripts.rs delete mode 100644 src-tauri/src/helper.rs create mode 100644 src-tauri/src/migration.rs create mode 100644 src-tauri/src/nsis_runtime.rs create mode 100644 src-tauri/src/nsis_runtime_tests.rs delete mode 100644 src-tauri/src/powershell.rs create mode 100644 src-tauri/src/privileged_jobs.rs create mode 100644 src-tauri/src/privileged_runtime.rs create mode 100644 src-tauri/src/proxifyre_runtime/cutover_scm_tests.rs create mode 100644 src-tauri/src/proxifyre_runtime/native_tests.rs delete mode 100644 src-tauri/src/proxifyre_scripts.rs create mode 100644 src-tauri/src/route_state.rs create mode 100644 src-tauri/src/singbox_runtime/native_tests.rs create mode 100644 src-tauri/src/singbox_runtime/system.rs create mode 100644 src-tauri/tests/authenticode_tests.rs create mode 100644 src-tauri/tests/component_catalog_tests.rs create mode 100644 src-tauri/tests/component_cutover_tests.rs create mode 100644 src-tauri/tests/component_inventory_tests.rs create mode 100644 src-tauri/tests/component_package_update_tests.rs create mode 100644 src-tauri/tests/component_packages_tests.rs create mode 100644 src-tauri/tests/configuration_transaction_tests.rs create mode 100644 src-tauri/tests/fixture_contract_tests.rs create mode 100644 src-tauri/tests/fixtures/legacy/contract.json create mode 100644 src-tauri/tests/fixtures/legacy/markers/install-proxyfier.marker.json create mode 100644 src-tauri/tests/fixtures/legacy/markers/proxywarden-component.json create mode 100644 src-tauri/tests/fixtures/legacy/pre-1.2-split/config/components.json create mode 100644 src-tauri/tests/fixtures/legacy/pre-1.2-split/config/local-singbox.json create mode 100644 src-tauri/tests/fixtures/legacy/pre-1.2-split/config/profiles.json create mode 100644 src-tauri/tests/fixtures/legacy/pre-1.2-split/config/targets.json create mode 100644 src-tauri/tests/fixtures/legacy/proxifyre-generated/app-config.json create mode 100644 src-tauri/tests/fixtures/legacy/proxifyre-real-sanitized/app-config.json create mode 100644 src-tauri/tests/fixtures/legacy/proxifyre-unsupported/app-config.json delete mode 100644 src-tauri/tests/helper_tests.rs create mode 100644 src-tauri/tests/lifecycle_preflight_tests.rs create mode 100644 src-tauri/tests/migration_tests.rs create mode 100644 src-tauri/tests/privileged_jobs_tests.rs create mode 100644 src-tauri/tests/privileged_package_trust_tests.rs create mode 100644 src-tauri/tests/privileged_runtime_tests.rs create mode 100644 src/app/components/ComponentPackageStatus.test.tsx create mode 100644 src/app/components/ComponentPackageStatus.tsx create mode 100644 src/app/components/MigrationNotice.test.tsx create mode 100644 src/app/components/MigrationNotice.tsx delete mode 100644 src/app/components/ProxiFyreSetupStrip.tsx create mode 100644 src/app/components/SummaryStatusControl.test.tsx create mode 100644 src/app/hooks/useApplyFlow.test.ts create mode 100644 src/app/hooks/useApplyFlow.ts create mode 100644 src/app/hooks/useComponentPackages.test.ts create mode 100644 src/app/hooks/useComponentPackages.ts create mode 100644 src/app/hooks/useConfigurationDraft.test.ts create mode 100644 src/app/hooks/useConfigurationDraft.ts create mode 100644 src/app/hooks/useSubscription.ts create mode 100644 src/styles/proxifyre.css diff --git a/.agent/skills/repository-orientation/SKILL.md b/.agent/skills/repository-orientation/SKILL.md index 3b48062..4917108 100644 --- a/.agent/skills/repository-orientation/SKILL.md +++ b/.agent/skills/repository-orientation/SKILL.md @@ -22,18 +22,30 @@ src/styles/app.css global/component CSS src-tauri/src/models.rs Rust domain models src-tauri/src/validation.rs input normalization/validation src-tauri/src/storage.rs JSON config/state storage +src-tauri/src/migration.rs versioned storage adoption/migration src-tauri/src/activity.rs activity log src-tauri/src/subscription.rs subscription fetch/parse -src-tauri/src/component_detection.rs component status detection +src-tauri/src/component_catalog.rs pinned offline package catalog +src-tauri/src/component_inventory.rs exact native SCM/process/registry inventory +src-tauri/src/component_detection.rs component status mapping +src-tauri/src/component_packages.rs bundled/cache package plans +src-tauri/src/component_cutover.rs durable legacy cutover/rollback/cleanup +src-tauri/src/privileged_jobs.rs sealed one-shot elevation records +src-tauri/src/privileged_runtime.rs fixed native privileged actions +src-tauri/src/proxifyre_runtime.rs native ProxiFyre lifecycle +src-tauri/src/singbox_runtime.rs native sing-box lifecycle src-tauri/src/adapters/* ProxiFyre/sing-box/proxy router adapters src-tauri/src/commands.rs Tauri command layer, currently too large src-tauri/tests/* Rust tests -scripts/*.ps1 Windows install/control scripts +scripts/*.ps1 build/release/QA tooling only ``` ## Source of truth - Persistent app config/state: `C:\ProgramData\ProxyWarden\config` and `state`. +- `config\components.json` is legacy migration input only; live component truth comes from native inventory and verified receipts. +- Current managed roots: `C:\Program Files\ProxyWarden\components\ProxiFyre` and `...\sing-box`. +- Offline baseline: packaged component catalog; verified downloaded cache: `C:\ProgramData\ProxyWarden\packages`. - Generated artifacts: `C:\ProgramData\ProxyWarden\generated`. - Frontend state is not source of truth. It should represent backend state and pending UI edits. - Component detection/runtime status should come from backend, not guessed in UI. @@ -45,13 +57,17 @@ scripts/*.ps1 Windows install/control scripts 3. Inspect the relevant source files listed above. 4. Determine whether the change crosses the Tauri boundary. If yes, update both Rust DTO/command and TypeScript wrapper/types. 5. Determine whether the change touches secrets, service control, generated configs, process execution, filesystem deletion, or network fetch. If yes, apply security checklist. -6. Prefer small, isolated changes over broad rewrites. +6. If the change touches install/service/elevation, trace `privileged_jobs.rs`/`privileged_runtime.rs` and the native component runtime. Do not introduce PowerShell runtime fallback. +7. If the change touches packaging or release tooling, run `scripts/check-runtime-powershell-boundary.ps1 -CheckOnly`. +8. Prefer small, isolated changes over broad rewrites. ## Do not - Do not treat `App.tsx` or `commands.rs` as the correct permanent architecture just because they currently contain lots of logic. - Do not introduce a second storage system. - Do not move service/install logic into frontend. +- Do not treat legacy `components.json`, fuzzy paths, or service name alone as ownership proof. +- Do not add `.ps1`, `powershell.exe`, `pwsh`, or generated script text to production Rust/Tauri/NSIS paths. - Do not claim Windows service/elevation behavior is verified unless it was actually tested on Windows. ## Output expectations diff --git a/.agent/skills/testing-ci-release/SKILL.md b/.agent/skills/testing-ci-release/SKILL.md index 059d5e1..91fff9b 100644 --- a/.agent/skills/testing-ci-release/SKILL.md +++ b/.agent/skills/testing-ci-release/SKILL.md @@ -2,7 +2,7 @@ ## Когда использовать -Используй этот skill при добавлении CI, release scripts, build fixes, test changes, dependency updates, packaging changes или перед финальным отчетом по крупной задаче. +Используй этот skill при изменениях CI, release scripts, tests, dependencies, Tauri/NSIS packaging, offline component catalog или перед финальным отчётом по крупной задаче. ## Minimal local checks @@ -10,109 +10,84 @@ Frontend: ```powershell npm ci +npm run format:check +npm run lint +npm run typecheck +npm test -- --run npm run build ``` Rust: ```powershell -cd src-tauri +Push-Location src-tauri cargo fmt --all -- --check cargo clippy --all-targets --all-features -- -D warnings cargo test --all-targets +Pop-Location ``` -Tauri: +Tauri/build/release boundaries: ```powershell npm run tauri -- info -npm run tauri -- build +& .\scripts\check-runtime-powershell-boundary.ps1 -CheckOnly +& .\scripts\update-component-bundle.ps1 -PlanOnly +& .\scripts\update-component-bundle.ps1 -CheckOnly +& .\scripts\audit-windows-smoke.ps1 -Mode PlanOnly +& .\scripts\prepare-release.ps1 -PlanOnly -SkipBuild ``` -PowerShell plan-only: - -```powershell -& .\scripts\install-control-app.ps1 -PlanOnly -& .\scripts\install-proxyfier.ps1 -PlanOnly -& .\scripts\install-singbox.ps1 -PlanOnly -``` +`PlanOnly`/`CheckOnly` должны возвращать structured JSON с `changed: false` и не менять repo, ProgramData, services или network state. ## Interaction smoke for UI motion -Build, lint, and unit tests do not validate motion or pointer behavior. For any hover, disclosure, stagger, or hit-target change, verify: +Build, lint и unit tests не проверяют motion/geometry. Для hover, disclosure, stagger или hit-target изменений проверь first/repeated/rapid toggle, keyboard focus, loading/long labels, `prefers-reduced-motion`, desktop и narrow window. Если visual smoke не выполнен, так и напиши. -- first open and first close; -- repeated and rapid toggle; -- hover and click before, during, and after transition; -- keyboard focus and hidden-control tab order; -- loading and longest localized labels; -- `prefers-reduced-motion`; -- desktop and narrow window geometry. +## CI contract -Use a controlled mock or preview state when backend status is difficult to reproduce. If no visual interaction smoke is possible, report that evidence as missing and do not claim the motion task is complete. +Windows baseline должен включать: -## CI recommendation +- frontend format/lint/typecheck/tests/build; +- Rust fmt/clippy/all-target tests; +- Tauri environment check; +- runtime PowerShell boundary check; +- offline bundle PlanOnly + CheckOnly; +- Windows audit PlanOnly; +- release preparation PlanOnly with build skipped. -Add GitHub Actions with at least: - -- frontend build on Windows and Ubuntu if practical; -- Rust fmt/clippy/test; -- PowerShell syntax/plan-only smoke on Windows; -- Tauri build on Windows for release branches/tags; -- artifact upload only for trusted release workflow. +CI не изображает реальную SCM/UAC/driver проверку. Artifact upload и tag/publish допустимы только в отдельном trusted release workflow после принятой VM evidence. ## Dependency updates -When changing dependencies: - -- Update lockfiles. -- Check Tauri v2 compatibility. -- Avoid adding large UI/runtime dependencies for tiny tasks. -- Avoid adding shell/process libraries that bypass existing backend boundaries. -- Note why dependency is needed. +- Обновить lockfiles. +- Проверить Tauri v2 и Windows x64 compatibility. +- Не добавлять dependency, если stdlib/native API или уже установленный crate решает задачу. +- Не добавлять shell/process library, возвращающую production PowerShell path. +- Объяснить, зачем dependency нужна и какой owner её вызывает. ## Release hygiene -Before release: +Перед release candidate: -- Verify app version in `package.json` and Tauri config if applicable. -- Verify icons/assets size. -- Verify CSP and capabilities. -- Verify no raw secrets/test URLs in repo. -- Verify installer scripts with `-PlanOnly`. -- Verify clean install on Windows VM. -- Verify external SOCKS5 flow. -- Verify local sing-box subscription flow. -- Verify uninstall/safe cleanup behavior. +- версии совпадают в `package.json`, `package-lock.json`, `src-tauri/tauri.conf.json` и `src-tauri/Cargo.toml`; +- packaged component catalog, asset hashes, licenses и `THIRD_PARTY_NOTICES.md` согласованы; +- installer содержит consolidated offline component bundle и WebView2 Offline Installer; +- runtime PowerShell checker проходит, bundled cleanup script отсутствует; +- NSIS hook разделяет verify-only upgrade и full managed uninstall; +- fresh offline VM, legacy upgrade/rollback, foreign service refusal, UAC cancel, uninstall/reboot и реальные routing flows записаны в evidence; +- tag/publish выполняются только для того же проверенного commit. -## Final report format +Установка Control App не должна скрыто install/start/update routing-компоненты. Payloads могут быть в installer, но component mutation остаётся отдельным user action. -```text -Changed: -- ... +## Финальный отчёт -Verified: -- npm run build -- cargo test +Разделить: -Not verified: -- Windows elevated install/uninstall, because ... +- `Проверено`: точные команды и результаты; +- `Не проверено`: Windows VM/UAC/SCM/driver/installer gaps; +- `Риски`: только конкретные release blockers. -Risks: -- ... -``` +Не писать «все тесты проходят», если весь релевантный набор действительно не запускался. -Do not write “all tests pass” unless all listed relevant tests actually ran. Humanity has enough fictional dashboards. - -## Как отчитываться - -Перед финальным ответом применить `.agent/skills/communication-reporting/SKILL.md` и `.agent/checklists/communication.md`. - -Минимум для нетривиальной задачи: - -- короткая сводка; -- таблица файлов `Файл / Что изменилось / Зачем`; -- важные места без пересказа каждой строки; -- что проверено; -- что не проверено; -- конкретные риски. +Перед ответом применить `.agent/skills/communication-reporting/SKILL.md` и `.agent/checklists/communication.md`. diff --git a/.agent/skills/windows-services-powershell/SKILL.md b/.agent/skills/windows-services-powershell/SKILL.md index 42bbe77..4c58a76 100644 --- a/.agent/skills/windows-services-powershell/SKILL.md +++ b/.agent/skills/windows-services-powershell/SKILL.md @@ -2,93 +2,73 @@ ## Когда использовать -Используй этот skill при изменениях в `scripts/*.ps1`, ProxiFyre install/start/stop/uninstall, sing-box service control, UAC/admin checks, helper/elevation boundary, component detection. +Используй этот skill при изменениях ProxiFyre/sing-box install/start/stop/update/uninstall, UAC/admin boundary, native inventory, NSIS upgrade/uninstall или build/release/QA PowerShell scripts. ## Цель -Сохранять service/install operations явными, безопасными и проверяемыми. Пользователь должен понимать, что приложение собирается менять в системе. Компьютер пользователя — не песочница для творческих экспериментов агента, как ни печально. +Сохранять системные операции явными, native и проверяемыми. Production runtime не зависит от PowerShell; Rust владеет Windows SCM, registry, process, filesystem, package verification и UAC flow. -## Инварианты +## Runtime-инварианты -- Install/start/stop/uninstall are explicit user actions. -- `apply` must not silently install/uninstall/start/stop components unless that behavior is clearly designed and surfaced. -- `-PlanOnly` scripts must be side-effect-free. -- PowerShell output intended for UI/backend must be structured JSON. -- Service detection must distinguish managed service from fuzzy candidate. -- Never relax safe-path checks to make uninstall easier. +- Install/start/stop/update/uninstall/migrate — только явные действия пользователя. +- `apply` не устанавливает, не обновляет, не переносит и не удаляет компоненты. +- Current managed roots — только `C:\Program Files\ProxyWarden\components\ProxiFyre` и `...\sing-box`. +- Service control требует exact `PathName`, marker/receipt, canonical path и non-reparse checks. Имя службы или fuzzy candidate недостаточны. +- Elevated UI action передаёт только UUID sealed job record; fixed early mode сам повторно проверяет ACL, TTL, action, paths, hashes и ownership. +- NSIS использует только exact `--nsis-verify-upgrade` и `--nsis-uninstall-managed`; никаких user/path/script arguments. +- Active/recovery/pending cutover journal блокирует upgrade/uninstall и не удаляется общим cleanup. +- Runtime-generated scripts и запуск `powershell.exe`/`pwsh` запрещены. -## Script rules +## PowerShell allowlist -PowerShell scripts should: +PowerShell остаётся только для build/release/QA: -- use `Set-StrictMode -Version Latest` where practical; -- set `$ErrorActionPreference = 'Stop'`; -- return structured JSON for plan/status paths; -- avoid localized text parsing for control flow; -- avoid writing secrets to host output; -- have clear exit codes; -- support `-PlanOnly` for dry-run/status checks; -- avoid downloading/executing arbitrary remote scripts. +- `scripts/check-runtime-powershell-boundary.ps1`; +- `scripts/update-component-bundle.ps1`; +- `scripts/prepare-release.ps1`; +- `scripts/audit-windows-smoke.ps1`. -## Elevation rules +`PlanOnly`/`CheckOnly` должны быть side-effect-free и возвращать structured JSON с `changed: false`. Любой новый `.ps1`, `.psm1`, `.psd1`, production caller или bundled cleanup resource должен ломать boundary checker. -When launching elevated PowerShell: +## Native service flow -- keep command fixed and parameters escaped; -- avoid user-controlled script text; -- avoid predictable temp script names; -- do not pass secrets via command line; -- verify script path before launch; -- clean up temp artifacts best-effort; -- return clear error if user cancels UAC. +1. Получить inventory через Windows API и canonicalize все пути. +2. Классифицировать `Missing / Managed / Foreign / Incomplete` до первой mutation. +3. Проверить marker/receipt, service `PathName`, file identity, ACL и reparse boundary. +4. Захватить общий lifecycle lock. +5. Выполнить только allowlisted fixed action. +6. Query-back подтвердить service/path/start policy/state. +7. При ошибке оставить durable recovery state; не угадывать cleanup. -## Service detection +Для uninstall сначала preflight всех компонентов. `Missing` — no-op; `Foreign`/`Incomplete` — zero mutation. Running service сначала останавливается и проверяется, затем удаляется. MSI code `3010` означает success with reboot required, а не обычную ошибку. -Preferred approach: +## Удаление файлов -1. Search known managed service names first. -2. Read service `PathName` through WMI/CIM. -3. Verify binary path and managed install metadata. -4. Only then mark as managed/controllable. -5. Fuzzy matches should be shown as candidates, not automatically controlled. +- Не использовать generic recursive delete по app root. +- Удалять только exact receipt/journal-owned entries после safe-path, ACL, reparse и file-shape checks. +- `.proxywarden-cutover` и `.proxywarden-quarantine` удаляет только owner terminal retirement после проверки journal state. +- Unexpected files, active jobs, process/service references или partial tombstone блокируют cleanup. -## Testing +## Проверка -Pure logic can be tested cross-platform with mocks. - -Real verification requires Windows: +Cross-platform/pure logic: ```powershell -& .\scripts\install-control-app.ps1 -PlanOnly -& .\scripts\install-proxyfier.ps1 -PlanOnly -& .\scripts\install-singbox.ps1 -PlanOnly -npm run tauri -- dev +Push-Location src-tauri +cargo fmt --all -- --check +cargo clippy --all-targets --all-features -- -D warnings +cargo test --all-targets +Pop-Location + +& .\scripts\check-runtime-powershell-boundary.ps1 -CheckOnly +& .\scripts\update-component-bundle.ps1 -CheckOnly +& .\scripts\audit-windows-smoke.ps1 -Mode PlanOnly ``` -For real service tests: +Реальная проверка требует Windows 10/11 x64 VM: UAC cancel/success, SCM create/start/stop/delete, driver/VC installer exit codes, fresh offline install, foreign same-name service refusal, legacy rollback/recovery и NSIS upgrade/uninstall/reboot. -- Windows 10/11. -- Admin/UAC path. -- Fresh machine or VM snapshot. -- Existing ProxiFyre/sing-box absent. -- Existing fuzzy ProxiFyre-like service present, if testing safety. - -## Do not - -- Do not claim actual service operations were tested unless they were run on Windows. -- Do not parse human-localized `sc.exe` output if structured WMI/CIM data is available. -- Do not delete paths from fuzzy discovery alone. -- Do not make scripts silently modify firewall/proxy/system settings outside their stated purpose. +Не называть service/elevation behavior проверенным без этой VM evidence. ## Как отчитываться -Перед финальным ответом применить `.agent/skills/communication-reporting/SKILL.md` и `.agent/checklists/communication.md`. - -Минимум для нетривиальной задачи: - -- короткая сводка; -- таблица файлов `Файл / Что изменилось / Зачем`; -- важные места без пересказа каждой строки; -- что проверено; -- что не проверено; -- конкретные риски. +Перед финальным ответом применить `.agent/skills/communication-reporting/SKILL.md` и `.agent/checklists/communication.md`. Отдельно перечислить automated evidence, Windows/manual evidence и незакрытые UAC/SCM/driver риски. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6615479..fb64b84 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -49,6 +49,10 @@ jobs: working-directory: src-tauri run: cargo clippy --all-targets --all-features -- -D warnings + - name: Test component catalog contract + working-directory: src-tauri + run: cargo test --test component_catalog_tests + - name: Run Rust tests working-directory: src-tauri run: cargo test --all-targets @@ -56,18 +60,25 @@ jobs: - name: Check Tauri environment run: npm run tauri -- info - - name: Plan control app installer + - name: Check runtime PowerShell boundary shell: pwsh - run: .\scripts\install-control-app.ps1 -PlanOnly + run: .\scripts\check-runtime-powershell-boundary.ps1 -CheckOnly - - name: Plan ProxiFyre installer + - name: Plan component bundle update shell: pwsh - run: .\scripts\install-proxyfier.ps1 -PlanOnly + run: .\scripts\update-component-bundle.ps1 -PlanOnly - - name: Plan sing-box installer + - name: Check component bundle shell: pwsh - run: .\scripts\install-singbox.ps1 -PlanOnly + run: .\scripts\update-component-bundle.ps1 -CheckOnly - name: Plan Windows smoke evidence capture shell: pwsh run: .\scripts\audit-windows-smoke.ps1 -Mode PlanOnly + + - name: Plan release preparation + shell: pwsh + run: .\scripts\prepare-release.ps1 -PlanOnly -SkipBuild + + - name: Test release workflow with local Git remotes + run: node --test scripts/prepare-release.check.mjs diff --git a/.gitignore b/.gitignore index 26e00b6..eba3b37 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,6 @@ node_modules/ +.pnpm-store/ +*.tsbuildinfo dist/ releases/ src-tauri/target/ diff --git a/AGENTS.md b/AGENTS.md index cf509aa..3c5831c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -2,7 +2,7 @@ ## Назначение -ProxyWarden — standalone Windows desktop-приложение для удобного per-app proxy routing. Стек: Tauri 2, Rust backend, React/TypeScript frontend, Vite, PowerShell installer/control scripts. Приложение управляет выбранными Windows-приложениями через ProxiFyre и, опционально, через локальный sing-box runtime. +ProxyWarden — standalone Windows desktop-приложение для удобного per-app proxy routing. Стек: Tauri 2, Rust backend, React/TypeScript frontend и Vite. Production install/service/UAC runtime реализован в Rust; PowerShell остаётся только build/release/QA tooling. Приложение управляет выбранными Windows-приложениями через ProxiFyre и, опционально, через локальный sing-box runtime. Этот файл — главный контракт для кодового агента. Любой агент, который меняет репозиторий, обязан соблюдать эти правила. Да, даже если ему очень хочется «быстренько поправить одну кнопочку» и случайно переписать половину сетевого стека. Особенно тогда. @@ -21,6 +21,9 @@ ProxyWarden — standalone Windows desktop-приложение для удоб - `src/api/tauriCommands.ts` — единственная TypeScript-обертка над `invoke(...)`. - Rust backend отвечает за storage, validation, config generation, component detection, service/install orchestration и structured errors. - `C:\ProgramData\ProxyWarden\config` и `C:\ProgramData\ProxyWarden\state` — source of truth. +- `C:\Program Files\ProxyWarden\components\ProxiFyre` и `C:\Program Files\ProxyWarden\components\sing-box` — единственные current managed component roots. +- `config\components.json` — только legacy migration input. Реальный component status принадлежит native Windows inventory и проверенным receipts. +- Packaged component catalog — immutable offline baseline; проверенный download cache лежит отдельно в `C:\ProgramData\ProxyWarden\packages`. - `C:\ProgramData\ProxyWarden\generated\proxifyre-app-config.json` и `sing-box-config.json` — derived artifacts. Их можно пересоздавать. - Install/start/stop/uninstall — только явные действия пользователя. `apply` не должен скрыто устанавливать, удалять или «чинить» компоненты. - Subscription URL, credentials, proxy passwords и userinfo нельзя выводить полностью в UI, logs, diagnostics, crash text или activity. @@ -47,18 +50,25 @@ src-tauri/ src/storage.rs # JSON storage, tmp/bak writes src/activity.rs # activity log src/subscription.rs # subscription fetch/parse - src/component_detection.rs # ProxiFyre/sing-box detection - src/singbox_service.rs # sing-box Windows service logic - src/process.rs # process/system helpers - src/helper.rs # helper/elevation boundary + src/component_catalog.rs # pinned offline component catalog + src/component_inventory.rs # exact native SCM/process/registry inventory + src/component_packages.rs # verified bundled/cache package plans + src/component_cutover.rs # durable legacy cutover/rollback/cleanup + src/migration.rs # versioned storage migration/adoption + src/privileged_jobs.rs # sealed one-shot elevated job records + src/privileged_runtime.rs # fixed native elevated action dispatcher + src/proxifyre_runtime.rs # native ProxiFyre lifecycle + src/singbox_runtime.rs # native sing-box lifecycle + src/singbox_service.rs # WinSW service spec/status logic + src/safe_fs.rs # safe path/ACL/reparse helpers src/adapters/* # ProxiFyre/sing-box/proxy-router adapters src/commands.rs # Tauri command handlers; currently too large tests/* # Rust integration/domain tests scripts/ - install-control-app.ps1 - install-proxyfier.ps1 - install-singbox.ps1 + check-runtime-powershell-boundary.ps1 + update-component-bundle.ps1 + audit-windows-smoke.ps1 prepare-release.ps1 ``` @@ -150,13 +160,16 @@ scripts/ - Не отключать CSP. Если CSP мешает, исправлять source policy, а не ставить `csp: null`. - Не добавлять Tauri shell permissions без жесткого scope и отдельного обоснования. - Не запускать произвольные команды из UI input. -- Runtime-generated elevated scripts должны использовать непредсказуемые имена, safe directory/ACL и cleanup best-effort. +- Runtime-generated elevated scripts запрещены. Elevation использует current exe, fixed early modes и sealed typed job records без arbitrary command/path arguments. - Удаление директорий допускается только после safe-path/marker/service-path checks. - Subscription fetch должен иметь timeout и защиту от очевидно опасных/local metadata адресов либо explicit allow-mode. ### Windows/service boundary -- `-PlanOnly` у PowerShell scripts должен оставаться side-effect-free и возвращать structured JSON. +- PowerShell разрешён только в build/release/QA allowlist: `check-runtime-powershell-boundary.ps1`, `update-component-bundle.ps1`, `audit-windows-smoke.ps1`, `prepare-release.ps1`. +- `PlanOnly`/`CheckOnly` у этих scripts должны быть side-effect-free, возвращать structured JSON и иметь `changed: false`. +- Production Rust, Tauri resources и NSIS hooks не должны запускать `powershell.exe`, `pwsh`, `.ps1` или generated script text. +- После изменения этой границы запускать `scripts/check-runtime-powershell-boundary.ps1 -CheckOnly`. - Install/start/stop/uninstall должны быть явными user actions. - Fuzzy-detected service не считать managed service без проверки `PathName`/metadata. - В Linux/macOS CI не пытаться «проверить» Windows service operations как реальные. Тестировать pure logic/mocks. @@ -165,12 +178,6 @@ scripts/ - `src-tauri/src/commands.rs` слишком большой. Главная цель рефакторинга: разрезать на модули по use-case. - `src/app/App.tsx` слишком большой. Главная цель frontend-рефакторинга: hooks/components/view-model helpers. -- `tauri.conf.json` сейчас требует security review, особенно CSP и window resize settings. -- JSON storage молча возвращает default при invalid JSON. Нужен corruption recovery через `.bak` и user-visible warning. -- ProxiFyre config apply должен стать atomic. -- Subscription URL redaction должен исключать userinfo/password. -- Link subscription parser сейчас ориентирован на VLESS; не обещать больше, чем реально поддерживается. -- Ping/select по server tag может ломаться при duplicate tags. Нужен stable server id. ## Минимальная проверка перед ответом @@ -200,12 +207,14 @@ npm run tauri -- dev npm run tauri -- build ``` -Для installer boundaries: +Для offline bundle/release boundaries: ```powershell -& .\scripts\install-control-app.ps1 -PlanOnly -& .\scripts\install-proxyfier.ps1 -PlanOnly -& .\scripts\install-singbox.ps1 -PlanOnly +& .\scripts\check-runtime-powershell-boundary.ps1 -CheckOnly +& .\scripts\update-component-bundle.ps1 -PlanOnly +& .\scripts\update-component-bundle.ps1 -CheckOnly +& .\scripts\audit-windows-smoke.ps1 -Mode PlanOnly +& .\scripts\prepare-release.ps1 -PlanOnly -SkipBuild ``` Не оставлять dev/preview/Tauri dev servers запущенными после проверки. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8f20688..97d9857 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -19,10 +19,11 @@ cargo test --all-targets Pop-Location npm run tauri -- info -& .\scripts\install-control-app.ps1 -PlanOnly -& .\scripts\install-proxyfier.ps1 -PlanOnly -& .\scripts\install-singbox.ps1 -PlanOnly +& .\scripts\check-runtime-powershell-boundary.ps1 -CheckOnly +& .\scripts\update-component-bundle.ps1 -PlanOnly +& .\scripts\update-component-bundle.ps1 -CheckOnly & .\scripts\audit-windows-smoke.ps1 -Mode PlanOnly +& .\scripts\prepare-release.ps1 -PlanOnly -SkipBuild ``` Windows service, UAC, installer и реальный routing нельзя считать проверенными только по unit-тестам. Для таких изменений укажите выполненный ручной сценарий или явно оставьте этот пробел в отчете. @@ -32,6 +33,8 @@ Windows service, UAC, installer и реальный routing нельзя счи - Держите `src/api/tauriCommands.ts` единственным TypeScript facade над Tauri `invoke`. - Не показывайте subscription URL, credentials, proxy password или `X-HWID` в логах и UI. - Не добавляйте скрытые install/start/stop/uninstall действия в apply. +- Не добавляйте PowerShell, `.ps1` resources или generated scripts в production runtime. PowerShell разрешён только в точном build/release/QA allowlist, который проверяет `check-runtime-powershell-boundary.ps1`. +- Храните managed components только в `C:\Program Files\ProxyWarden\components`; `config\components.json` допустим лишь как legacy migration input, а не source of truth. - Добавляйте минимальный тест для новой ветвящейся логики. - Не коммитьте runtime-файлы из `C:\ProgramData\ProxyWarden` и generated output. diff --git a/README.md b/README.md index aa26364..c371daf 100644 --- a/README.md +++ b/README.md @@ -1,35 +1,36 @@ # ProxyWarden -ProxyWarden - это standalone Windows desktop-приложение для маршрутизации выбранных программ через SOCKS5-прокси. По сути это удобная оболочка управления над внешними компонентами: обязательным маршрутизатором приложений ProxiFyre и, опционально, локальным runtime `sing-box`. +ProxyWarden — standalone Windows desktop-приложение для маршрутизации выбранных программ через SOCKS5-прокси. Оно управляет обязательным ProxiFyre и, при необходимости, локальным `sing-box`, но само не является VPN-драйвером, proxy server или облачным control plane. -ProxyWarden сам не является VPN-драйвером, прокси-сервером или отдельным gateway/server. Он хранит настройки, показывает состояние компонентов, генерирует конфиги и запускает только явные действия пользователя: установить, запустить, остановить, удалить или применить конфиг. +Все системные действия остаются явными: `apply` только проверяет и применяет конфигурацию; установка, обновление, запуск, остановка, перенос и удаление компонентов выполняются отдельными командами пользователя. ## Главное -- Работает как Windows-клиент: Tauri 2 + React/TypeScript UI + Rust backend. -- Маршрутизирует не всю систему, а выбранные приложения: процесс, папку или конкретный `.exe`. -- Не меняет глобальный proxy в Windows. -- Для per-app routing нужен ProxiFyre. -- Local sing-box нужен только для сценария с подпиской и локальным SOCKS5 endpoint. -- Внешний SOCKS5-прокси работает без Local sing-box. -- Применение профиля не устанавливает и не чинит компоненты скрыто. +- Tauri 2 + React/TypeScript UI + Rust backend. +- Маршрутизируются выбранные процессы, папки или `.exe`, а не вся система. +- Глобальный Windows proxy не меняется. +- Внешний SOCKS5 работает без Local sing-box. +- Production runtime не запускает PowerShell: service/install/UAC orchestration принадлежит native Rust. +- x64 installer содержит проверенные offline payloads компонентов и WebView2 Offline Installer; сеть для baseline-установки не нужна. -## Из чего состоит +## Компоненты -| Компонент | Что это | Нужен когда | Откуда берется | -| --- | --- | --- | --- | -| ProxyWarden Control App | Окно управления, настройки, status/readiness, генерация конфигов | Всегда | Этот репозиторий | -| [ProxiFyre](https://github.com/wiresock/proxifyre) | Windows-приложение/служба для перехвата трафика выбранных процессов и отправки его в SOCKS5 | Всегда для маршрутизации приложений | GitHub releases `wiresock/proxifyre` | -| [Windows Packet Filter / NDISAPI](https://github.com/wiresock/ndisapi) | Сетевой драйвер, который нужен ProxiFyre | Устанавливается вместе с ProxiFyre, если отсутствует | GitHub releases `wiresock/ndisapi` | -| [Microsoft Visual C++ Redistributable](https://learn.microsoft.com/cpp/windows/latest-supported-vc-redist) | Runtime-зависимость для `ProxiFyre.exe` | Устанавливается вместе с ProxiFyre, если отсутствует | Официальный `vc_redist` Microsoft | -| [sing-box](https://github.com/SagerNet/sing-box) | Локальный proxy/VPN runtime, который слушает `127.0.0.1:1080` | Только для маршрута через subscription/выбранный сервер | GitHub releases `SagerNet/sing-box` | -| [WinSW](https://github.com/winsw/winsw) | Wrapper, который запускает Local sing-box как Windows-службу | Только для Local sing-box | GitHub releases `winsw/winsw` | +| Компонент | Роль | Когда нужен | +| --- | --- | --- | +| ProxyWarden Control App | UI, storage, validation, config generation и orchestration | Всегда | +| [ProxiFyre](https://github.com/wiresock/proxifyre) | Перехватывает трафик выбранных приложений и направляет его в SOCKS5 | Для любого per-app routing | +| [Windows Packet Filter / NDISAPI](https://github.com/wiresock/ndisapi) | Сетевой драйвер ProxiFyre | Устанавливается вместе с ProxiFyre, если отсутствует | +| [Microsoft Visual C++ Redistributable](https://learn.microsoft.com/cpp/windows/latest-supported-vc-redist) | Runtime-зависимость ProxiFyre | Устанавливается при необходимости | +| [sing-box](https://github.com/SagerNet/sing-box) | Создаёт локальный SOCKS5 endpoint для выбранного subscription-сервера | Только для Local sing-box flow | +| [WinSW](https://github.com/winsw/winsw) | Запускает sing-box как Windows-службу | Только для Local sing-box flow | -В UI и коде компонент ProxiFyre иногда проходит через внутренний id `proxyfier`. Это не отдельный продукт Proxifier; текущий backend adapter работает именно с ProxiFyre. +Версии, SHA-256 и лицензии offline payloads зафиксированы в packaged component catalog. Установка Control App не запускает routing-компоненты: нужный компонент устанавливается отдельным действием в UI. -## Как идут маршруты +В UI и части внутренних DTO ProxiFyre может иметь исторический id `proxyfier`. Это не продукт Proxifier. -Внешний SOCKS5-прокси: +## Маршруты + +Внешний SOCKS5: ```text выбранные приложения -> ProxiFyre -> внешний SOCKS5 proxy @@ -38,192 +39,140 @@ ProxyWarden сам не является VPN-драйвером, прокси-с Local sing-box: ```text -выбранные приложения -> ProxiFyre -> Local sing-box 127.0.0.1:1080 -> выбранный сервер из подписки +выбранные приложения -> ProxiFyre -> Local sing-box 127.0.0.1:1080 -> выбранный subscription-сервер ``` -Во втором сценарии ProxiFyre все равно обязателен: именно он делает маршрутизацию конкретных Windows-приложений. Local sing-box только дает локальный SOCKS5 endpoint и ходит дальше к выбранному серверу. +Во втором маршруте ProxiFyre по-прежнему отвечает за выбор приложений. Local sing-box только предоставляет локальный SOCKS5 endpoint и соединяется с выбранным сервером. -## Что устанавливается +## Установка и системные пути -### Control App - -Обычная сборка Tauri создает desktop-приложение ProxyWarden. Отдельный скрипт `scripts/install-control-app.ps1` сейчас подготавливает стандартные директории: +Tauri NSIS installer устанавливает Control App per-machine. Managed runtime-компоненты лежат только под текущим app root: ```text -C:\Program Files\ProxyWarden\ControlApp -C:\ProgramData\ProxyWarden\config -C:\ProgramData\ProxyWarden\state -C:\ProgramData\ProxyWarden\generated +C:\Program Files\ProxyWarden +C:\Program Files\ProxyWarden\components\ProxiFyre +C:\Program Files\ProxyWarden\components\sing-box ``` -### ProxiFyre - -Явная установка ProxiFyre из приложения выполняется через elevated PowerShell и ставит/обновляет: +Службы: ```text -C:\Tools\ProxiFyre -C:\Tools\ProxiFyre\ProxiFyre.exe -C:\Tools\ProxiFyre\app-config.json -Windows service: ProxiFyreService +ProxiFyreService +ProxyWardenSingBox ``` -Если на машине не найдены зависимости, установщик также скачивает и ставит Microsoft Visual C++ Redistributable и Windows Packet Filter / NDISAPI. +ProxyWarden управляет службой только после точной проверки `PathName`, marker/receipt и canonical component root. Похожее имя службы или найденная папка сами по себе не дают права на start/stop/delete. -### Local sing-box +## Релиз одной командой -Явная установка Local sing-box ставит: +В PowerShell из корня проекта: + +```powershell +.\release.cmd +``` + +То же действие доступно как `npm run release`. Сценарий показывает изменения Git и предлагает patch/minor/major, произвольную версию или текущую ещё не выпущенную версию. Можно сразу ввести номер вроде `1.2.1`. + +После выбора он синхронизирует версии в package.json, package-lock.json, tauri.conf.json, Cargo.toml и Cargo.lock, проверяет frontend/Rust/offline bundle, собирает NSIS и готовит папку `releases/proxywarden-vX.Y.Z`. Затем создаёт commit со всеми текущими отслеживаемыми и неигнорируемыми новыми файлами, annotated tag `vX.Y.Z` и одним atomic push отправляет текущую ветку и этот тег в `origin`. При отсутствии изменений новый commit не нужен. Артефакты не попадают в Git. + +В папке релиза: `artifacts/nsis/ProxyWarden_X.Y.Z_x64-setup.exe`, `SHA256SUMS.txt`, `release-manifest.json` с точным commit/hash и `release-notes.md`. EXE загружается на сайт вручную; GitHub/Gitea release page автоматически не создаётся. + +Нужны Git с настроенной identity и доступом к origin, Node, установленные frontend-зависимости (`npm ci` один раз), Rust/MSVC/Windows SDK. Сам сценарий сборки использует Node напрямую и не требует npm в PATH. Запуск от администратора не нужен. + +```powershell +.\release.cmd -PlanOnly # только JSON-план: без записи, сборки и сети +.\release.cmd -Version 1.2.1 # версия без вопроса +.\release.cmd -Version 1.2.1 -Resume # повторить только неудачный push +``` + +Не меняйте исходники во время сборки. Существующие теги не перезаписываются; при расхождении с удалённой веткой сценарий останавливается до изменения версий. При ошибке сборки изменения версии остаются локально для исправления, commit/tag/push не выполняются. При неудачном push готовая папка и локальный commit/tag сохраняются; `-Resume` проверяет исходники и SHA-256 перед повторной отправкой. + +Для локальной подготовки без commit/tag/push остаётся `scripts/prepare-release.ps1 -Version X.Y.Z`. Автоматические проверки не заменяют Windows VM/UAC/driver/routing acceptance: в manifest это отмечается отдельно. + +## Данные и source of truth + +Настройки и состояние лежат под `C:\ProgramData\ProxyWarden`: ```text -C:\Program Files\ProxyWarden\sing-box\sing-box.exe -C:\Program Files\ProxyWarden\sing-box\ProxyWardenSingBox.exe -C:\Program Files\ProxyWarden\sing-box\ProxyWardenSingBox.xml -C:\Program Files\ProxyWarden\sing-box\config.json -Windows service: ProxyWardenSingBox +config\profiles.json +config\targets.json +config\local-singbox.json +config\storage-meta.json +state\activity.json +state\component-layout.json +state\component-updates.json +state\migrations\... +packages\... ``` -`ProxyWardenSingBox.exe` - это WinSW wrapper. Он нужен только чтобы запускать `sing-box.exe` как Windows-службу. +`config\components.json` не является текущим источником статуса компонентов. Это только legacy input: migration может проверить, сохранить snapshot/archive и затем перестать использовать его. Фактический install/service/version status читается из native inventory Windows и проверенных receipts. + +Generated artifacts можно пересоздать: + +```text +generated\proxifyre-app-config.json +generated\sing-box-config.json +``` + +Не редактируйте generated-файлы как основной источник правды. Subscription URL, userinfo, credentials, proxy password и внутренние migration/job records нельзя выводить целиком в UI, logs или diagnostics. + +## Миграция старой установки + +- Startup выполняет только безопасную storage adoption/migration: backup, validation, atomic commit и повторный no-op. +- Старые component roots и службы сначала обнаруживаются read-only. +- Перенос компонента — отдельное UAC-действие с exact identity checks, rollback journal и quarantine. +- Foreign или incomplete installation не управляется автоматически. +- Пока cutover journal активен, требует recovery или quarantine ещё не подтверждён к удалению, upgrade/uninstall блокируется до безопасного завершения. ## Права администратора -Без прав администратора можно открыть приложение, редактировать настройки, добавлять приложения, вводить внешний proxy, загружать/выбирать подписку и смотреть состояние. +Без UAC можно редактировать настройки, выбирать приложения и proxy, загружать subscription, смотреть статус и генерировать конфигурацию. -Права администратора или UAC confirmation нужны для операций, которые меняют систему: +UAC требуется для явных действий, которые меняют Windows: -- установка или удаление ProxiFyre; -- установка Windows Packet Filter / NDISAPI; -- установка Microsoft Visual C++ Redistributable, если его нет; -- установка или удаление Local sing-box; -- создание, запуск и остановка Windows-служб; -- удаление install folder для managed-компонентов. +- install/update/uninstall ProxiFyre или Local sing-box; +- установка Windows Packet Filter и VC++ Runtime при необходимости; +- start/stop/create/delete Windows-служб; +- подтверждённый legacy component cutover и его cleanup. -Применение профиля не запускает установку. Оно генерирует derived config и пытается записать его в найденную установку ProxiFyre. Если прав на запись в папку установки не хватает, операция должна завершиться ошибкой, а не устанавливать что-то скрыто. - -## Поддержанная среда - -Подтверждено вручную сейчас: - -```text -Windows 11 -PowerShell 7 как пользовательская shell для запуска команд разработки -``` - -Важно: Rust backend и elevated-операции сейчас запускают именно `powershell.exe` с `-NoProfile` и `-ExecutionPolicy Bypass`. На Windows это обычно Windows PowerShell 5.1. Скрипты используют стандартные команды вроде `Get-CimInstance`, `Invoke-WebRequest`, `Expand-Archive`, `Get-FileHash`, `Start-Service`, `Stop-Service`, `ConvertTo-Json`, поэтому должны быть близки к Windows PowerShell 5.1, но полный ручной тест пока был только на Windows 11 с PowerShell 7 в окружении разработки. - -Ожидаемая, но не полностью подтвержденная область: - -- Windows 10/11 desktop; -- x64 как основной сценарий; -- x86 и ARM64 частично учтены в installer-логике через выбор release assets, но не считаются проверенными; -- обычный desktop/laptop без специальных требований к GPU; -- доступ в интернет к GitHub releases и Microsoft download endpoints для установки компонентов. - -Linux/macOS не являются целевой платформой для этого клиента. - -## Где лежат настройки - -Source of truth лежит в JSON под `C:\ProgramData\ProxyWarden`: - -```text -C:\ProgramData\ProxyWarden\config\profiles.json -C:\ProgramData\ProxyWarden\config\targets.json -C:\ProgramData\ProxyWarden\config\components.json -C:\ProgramData\ProxyWarden\config\local-singbox.json -C:\ProgramData\ProxyWarden\state\activity.json -C:\ProgramData\ProxyWarden\state\singbox-subscription-cache.json -``` - -Сгенерированные файлы лежат отдельно и могут быть пересозданы: - -```text -C:\ProgramData\ProxyWarden\generated\proxifyre-app-config.json -C:\ProgramData\ProxyWarden\generated\sing-box-config.json -``` - -Не редактируйте generated-файлы как основной источник правды. При следующей генерации они могут быть перезаписаны. - -Subscription URL считается секретом. UI и diagnostics должны показывать только редактированную/сокращенную версию ссылки. - -При загрузке подписки ProxyWarden отправляет провайдеру стандартные идентификационные заголовки приложения и `X-HWID` - случайный постоянный UUID этой установки. Это не серийный номер оборудования, но провайдер может использовать его для связывания запросов одной установки. Проверка маршрута делает HTTPS-запросы через выбранный proxy к Cloudflare и ipify, чтобы подтвердить выход и определить внешний IP. +Elevated mode принимает только заранее записанный typed job ID либо один из фиксированных NSIS modes. UI не передаёт произвольную команду, script text или install path. ## Типовые сценарии ### Внешний SOCKS5 -1. Запустите ProxyWarden. -2. Установите или проверьте ProxiFyre. -3. На вкладке `VPN / Прокси` выберите внешний proxy. -4. Введите `host:port` или `socks5://host:port`. -5. На вкладке `ProxiFyre` добавьте приложения. -6. Нажмите `Применить в ProxiFyre`. +1. Установите ProxiFyre явной кнопкой, если он отсутствует. +2. На вкладке `VPN / Прокси` выберите внешний proxy и укажите `host:port` или `socks5://host:port`. +3. Добавьте приложения в ProxiFyre route. +4. Нажмите `Применить`. Local sing-box для этого сценария не нужен. ### Local sing-box с подпиской -1. Запустите ProxyWarden. -2. Установите ProxiFyre. -3. Установите Local sing-box. -4. Вставьте subscription URL. -5. Загрузите список серверов и выберите сервер. -6. Добавьте приложения. -7. Сгенерируйте/примените маршрут. +1. Явно установите ProxiFyre и Local sing-box. +2. Добавьте subscription URL, загрузите список и выберите сервер. +3. Добавьте приложения и примените маршрут. -## Установка и запуск из исходников +## Разработка -Нужны: - -- Windows 11 для подтвержденного пути разработки; -- Node.js и npm; -- Rust через rustup; -- Visual Studio Build Tools с MSVC и Windows SDK; -- Microsoft Edge WebView2 Runtime; -- PowerShell 7 удобно использовать как shell разработки, но elevated runtime-команды приложения запускаются через `powershell.exe`. - -Установка зависимостей и запуск: +Целевая платформа — Windows 10/11 x64. Для сборки нужны Node.js/npm, Rust через rustup, Visual Studio Build Tools с MSVC и Windows SDK. PowerShell 7 используется только для build/release/QA tooling; установленному приложению PowerShell не нужен. ```powershell -cd D:\repos\ProxyWarden -npm install +Set-Location D:\repos\ProxyWarden +npm ci npm run tauri -- dev ``` -Собрать frontend: - -```powershell -npm run build -``` - -Собрать установочный пакет Tauri: - -```powershell -npm run tauri -- build -``` - -Запустить только browser-preview без нативных Tauri-команд: +Browser preview не доказывает работу Tauri commands, UAC или Windows-служб: ```powershell npm run dev -- --host 127.0.0.1 ``` -Browser-preview годится для проверки интерфейса, но не доказывает работу Windows-служб, elevated-операций и Tauri command handlers. +## Проверка -## Installer-скрипты - -В репозитории есть явные entrypoint-скрипты: - -```powershell -& .\scripts\install-control-app.ps1 -PlanOnly -& .\scripts\install-proxyfier.ps1 -PlanOnly -& .\scripts\install-singbox.ps1 -PlanOnly -``` - -`-PlanOnly` возвращает structured JSON и не должен иметь side effects. - -Реальная установка через эти скрипты требует прав администратора. `scripts/install-proxyfier.ps1` как standalone boundary сейчас ожидает локальный `-PackagePath`; путь установки из UI/backend использует отдельный elevated-скрипт, который скачивает ProxiFyre, Windows Packet Filter и runtime-зависимости сам. - -## Проверка для разработчика - -Frontend/UI: +Frontend и Rust: ```powershell npm run format:check @@ -231,37 +180,34 @@ npm run lint npm run typecheck npm test -- --run npm run build + +Push-Location src-tauri +cargo fmt --all -- --check +cargo clippy --all-targets --all-features -- -D warnings +cargo test --all-targets +Pop-Location ``` -Rust/backend: - -```powershell -cd D:\repos\ProxyWarden\src-tauri -cargo test -``` - -Tauri/toolchain: +Build/release/QA boundaries: ```powershell +& .\scripts\check-runtime-powershell-boundary.ps1 -CheckOnly +& .\scripts\update-component-bundle.ps1 -PlanOnly +& .\scripts\update-component-bundle.ps1 -CheckOnly +& .\scripts\audit-windows-smoke.ps1 -Mode PlanOnly +& .\scripts\prepare-release.ps1 -PlanOnly -SkipBuild npm run tauri -- info -npm run tauri -- dev npm run tauri -- build ``` -Installer boundaries: +`PlanOnly` и `CheckOnly` возвращают structured JSON с `changed: false`. Обновление packaged component catalog — отдельная release-команда и не является runtime action. -```powershell -& .\scripts\install-control-app.ps1 -PlanOnly -& .\scripts\install-proxyfier.ps1 -PlanOnly -& .\scripts\install-singbox.ps1 -PlanOnly -``` +Unit tests и build не подтверждают реальный UAC/SCM/driver/routing flow. Для release candidate нужны Windows VM smoke-сценарии: fresh offline install, legacy upgrade/rollback, foreign same-name service refusal и uninstall/reboot behavior. -## Ограничения текущей версии +## Ограничения -- Основной поддержанный маршрут - SOCKS5. -- Link-подписки разбирают VLESS, VMess, Trojan и Shadowsocks; sing-box JSON также принимает поддержанные proxy outbounds. Неизвестные форматы отклоняются явно. -- Для VLESS outbound без собственного `packet_encoding` генератор добавляет `xudp`; значение, заданное провайдером подписки, не перезаписывается. -- ProxiFyre является текущим backend-слоем для per-app routing. -- Local sing-box остается опциональным и не требуется для внешнего SOCKS5. -- Elevated install/start/stop/uninstall операции считаются реализованными, но требуют дополнительной проверки на реальной Windows-машине с UAC/admin confirmation. -- Windows 10, Windows PowerShell 5.1, ARM64 и x86 нужно отдельно подтвердить перед тем, как называть их официально поддержанными. +- Основной routing protocol — SOCKS5. +- Link subscriptions поддерживают только форматы, которые явно принимает текущий parser; неизвестные поля/форматы отклоняются, а не теряются молча. +- Local sing-box остаётся optional. +- x86 и ARM64 не входят в текущий release contract. +- Реальные Windows service, UAC, driver и offline installer сценарии нельзя считать подтверждёнными без VM evidence. diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md new file mode 100644 index 0000000..e035fe1 --- /dev/null +++ b/THIRD_PARTY_NOTICES.md @@ -0,0 +1,41 @@ +# Third-party notices for the ProxyWarden offline bundle + +This file records the third-party runtime payload planned for the ProxyWarden +`1.2.0` Windows x64 installer. It is an engineering inventory, not legal advice +or a completed distribution approval. Exact bundled hashes and sizes are owned +by `src-tauri/bundled/components/catalog.json`. + +## Managed runtime assets + +| Component | Pinned asset and official source | License copy | Update trust and distribution note | +| ---------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| ProxiFyre | `2.4.0`, [`ProxiFyre-v2.4.0-x64-signed.zip`](https://github.com/wiresock/proxifyre/releases/download/v2.4.0/ProxiFyre-v2.4.0-x64-signed.zip); [commit-pinned source archive](https://github.com/wiresock/proxifyre/archive/dd1512840e1e3bc596b06b80eda4e2dcd6a9c9ed.tar.gz) | `AGPL-3.0-only`; `src-tauri/bundled/components/proxifyre/LICENSE` | Origin is accepted only with the official GitHub release digest and the Authenticode publisher `The Anti-Cloud Corporation` on the inner executable. Before release, the project/release owner must record the corresponding-source or written-source-offer decision and approve redistribution. | +| Windows Packet Filter | release `3.6.2`, product `3.6.2.1`, [`Windows.Packet.Filter.3.6.2.1.x64.msi`](https://github.com/wiresock/ndisapi/releases/download/v3.6.2/Windows.Packet.Filter.3.6.2.1.x64.msi); [commit-pinned source archive](https://github.com/wiresock/ndisapi/archive/417b8734e844083a10236387fba705d94a2d6bc9.tar.gz) | `MIT`; `src-tauri/bundled/components/windows-packet-filter/LICENSE` | Origin is accepted only with the official GitHub release digest and MSI Authenticode publisher `The Anti-Cloud Corporation`. The MSI is a shared system dependency; its presence alone does not prove ProxyWarden ownership and does not authorize uninstall. | +| Microsoft Visual C++ x64 Redistributable | file/product version `14.51.36247.0`, [`VC_redist.x64.exe`](https://aka.ms/vs/18/release/14.51.36247/VC_redist.x64.exe) | `LicenseRef-Microsoft-Visual-Cpp-v14-Redistributable-2026`; [`src-tauri/bundled/components/vc-runtime/LICENSE.docx`](https://visualstudio.microsoft.com/wp-content/uploads/2025/10/Visual-C-V14-License-Redistributable_and_Runtime_ENU.docx) | Build-time refresh only. The pinned file must retain a valid Microsoft Corporation Authenticode signature; no in-app remote update is offered. This is proprietary Microsoft software, so the project/release owner must approve its redistribution under the bundled official terms before release. | +| sing-box | `1.13.19`, [`sing-box-1.13.19-windows-amd64.zip`](https://github.com/SagerNet/sing-box/releases/download/v1.13.19/sing-box-1.13.19-windows-amd64.zip); [commit-pinned source archive](https://github.com/SagerNet/sing-box/archive/b5ebaa1fc0f2b94256180b95468e73ef53caa27d.tar.gz) | `LicenseRef-Sing-Box-Project` (GPL-3.0-or-later plus the upstream name restriction); `src-tauri/bundled/components/sing-box/LICENSE` | Origin is accepted only with the official GitHub release digest. Redistribution must preserve the GPL terms and the upstream name restriction. Before release, the project/release owner must record the corresponding-source/source-offer decision and approve the notice text. | +| WinSW | `2.12.0`, [`WinSW.NET461.exe`](https://github.com/winsw/winsw/releases/download/v2.12.0/WinSW.NET461.exe); [commit-pinned source archive](https://github.com/winsw/winsw/archive/eef5bade59fca0254e387ac73ed7625ba6aa7147.tar.gz) | `MIT`; `src-tauri/bundled/components/winsw/LICENSE.txt` | The selected binary is IL-only AnyCPU and is used on the x64 target with supported .NET Framework 4.8/4.8.1. Upstream supplies neither an independent digest nor an Authenticode signature for this asset, so it is `bundled-only/no-independent-proof`: remote update is disabled and a newer bundle is required to replace it. | + +## WebView2 prerequisite + +Microsoft Edge WebView2 Runtime is not part of the managed component catalog and +does not receive an in-app update action. Tauri packages the Microsoft WebView2 +Evergreen Standalone Offline Installer into the NSIS installer through +`bundle.windows.webviewInstallMode.type = "offlineInstaller"`. Microsoft/Windows +owns later runtime servicing. The release evidence must prove that the offline +payload is present and that a clean Windows 10/11 x64 machine can install and +start ProxyWarden without network access. See the official +[WebView2 distribution page](https://developer.microsoft.com/en-us/microsoft-edge/webview2/). + +## Release compliance gate + +No license or distribution sign-off is claimed by this file. Before tagging or +publishing `1.2.0`, the project/release owner must record in +`docs/goals/production-ready-offline-migration/EVIDENCE.md`: + +- the exact installer composition and catalog hashes; +- the reviewed license copies and source links; +- the corresponding-source/source-offer decisions for ProxiFyre and sing-box; +- the Microsoft Visual C++ and WebView2 redistribution decision; +- reviewer name/date and explicit approval. + +Until that record exists, license/distribution remains a release blocker. diff --git a/package-lock.json b/package-lock.json index bf42fec..c7417c8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "proxywarden", - "version": "1.1.0", + "version": "2.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "proxywarden", - "version": "1.1.0", + "version": "2.0.0", "dependencies": { "@fontsource-variable/jetbrains-mono": "^5.2.8", "@tauri-apps/api": "^2.0.0", diff --git a/package.json b/package.json index b02b8fb..4ce55ef 100644 --- a/package.json +++ b/package.json @@ -1,10 +1,12 @@ { "name": "proxywarden", - "version": "1.1.0", + "version": "2.0.0", "private": true, "type": "module", "description": "Standalone Windows desktop proxy management app for ProxyWarden.", "scripts": { + "release": ".\\release.cmd", + "test:release": "node --test scripts/prepare-release.check.mjs", "dev": "vite", "build": "npm run typecheck && vite build", "typecheck": "tsc --noEmit", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml deleted file mode 100644 index 7646ea4..0000000 --- a/pnpm-lock.yaml +++ /dev/null @@ -1,2356 +0,0 @@ -lockfileVersion: '9.0' - -settings: - autoInstallPeers: true - excludeLinksFromLockfile: false - -importers: - - .: - dependencies: - '@fontsource-variable/jetbrains-mono': - specifier: ^5.2.8 - version: 5.3.0 - '@tauri-apps/api': - specifier: ^2.0.0 - version: 2.11.1 - '@tauri-apps/plugin-dialog': - specifier: ^2.7.1 - version: 2.7.2 - lucide-react: - specifier: ^1.23.0 - version: 1.25.0(react@19.2.7) - react: - specifier: ^19.0.0 - version: 19.2.7 - react-dom: - specifier: ^19.0.0 - version: 19.2.7(react@19.2.7) - devDependencies: - '@eslint/js': - specifier: ^10.0.1 - version: 10.0.1(eslint@10.7.0) - '@tauri-apps/cli': - specifier: ^2.0.0 - version: 2.11.4 - '@types/react': - specifier: ^19.0.0 - version: 19.2.17 - '@types/react-dom': - specifier: ^19.0.0 - version: 19.2.3(@types/react@19.2.17) - '@vitejs/plugin-react': - specifier: ^5.0.0 - version: 5.2.0(vite@7.3.6) - eslint: - specifier: ^10.7.0 - version: 10.7.0 - prettier: - specifier: ^3.9.5 - version: 3.9.5 - typescript: - specifier: ^5.8.0 - version: 5.9.3 - typescript-eslint: - specifier: ^8.63.0 - version: 8.65.0(eslint@10.7.0)(typescript@5.9.3) - vite: - specifier: ^7.0.0 - version: 7.3.6 - vitest: - specifier: ^3.2.4 - version: 3.2.7 - -packages: - - '@babel/code-frame@7.29.7': - resolution: {integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==} - engines: {node: '>=6.9.0'} - - '@babel/compat-data@7.29.7': - resolution: {integrity: sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==} - engines: {node: '>=6.9.0'} - - '@babel/core@7.29.7': - resolution: {integrity: sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==} - engines: {node: '>=6.9.0'} - - '@babel/generator@7.29.7': - resolution: {integrity: sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==} - engines: {node: '>=6.9.0'} - - '@babel/helper-compilation-targets@7.29.7': - resolution: {integrity: sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==} - engines: {node: '>=6.9.0'} - - '@babel/helper-globals@7.29.7': - resolution: {integrity: sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==} - engines: {node: '>=6.9.0'} - - '@babel/helper-module-imports@7.29.7': - resolution: {integrity: sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==} - engines: {node: '>=6.9.0'} - - '@babel/helper-module-transforms@7.29.7': - resolution: {integrity: sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0 - - '@babel/helper-plugin-utils@7.29.7': - resolution: {integrity: sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==} - engines: {node: '>=6.9.0'} - - '@babel/helper-string-parser@7.29.7': - resolution: {integrity: sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==} - engines: {node: '>=6.9.0'} - - '@babel/helper-validator-identifier@7.29.7': - resolution: {integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==} - engines: {node: '>=6.9.0'} - - '@babel/helper-validator-option@7.29.7': - resolution: {integrity: sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==} - engines: {node: '>=6.9.0'} - - '@babel/helpers@7.29.7': - resolution: {integrity: sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==} - engines: {node: '>=6.9.0'} - - '@babel/parser@7.29.7': - resolution: {integrity: sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==} - engines: {node: '>=6.0.0'} - hasBin: true - - '@babel/plugin-transform-react-jsx-self@7.29.7': - resolution: {integrity: sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-react-jsx-source@7.29.7': - resolution: {integrity: sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/template@7.29.7': - resolution: {integrity: sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==} - engines: {node: '>=6.9.0'} - - '@babel/traverse@7.29.7': - resolution: {integrity: sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==} - engines: {node: '>=6.9.0'} - - '@babel/types@7.29.7': - resolution: {integrity: sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==} - engines: {node: '>=6.9.0'} - - '@esbuild/aix-ppc64@0.28.1': - resolution: {integrity: sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==} - engines: {node: '>=18'} - cpu: [ppc64] - os: [aix] - - '@esbuild/android-arm64@0.28.1': - resolution: {integrity: sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==} - engines: {node: '>=18'} - cpu: [arm64] - os: [android] - - '@esbuild/android-arm@0.28.1': - resolution: {integrity: sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==} - engines: {node: '>=18'} - cpu: [arm] - os: [android] - - '@esbuild/android-x64@0.28.1': - resolution: {integrity: sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==} - engines: {node: '>=18'} - cpu: [x64] - os: [android] - - '@esbuild/darwin-arm64@0.28.1': - resolution: {integrity: sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==} - engines: {node: '>=18'} - cpu: [arm64] - os: [darwin] - - '@esbuild/darwin-x64@0.28.1': - resolution: {integrity: sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==} - engines: {node: '>=18'} - cpu: [x64] - os: [darwin] - - '@esbuild/freebsd-arm64@0.28.1': - resolution: {integrity: sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==} - engines: {node: '>=18'} - cpu: [arm64] - os: [freebsd] - - '@esbuild/freebsd-x64@0.28.1': - resolution: {integrity: sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==} - engines: {node: '>=18'} - cpu: [x64] - os: [freebsd] - - '@esbuild/linux-arm64@0.28.1': - resolution: {integrity: sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==} - engines: {node: '>=18'} - cpu: [arm64] - os: [linux] - - '@esbuild/linux-arm@0.28.1': - resolution: {integrity: sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==} - engines: {node: '>=18'} - cpu: [arm] - os: [linux] - - '@esbuild/linux-ia32@0.28.1': - resolution: {integrity: sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==} - engines: {node: '>=18'} - cpu: [ia32] - os: [linux] - - '@esbuild/linux-loong64@0.28.1': - resolution: {integrity: sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==} - engines: {node: '>=18'} - cpu: [loong64] - os: [linux] - - '@esbuild/linux-mips64el@0.28.1': - resolution: {integrity: sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==} - engines: {node: '>=18'} - cpu: [mips64el] - os: [linux] - - '@esbuild/linux-ppc64@0.28.1': - resolution: {integrity: sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==} - engines: {node: '>=18'} - cpu: [ppc64] - os: [linux] - - '@esbuild/linux-riscv64@0.28.1': - resolution: {integrity: sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==} - engines: {node: '>=18'} - cpu: [riscv64] - os: [linux] - - '@esbuild/linux-s390x@0.28.1': - resolution: {integrity: sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==} - engines: {node: '>=18'} - cpu: [s390x] - os: [linux] - - '@esbuild/linux-x64@0.28.1': - resolution: {integrity: sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==} - engines: {node: '>=18'} - cpu: [x64] - os: [linux] - - '@esbuild/netbsd-arm64@0.28.1': - resolution: {integrity: sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==} - engines: {node: '>=18'} - cpu: [arm64] - os: [netbsd] - - '@esbuild/netbsd-x64@0.28.1': - resolution: {integrity: sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==} - engines: {node: '>=18'} - cpu: [x64] - os: [netbsd] - - '@esbuild/openbsd-arm64@0.28.1': - resolution: {integrity: sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==} - engines: {node: '>=18'} - cpu: [arm64] - os: [openbsd] - - '@esbuild/openbsd-x64@0.28.1': - resolution: {integrity: sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==} - engines: {node: '>=18'} - cpu: [x64] - os: [openbsd] - - '@esbuild/openharmony-arm64@0.28.1': - resolution: {integrity: sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==} - engines: {node: '>=18'} - cpu: [arm64] - os: [openharmony] - - '@esbuild/sunos-x64@0.28.1': - resolution: {integrity: sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==} - engines: {node: '>=18'} - cpu: [x64] - os: [sunos] - - '@esbuild/win32-arm64@0.28.1': - resolution: {integrity: sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==} - engines: {node: '>=18'} - cpu: [arm64] - os: [win32] - - '@esbuild/win32-ia32@0.28.1': - resolution: {integrity: sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==} - engines: {node: '>=18'} - cpu: [ia32] - os: [win32] - - '@esbuild/win32-x64@0.28.1': - resolution: {integrity: sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==} - engines: {node: '>=18'} - cpu: [x64] - os: [win32] - - '@eslint-community/eslint-utils@4.9.1': - resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - peerDependencies: - eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 - - '@eslint-community/regexpp@4.12.2': - resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==} - engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} - - '@eslint/config-array@0.23.5': - resolution: {integrity: sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==} - engines: {node: ^20.19.0 || ^22.13.0 || >=24} - - '@eslint/config-helpers@0.6.0': - resolution: {integrity: sha512-ii6Bw9jJ2zi2cWA2Z+9/QZ/+3DX6kwaV5Q986D/CdP3Lap3w/pgQZ373FV7byY/i7L4IRH/G43I5dz1ClsCbpA==} - engines: {node: ^20.19.0 || ^22.13.0 || >=24} - - '@eslint/core@1.2.1': - resolution: {integrity: sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==} - engines: {node: ^20.19.0 || ^22.13.0 || >=24} - - '@eslint/js@10.0.1': - resolution: {integrity: sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==} - engines: {node: ^20.19.0 || ^22.13.0 || >=24} - peerDependencies: - eslint: ^10.0.0 - peerDependenciesMeta: - eslint: - optional: true - - '@eslint/object-schema@3.0.5': - resolution: {integrity: sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==} - engines: {node: ^20.19.0 || ^22.13.0 || >=24} - - '@eslint/plugin-kit@0.7.2': - resolution: {integrity: sha512-+CNAzxglkrpNf/kKywqQfk74QjtceuOE7Qm+AF8miRvPF/wmmK5+OJOgVh3AVTT3RP2mH3+FOaxlE5v72owk0A==} - engines: {node: ^20.19.0 || ^22.13.0 || >=24} - - '@fontsource-variable/jetbrains-mono@5.3.0': - resolution: {integrity: sha512-F32xpS2NsGYoQi2ADSkKTgpJj7ozajsGgDJ8woTnqjmIB+dxDIqImjl4pXZVEExu8UFZ2ndhmX18EBS/hdz3Lw==} - - '@humanfs/core@0.19.2': - resolution: {integrity: sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==} - engines: {node: '>=18.18.0'} - - '@humanfs/node@0.16.8': - resolution: {integrity: sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==} - engines: {node: '>=18.18.0'} - - '@humanfs/types@0.15.0': - resolution: {integrity: sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==} - engines: {node: '>=18.18.0'} - - '@humanwhocodes/module-importer@1.0.1': - resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} - engines: {node: '>=12.22'} - - '@humanwhocodes/retry@0.4.3': - resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} - engines: {node: '>=18.18'} - - '@jridgewell/gen-mapping@0.3.13': - resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} - - '@jridgewell/remapping@2.3.5': - resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} - - '@jridgewell/resolve-uri@3.1.2': - resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} - engines: {node: '>=6.0.0'} - - '@jridgewell/sourcemap-codec@1.5.5': - resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} - - '@jridgewell/trace-mapping@0.3.31': - resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} - - '@rolldown/pluginutils@1.0.0-rc.3': - resolution: {integrity: sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q==} - - '@rollup/rollup-android-arm-eabi@4.62.2': - resolution: {integrity: sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==} - cpu: [arm] - os: [android] - - '@rollup/rollup-android-arm64@4.62.2': - resolution: {integrity: sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw==} - cpu: [arm64] - os: [android] - - '@rollup/rollup-darwin-arm64@4.62.2': - resolution: {integrity: sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A==} - cpu: [arm64] - os: [darwin] - - '@rollup/rollup-darwin-x64@4.62.2': - resolution: {integrity: sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA==} - cpu: [x64] - os: [darwin] - - '@rollup/rollup-freebsd-arm64@4.62.2': - resolution: {integrity: sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw==} - cpu: [arm64] - os: [freebsd] - - '@rollup/rollup-freebsd-x64@4.62.2': - resolution: {integrity: sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg==} - cpu: [x64] - os: [freebsd] - - '@rollup/rollup-linux-arm-gnueabihf@4.62.2': - resolution: {integrity: sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg==} - cpu: [arm] - os: [linux] - libc: [glibc] - - '@rollup/rollup-linux-arm-musleabihf@4.62.2': - resolution: {integrity: sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA==} - cpu: [arm] - os: [linux] - libc: [musl] - - '@rollup/rollup-linux-arm64-gnu@4.62.2': - resolution: {integrity: sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA==} - cpu: [arm64] - os: [linux] - libc: [glibc] - - '@rollup/rollup-linux-arm64-musl@4.62.2': - resolution: {integrity: sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ==} - cpu: [arm64] - os: [linux] - libc: [musl] - - '@rollup/rollup-linux-loong64-gnu@4.62.2': - resolution: {integrity: sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg==} - cpu: [loong64] - os: [linux] - libc: [glibc] - - '@rollup/rollup-linux-loong64-musl@4.62.2': - resolution: {integrity: sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ==} - cpu: [loong64] - os: [linux] - libc: [musl] - - '@rollup/rollup-linux-ppc64-gnu@4.62.2': - resolution: {integrity: sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A==} - cpu: [ppc64] - os: [linux] - libc: [glibc] - - '@rollup/rollup-linux-ppc64-musl@4.62.2': - resolution: {integrity: sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w==} - cpu: [ppc64] - os: [linux] - libc: [musl] - - '@rollup/rollup-linux-riscv64-gnu@4.62.2': - resolution: {integrity: sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg==} - cpu: [riscv64] - os: [linux] - libc: [glibc] - - '@rollup/rollup-linux-riscv64-musl@4.62.2': - resolution: {integrity: sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q==} - cpu: [riscv64] - os: [linux] - libc: [musl] - - '@rollup/rollup-linux-s390x-gnu@4.62.2': - resolution: {integrity: sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg==} - cpu: [s390x] - os: [linux] - libc: [glibc] - - '@rollup/rollup-linux-x64-gnu@4.62.2': - resolution: {integrity: sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A==} - cpu: [x64] - os: [linux] - libc: [glibc] - - '@rollup/rollup-linux-x64-musl@4.62.2': - resolution: {integrity: sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg==} - cpu: [x64] - os: [linux] - libc: [musl] - - '@rollup/rollup-openbsd-x64@4.62.2': - resolution: {integrity: sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg==} - cpu: [x64] - os: [openbsd] - - '@rollup/rollup-openharmony-arm64@4.62.2': - resolution: {integrity: sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA==} - cpu: [arm64] - os: [openharmony] - - '@rollup/rollup-win32-arm64-msvc@4.62.2': - resolution: {integrity: sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg==} - cpu: [arm64] - os: [win32] - - '@rollup/rollup-win32-ia32-msvc@4.62.2': - resolution: {integrity: sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q==} - cpu: [ia32] - os: [win32] - - '@rollup/rollup-win32-x64-gnu@4.62.2': - resolution: {integrity: sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg==} - cpu: [x64] - os: [win32] - - '@rollup/rollup-win32-x64-msvc@4.62.2': - resolution: {integrity: sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA==} - cpu: [x64] - os: [win32] - - '@tauri-apps/api@2.11.1': - resolution: {integrity: sha512-M2FPuYND2m+wh5hfW9ZpSdxMPdEJovPBWwoHJmwUpysTYNHaOkVFN419m/K0LIgjb/7KU2vBgsUepJWugQCvAA==} - - '@tauri-apps/cli-darwin-arm64@2.11.4': - resolution: {integrity: sha512-1ryOF3ZhpZ/nemHV5zVwBQBz9jDGKmKPvWPADOhc83ig0P4bMc2iER4NbC6r9sjeIZ6RVQ4g3RZIYvezhcl4TQ==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [darwin] - - '@tauri-apps/cli-darwin-x64@2.11.4': - resolution: {integrity: sha512-uFsGQAAfuyz1k/yGLmkWfkBlgKAqZfxqlHmLWx81QU27RJWfmbNHCIq8T8w1e+VClleIuZUjpHWfoE4E3DLo3A==} - engines: {node: '>= 10'} - cpu: [x64] - os: [darwin] - - '@tauri-apps/cli-linux-arm-gnueabihf@2.11.4': - resolution: {integrity: sha512-IaHZn5CdBL21oUmjiVOS1ctw6Ip1O0pjp70FwOWmYz1myWe0SY96ZIj2FYf7pT0m8bI2h/hrs5ZbEXXh44/MkQ==} - engines: {node: '>= 10'} - cpu: [arm] - os: [linux] - - '@tauri-apps/cli-linux-arm64-gnu@2.11.4': - resolution: {integrity: sha512-N41/ukTRVe6XSuUTESuFdGeOW2i7k62tK+6gHK5Kd5/q5RPvvi19GaWAVPPb9u95HSGmTChSolBfzynUsssFaA==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [linux] - libc: [glibc] - - '@tauri-apps/cli-linux-arm64-musl@2.11.4': - resolution: {integrity: sha512-v277UnT/fB64xAfSroL5N3Km3tLmvATWqJJw/wRI+g6o+HkeD0slyE7gOhNs1MbjE41R7bQOTxMVoL3aomUJmw==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [linux] - libc: [musl] - - '@tauri-apps/cli-linux-riscv64-gnu@2.11.4': - resolution: {integrity: sha512-qqgNkQ2u1yZHxjhxsZaxUtRDW8dIqIYm33rx/mzwQv0SfY9x1B+iraj8vWeFiXjjSVVhEMepXSOts1TqPzvXNQ==} - engines: {node: '>= 10'} - cpu: [riscv64] - os: [linux] - libc: [glibc] - - '@tauri-apps/cli-linux-x64-gnu@2.11.4': - resolution: {integrity: sha512-2VRNWl84FOH0m2giiDkO2h0QXlcMJeX+zJDpI5kDIQAx6s+geF3v48F4DXfJez4GS/FdoDGnPnw1C2iYGbQ7bQ==} - engines: {node: '>= 10'} - cpu: [x64] - os: [linux] - libc: [glibc] - - '@tauri-apps/cli-linux-x64-musl@2.11.4': - resolution: {integrity: sha512-o9GyhYor/nc7xarmwDE3ka2szuW3uuZzXjHWh64Q8YX5AtSgxdQkFWzrY4O8KiGtVNvFBI14H3Q49Qj5TOIP/A==} - engines: {node: '>= 10'} - cpu: [x64] - os: [linux] - libc: [musl] - - '@tauri-apps/cli-win32-arm64-msvc@2.11.4': - resolution: {integrity: sha512-ld5Ehb598m0VkYyylRPNeCFsBe/km0jxis6KgMpl3IGY6I/i1RwQXO05I1AsXUXO2WC6AvB/Lw4qTf/asiuEiQ==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [win32] - - '@tauri-apps/cli-win32-ia32-msvc@2.11.4': - resolution: {integrity: sha512-12Hxi0XX/H5VFxO/bGgHkFWhml9VMgEOu9CidjeCeTNQ1l6fpUlbiGgSP7CLI3PFtW9/FfbeHieZ+kyWK5H7CA==} - engines: {node: '>= 10'} - cpu: [ia32] - os: [win32] - - '@tauri-apps/cli-win32-x64-msvc@2.11.4': - resolution: {integrity: sha512-+vDiqBIU5dMISg/wNvX3sF+ZHfgJGJ5T0AcO+EHNXV9GGAG+P5fzodlDXD3QdKCRgZxMoCm5PPvj3BqLNjBthw==} - engines: {node: '>= 10'} - cpu: [x64] - os: [win32] - - '@tauri-apps/cli@2.11.4': - resolution: {integrity: sha512-R8xGtMpwyetawSqm9kYOuMmEqkhUbvcUy8n0aNXIxollKBLESUu5f4Fx+64hgASYm1H+jSWq6jCW6zqTnH6hqQ==} - engines: {node: '>= 10'} - hasBin: true - - '@tauri-apps/plugin-dialog@2.7.2': - resolution: {integrity: sha512-pX0IGm1I3I6wc+zeKYcq1GSqogK6okCNX5fOdaNU5ab1AjGS6l1E5wFNjEb7meg7ZFSp0JUs+0jQGQNyOvLrsg==} - - '@types/babel__core@7.20.5': - resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} - - '@types/babel__generator@7.27.0': - resolution: {integrity: sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==} - - '@types/babel__template@7.4.4': - resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==} - - '@types/babel__traverse@7.28.0': - resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==} - - '@types/chai@5.2.3': - resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} - - '@types/deep-eql@4.0.2': - resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} - - '@types/esrecurse@4.3.1': - resolution: {integrity: sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==} - - '@types/estree@1.0.9': - resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} - - '@types/json-schema@7.0.15': - resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} - - '@types/react-dom@19.2.3': - resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==} - peerDependencies: - '@types/react': ^19.2.0 - - '@types/react@19.2.17': - resolution: {integrity: sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==} - - '@typescript-eslint/eslint-plugin@8.65.0': - resolution: {integrity: sha512-IEgob78X12rHpUmtcwFsXhZdVGJtwTVP8FiCLZkR6GlYVrl2PcuB+KhCE5BlVC/eQpQnu8WXRtkHZuPar+gCRA==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - '@typescript-eslint/parser': ^8.65.0 - eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 - typescript: '>=4.8.4 <6.1.0' - - '@typescript-eslint/parser@8.65.0': - resolution: {integrity: sha512-CZ4nMxWwgu1HEEFNkeaCptra9QCtkmKdgf3sWh1rl1trIhmxLilgTV4cwcbQ4wemnT4sWQN8CaKOmdYx+g2gMA==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 - typescript: '>=4.8.4 <6.1.0' - - '@typescript-eslint/project-service@8.65.0': - resolution: {integrity: sha512-SxnPhbTsGahizDgbu7oqFH/xVtzIqMd/s+WtnSxNxJZJpLbdT5IPdzg8EZxO3+PoKahXmwJLeNQOpKJb3/bi7Q==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - typescript: '>=4.8.4 <6.1.0' - - '@typescript-eslint/scope-manager@8.65.0': - resolution: {integrity: sha512-Esbl8OSYiVxBokYgWPf7VVWg/BE798wXhimnn9ML9Pt5qoDf8bfQlgjlKXR/k98+AcNzlLKYrpCcrcuZ9DZLgg==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@typescript-eslint/tsconfig-utils@8.65.0': - resolution: {integrity: sha512-j6GzGqCiRdA7Qhur2VVmKZAkBLfnHFQfx4TaJGL9RMveZqCo48jSHHO0DTgizEnGhtWnqmbtCUSrqSkdiY/0Hg==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - typescript: '>=4.8.4 <6.1.0' - - '@typescript-eslint/type-utils@8.65.0': - resolution: {integrity: sha512-YjaZ7PRI5qY7ax2L3PbvX0rRyGtipAReCWs0mhhDBHjH/vl0g0BonaGXrKdKpMbIIsMIwDgbk/xzkBTyAltS5g==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 - typescript: '>=4.8.4 <6.1.0' - - '@typescript-eslint/types@8.65.0': - resolution: {integrity: sha512-JSSwWNy+H0E/01jJEM+hrX6N0OFDzFzeIhHFSAS01tlVaevpG8cFyYRPhS5yjGOvBUx3sqQHVMjCL1CAZZMxBg==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@typescript-eslint/typescript-estree@8.65.0': - resolution: {integrity: sha512-JboAE2swaYt4tb1fHhHTABE2K+OLy09XfcTbhnk4Pw96f9dd2e9iYsJ28gBggHlo5z5x1rkyWvcPoTuNTd4oGg==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - typescript: '>=4.8.4 <6.1.0' - - '@typescript-eslint/utils@8.65.0': - resolution: {integrity: sha512-gXiwIHsYreboxeJucHKPvgwl7dXt50mF8s1/c00cP/WoVTyWKFdtfhRWwZiXYFU5H2O8vVoSLNrexFZjYS/SGA==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 - typescript: '>=4.8.4 <6.1.0' - - '@typescript-eslint/visitor-keys@8.65.0': - resolution: {integrity: sha512-8C71BQkGjiMmXtop7pHVJu1l2NNShFdkCyD6a2ezzs5vU/L3LRtb69EtcteFwz0mYMPzIgOw0n6OV4VBUWZd7A==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@vitejs/plugin-react@5.2.0': - resolution: {integrity: sha512-YmKkfhOAi3wsB1PhJq5Scj3GXMn3WvtQ/JC0xoopuHoXSdmtdStOpFrYaT1kie2YgFBcIe64ROzMYRjCrYOdYw==} - 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 - - '@vitest/expect@3.2.7': - resolution: {integrity: sha512-E8eBXaKibuvH2pSZErOjdVb5vF4PbKYcrnluBTYxEk1l/VhhwZg1kZQsdtjq+CsF5CFydf2Rdkz7jDHKSisi3w==} - - '@vitest/mocker@3.2.7': - resolution: {integrity: sha512-Trr0hYO9CM3Wj6ksWHRhK9IZpIY6wTMO5u/MqXurMxT57sWBaOPEtP3Oq60ihZuh5JsiagKfz95OcxdEP6dBrA==} - peerDependencies: - msw: ^2.4.9 - vite: ^5.0.0 || ^6.0.0 || ^7.0.0-0 - peerDependenciesMeta: - msw: - optional: true - vite: - optional: true - - '@vitest/pretty-format@3.2.7': - resolution: {integrity: sha512-KUHlwqVu0sRlhCdyPdQ/wBoTfRahjUky1MubOmYw9fWfIZy1gNoHpuaaQBPAaMaVYdQYHJLurzj8ECCj5OwTqA==} - - '@vitest/runner@3.2.7': - resolution: {integrity: sha512-sB9y4ovltoQP+WaUPwmSxO9WIg9Ig694Di5PalVPsYHklAdE027mehpWF2SQSVq+k6sFgaivbTjTJwZLSHbedA==} - - '@vitest/snapshot@3.2.7': - resolution: {integrity: sha512-7C+MwShwtBSI5Buwoyg3s/iY1eHL9PKAf+O1wVh/TdnjXUtkoL/9YQtre90i4MtNXM6edP1wJ2zOBpfCyhIS7g==} - - '@vitest/spy@3.2.7': - resolution: {integrity: sha512-Q2eQGI6d2L/hBtZ0qNuKcAGid68XK6cv1xsoaIma6PaJhHPoqcEJhYpXZ/5myCMqkNgtP6UKuBhbc0nHKnrkuQ==} - - '@vitest/utils@3.2.7': - resolution: {integrity: sha512-x6BDOd7dyo3PFLY3I9/HJ25X/6OurhGXk2/B9gOZNPF7XDVjeBK4k01lQE5uvDpbuheErh91qYuE1E2OEjK3Rw==} - - acorn-jsx@5.3.2: - resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} - peerDependencies: - acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 - - acorn@8.17.0: - resolution: {integrity: sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==} - engines: {node: '>=0.4.0'} - hasBin: true - - ajv@6.15.0: - resolution: {integrity: sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==} - - assertion-error@2.0.1: - resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} - engines: {node: '>=12'} - - balanced-match@4.0.4: - resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} - engines: {node: 18 || 20 || >=22} - - baseline-browser-mapping@2.10.44: - resolution: {integrity: sha512-T3ghW+sl/ZJ8w1v/yQx3qvJ9040DWoLBz8JT/CILbAKcFyG9b2MRe75v6W5uXjv6uH1lumK2Kv46y2zSkcej0Q==} - engines: {node: '>=6.0.0'} - hasBin: true - - brace-expansion@5.0.7: - resolution: {integrity: sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==} - engines: {node: 18 || 20 || >=22} - - browserslist@4.28.6: - resolution: {integrity: sha512-FQBYNK15VMslhLHpA7+n+n1GOlF1kId2xcCg7/j95f24AOF6VDYMNH4mFxF7KuaTdv627faazpOAjFzMrfJOUw==} - engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} - hasBin: true - - cac@6.7.14: - resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} - engines: {node: '>=8'} - - caniuse-lite@1.0.30001806: - resolution: {integrity: sha512-72Cuvd95zbSYPKq6Fhg8eDJRlzgWDf7/mtoZv6Qe/DYNCEBdNxoA3+rZAU2ZhGCpZlns3EssFavaZomckT5Uuw==} - - chai@5.3.3: - resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==} - engines: {node: '>=18'} - - check-error@2.1.3: - resolution: {integrity: sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==} - engines: {node: '>= 16'} - - convert-source-map@2.0.0: - resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} - - cross-spawn@7.0.6: - resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} - engines: {node: '>= 8'} - - csstype@3.2.3: - resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} - - debug@4.4.3: - resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} - engines: {node: '>=6.0'} - peerDependencies: - supports-color: '*' - peerDependenciesMeta: - supports-color: - optional: true - - deep-eql@5.0.2: - resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} - engines: {node: '>=6'} - - deep-is@0.1.4: - resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} - - electron-to-chromium@1.5.393: - resolution: {integrity: sha512-kiDJdIUawuEIcp9XoICKp1iTYDEbgguIPq526N1Q7jIQDeQ3CqoMx71025PI/7E48Ddtw2HuWsVjY7afEgNxmg==} - - es-module-lexer@1.7.0: - resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} - - esbuild@0.28.1: - resolution: {integrity: sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==} - engines: {node: '>=18'} - hasBin: true - - escalade@3.2.0: - resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} - engines: {node: '>=6'} - - escape-string-regexp@4.0.0: - resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} - engines: {node: '>=10'} - - eslint-scope@9.1.2: - resolution: {integrity: sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==} - engines: {node: ^20.19.0 || ^22.13.0 || >=24} - - eslint-visitor-keys@3.4.3: - resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - - eslint-visitor-keys@5.0.1: - resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==} - engines: {node: ^20.19.0 || ^22.13.0 || >=24} - - eslint@10.7.0: - resolution: {integrity: sha512-GVTD7s1vdIl6UYvAfriOPeY1Df8LIZjfofLvHwde+erDHGGuHyuM6xoxRxmHiebhYuD2p1vN4wWh0XzPARSGDQ==} - engines: {node: ^20.19.0 || ^22.13.0 || >=24} - hasBin: true - peerDependencies: - jiti: '*' - peerDependenciesMeta: - jiti: - optional: true - - espree@11.2.0: - resolution: {integrity: sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==} - engines: {node: ^20.19.0 || ^22.13.0 || >=24} - - esquery@1.7.0: - resolution: {integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==} - engines: {node: '>=0.10'} - - esrecurse@4.3.0: - resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} - engines: {node: '>=4.0'} - - estraverse@5.3.0: - resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} - engines: {node: '>=4.0'} - - estree-walker@3.0.3: - resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} - - esutils@2.0.3: - resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} - engines: {node: '>=0.10.0'} - - expect-type@1.4.0: - resolution: {integrity: sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==} - engines: {node: '>=12.0.0'} - - fast-deep-equal@3.1.3: - resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} - - fast-json-stable-stringify@2.1.0: - resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} - - fast-levenshtein@2.0.6: - resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} - - fdir@6.5.0: - resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} - engines: {node: '>=12.0.0'} - peerDependencies: - picomatch: ^3 || ^4 - peerDependenciesMeta: - picomatch: - optional: true - - file-entry-cache@8.0.0: - resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} - engines: {node: '>=16.0.0'} - - find-up@5.0.0: - resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} - engines: {node: '>=10'} - - flat-cache@4.0.1: - resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} - engines: {node: '>=16'} - - flatted@3.4.2: - resolution: {integrity: sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==} - - fsevents@2.3.3: - resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} - engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} - os: [darwin] - - gensync@1.0.0-beta.2: - resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} - engines: {node: '>=6.9.0'} - - glob-parent@6.0.2: - resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} - engines: {node: '>=10.13.0'} - - ignore@5.3.2: - resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} - engines: {node: '>= 4'} - - ignore@7.0.6: - resolution: {integrity: sha512-BAg6QkE8W+TuQLrrw0Ugr7HegXduRuuj8/ti2kSOc+jz1dmx8/WNcjr6XGnq5YpDWxFwwaavqD0+jIUOKelTsw==} - engines: {node: '>= 4'} - - imurmurhash@0.1.4: - resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} - engines: {node: '>=0.8.19'} - - is-extglob@2.1.1: - resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} - engines: {node: '>=0.10.0'} - - is-glob@4.0.3: - resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} - engines: {node: '>=0.10.0'} - - isexe@2.0.0: - resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} - - js-tokens@4.0.0: - resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} - - js-tokens@9.0.1: - resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==} - - jsesc@3.1.0: - resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} - engines: {node: '>=6'} - hasBin: true - - json-buffer@3.0.1: - resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} - - json-schema-traverse@0.4.1: - resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} - - json-stable-stringify-without-jsonify@1.0.1: - resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} - - json5@2.2.3: - resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} - engines: {node: '>=6'} - hasBin: true - - keyv@4.5.4: - resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} - - levn@0.4.1: - resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} - engines: {node: '>= 0.8.0'} - - locate-path@6.0.0: - resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} - engines: {node: '>=10'} - - loupe@3.2.1: - resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==} - - lru-cache@5.1.1: - resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} - - lucide-react@1.25.0: - resolution: {integrity: sha512-/mdJTRbiwcLOQ1NZZK1amZF9rIZyvO18D6r9TngE6TG1NmqHgFuT4eE7Xrkm9UsXMbBJD1NlfwHVltCDWHrOTw==} - peerDependencies: - react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0 - - magic-string@0.30.21: - resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} - - minimatch@10.2.5: - resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==} - engines: {node: 18 || 20 || >=22} - - ms@2.1.3: - resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} - - nanoid@3.3.16: - resolution: {integrity: sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==} - engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} - hasBin: true - - natural-compare@1.4.0: - resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} - - node-releases@2.0.51: - resolution: {integrity: sha512-wRNIrw4DmVLKQlbgOMdkMx27Wrpzes2hh5Jtbi2bjPd+4wJstWIqP5A+lscnqbm0xxmT5Bpg8Lec5ItEBwx6BQ==} - engines: {node: '>=18'} - - optionator@0.9.4: - resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} - engines: {node: '>= 0.8.0'} - - p-limit@3.1.0: - resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} - engines: {node: '>=10'} - - p-locate@5.0.0: - resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} - engines: {node: '>=10'} - - path-exists@4.0.0: - resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} - engines: {node: '>=8'} - - path-key@3.1.1: - resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} - engines: {node: '>=8'} - - pathe@2.0.3: - resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} - - pathval@2.0.1: - resolution: {integrity: sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==} - engines: {node: '>= 14.16'} - - picocolors@1.1.1: - resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} - - picomatch@4.0.5: - resolution: {integrity: sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==} - engines: {node: '>=12'} - - postcss@8.5.20: - resolution: {integrity: sha512-lW616l85ucIQL+FocMmL7pQFPqBmwejrCMg+iPxyImlrANNJG9NHq/RkyCZopDhd8C3LA03PHRJDjkbGu8vvug==} - engines: {node: ^10 || ^12 || >=14} - - prelude-ls@1.2.1: - resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} - engines: {node: '>= 0.8.0'} - - prettier@3.9.5: - resolution: {integrity: sha512-/FVl766LpUfB5vXgCYOYa0MeV/441Ia99AeICQIQFTY/Nw0roZwULcXpku5i1/m5kt/baz+s4Zogspd839HSMg==} - engines: {node: '>=14'} - hasBin: true - - punycode@2.3.1: - resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} - engines: {node: '>=6'} - - react-dom@19.2.7: - resolution: {integrity: sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==} - peerDependencies: - react: ^19.2.7 - - react-refresh@0.18.0: - resolution: {integrity: sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==} - engines: {node: '>=0.10.0'} - - react@19.2.7: - resolution: {integrity: sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==} - engines: {node: '>=0.10.0'} - - rollup@4.62.2: - resolution: {integrity: sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==} - engines: {node: '>=18.0.0', npm: '>=8.0.0'} - hasBin: true - - scheduler@0.27.0: - resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} - - semver@6.3.1: - resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} - hasBin: true - - semver@7.8.5: - resolution: {integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==} - engines: {node: '>=10'} - hasBin: true - - shebang-command@2.0.0: - resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} - engines: {node: '>=8'} - - shebang-regex@3.0.0: - resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} - engines: {node: '>=8'} - - siginfo@2.0.0: - resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} - - source-map-js@1.2.1: - resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} - engines: {node: '>=0.10.0'} - - stackback@0.0.2: - resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} - - std-env@3.10.0: - resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} - - strip-literal@3.1.0: - resolution: {integrity: sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==} - - tinybench@2.9.0: - resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} - - tinyexec@0.3.2: - resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} - - tinyglobby@0.2.17: - resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} - engines: {node: '>=12.0.0'} - - tinypool@1.1.1: - resolution: {integrity: sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==} - engines: {node: ^18.0.0 || >=20.0.0} - - tinyrainbow@2.0.0: - resolution: {integrity: sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==} - engines: {node: '>=14.0.0'} - - tinyspy@4.0.4: - resolution: {integrity: sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==} - engines: {node: '>=14.0.0'} - - ts-api-utils@2.5.0: - resolution: {integrity: sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==} - engines: {node: '>=18.12'} - peerDependencies: - typescript: '>=4.8.4' - - type-check@0.4.0: - resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} - engines: {node: '>= 0.8.0'} - - typescript-eslint@8.65.0: - resolution: {integrity: sha512-/ggrHAwyjENDusvyxbuqxAC2dTnZg/Z8F+fgQtYIz+L6n/9HfSlEZcFGV/NsMNa6CkGk0xUjUAFwC0vHOflvIA==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 - typescript: '>=4.8.4 <6.1.0' - - typescript@5.9.3: - resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} - engines: {node: '>=14.17'} - hasBin: true - - update-browserslist-db@1.2.3: - resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} - hasBin: true - peerDependencies: - browserslist: '>= 4.21.0' - - uri-js@4.4.1: - resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} - - vite-node@3.2.4: - resolution: {integrity: sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==} - engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} - hasBin: true - - vite@7.3.6: - resolution: {integrity: sha512-4XP60spRGjSZFf1qYH+dJIkK2znL3zQfl9KkOV9MkkRR/3Dls0dxaBsQPTloEc5BLXWPL9vsOxopxyKoMmDueg==} - engines: {node: ^20.19.0 || >=22.12.0} - hasBin: true - 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 - peerDependenciesMeta: - '@types/node': - optional: true - jiti: - optional: true - less: - optional: true - lightningcss: - optional: true - sass: - optional: true - sass-embedded: - optional: true - stylus: - optional: true - sugarss: - optional: true - terser: - optional: true - tsx: - optional: true - yaml: - optional: true - - vitest@3.2.7: - resolution: {integrity: sha512-KrxIJ62Fd89gfysR4WotlgZABiz2dqFPgqGzX7s+CwsqLFomRH7777ZcrOD6+WVAh7khPQP41A+BKbpcJFrdEg==} - engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} - hasBin: true - peerDependencies: - '@edge-runtime/vm': '*' - '@types/debug': ^4.1.12 - '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 - '@vitest/browser': 3.2.7 - '@vitest/ui': 3.2.7 - happy-dom: '*' - jsdom: '*' - peerDependenciesMeta: - '@edge-runtime/vm': - optional: true - '@types/debug': - optional: true - '@types/node': - optional: true - '@vitest/browser': - optional: true - '@vitest/ui': - optional: true - happy-dom: - optional: true - jsdom: - optional: true - - which@2.0.2: - resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} - engines: {node: '>= 8'} - hasBin: true - - why-is-node-running@2.3.0: - resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} - engines: {node: '>=8'} - hasBin: true - - word-wrap@1.2.5: - resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} - engines: {node: '>=0.10.0'} - - yallist@3.1.1: - resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} - - yocto-queue@0.1.0: - resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} - engines: {node: '>=10'} - -snapshots: - - '@babel/code-frame@7.29.7': - dependencies: - '@babel/helper-validator-identifier': 7.29.7 - js-tokens: 4.0.0 - picocolors: 1.1.1 - - '@babel/compat-data@7.29.7': {} - - '@babel/core@7.29.7': - dependencies: - '@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/core@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.4.3 - gensync: 1.0.0-beta.2 - json5: 2.2.3 - semver: 6.3.1 - transitivePeerDependencies: - - supports-color - - '@babel/generator@7.29.7': - dependencies: - '@babel/parser': 7.29.7 - '@babel/types': 7.29.7 - '@jridgewell/gen-mapping': 0.3.13 - '@jridgewell/trace-mapping': 0.3.31 - jsesc: 3.1.0 - - '@babel/helper-compilation-targets@7.29.7': - dependencies: - '@babel/compat-data': 7.29.7 - '@babel/helper-validator-option': 7.29.7 - browserslist: 4.28.6 - lru-cache: 5.1.1 - semver: 6.3.1 - - '@babel/helper-globals@7.29.7': {} - - '@babel/helper-module-imports@7.29.7': - dependencies: - '@babel/traverse': 7.29.7 - '@babel/types': 7.29.7 - transitivePeerDependencies: - - supports-color - - '@babel/helper-module-transforms@7.29.7(@babel/core@7.29.7)': - dependencies: - '@babel/core': 7.29.7 - '@babel/helper-module-imports': 7.29.7 - '@babel/helper-validator-identifier': 7.29.7 - '@babel/traverse': 7.29.7 - transitivePeerDependencies: - - supports-color - - '@babel/helper-plugin-utils@7.29.7': {} - - '@babel/helper-string-parser@7.29.7': {} - - '@babel/helper-validator-identifier@7.29.7': {} - - '@babel/helper-validator-option@7.29.7': {} - - '@babel/helpers@7.29.7': - dependencies: - '@babel/template': 7.29.7 - '@babel/types': 7.29.7 - - '@babel/parser@7.29.7': - dependencies: - '@babel/types': 7.29.7 - - '@babel/plugin-transform-react-jsx-self@7.29.7(@babel/core@7.29.7)': - dependencies: - '@babel/core': 7.29.7 - '@babel/helper-plugin-utils': 7.29.7 - - '@babel/plugin-transform-react-jsx-source@7.29.7(@babel/core@7.29.7)': - dependencies: - '@babel/core': 7.29.7 - '@babel/helper-plugin-utils': 7.29.7 - - '@babel/template@7.29.7': - dependencies: - '@babel/code-frame': 7.29.7 - '@babel/parser': 7.29.7 - '@babel/types': 7.29.7 - - '@babel/traverse@7.29.7': - dependencies: - '@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.4.3 - transitivePeerDependencies: - - supports-color - - '@babel/types@7.29.7': - dependencies: - '@babel/helper-string-parser': 7.29.7 - '@babel/helper-validator-identifier': 7.29.7 - - '@esbuild/aix-ppc64@0.28.1': - optional: true - - '@esbuild/android-arm64@0.28.1': - optional: true - - '@esbuild/android-arm@0.28.1': - optional: true - - '@esbuild/android-x64@0.28.1': - optional: true - - '@esbuild/darwin-arm64@0.28.1': - optional: true - - '@esbuild/darwin-x64@0.28.1': - optional: true - - '@esbuild/freebsd-arm64@0.28.1': - optional: true - - '@esbuild/freebsd-x64@0.28.1': - optional: true - - '@esbuild/linux-arm64@0.28.1': - optional: true - - '@esbuild/linux-arm@0.28.1': - optional: true - - '@esbuild/linux-ia32@0.28.1': - optional: true - - '@esbuild/linux-loong64@0.28.1': - optional: true - - '@esbuild/linux-mips64el@0.28.1': - optional: true - - '@esbuild/linux-ppc64@0.28.1': - optional: true - - '@esbuild/linux-riscv64@0.28.1': - optional: true - - '@esbuild/linux-s390x@0.28.1': - optional: true - - '@esbuild/linux-x64@0.28.1': - optional: true - - '@esbuild/netbsd-arm64@0.28.1': - optional: true - - '@esbuild/netbsd-x64@0.28.1': - optional: true - - '@esbuild/openbsd-arm64@0.28.1': - optional: true - - '@esbuild/openbsd-x64@0.28.1': - optional: true - - '@esbuild/openharmony-arm64@0.28.1': - optional: true - - '@esbuild/sunos-x64@0.28.1': - optional: true - - '@esbuild/win32-arm64@0.28.1': - optional: true - - '@esbuild/win32-ia32@0.28.1': - optional: true - - '@esbuild/win32-x64@0.28.1': - optional: true - - '@eslint-community/eslint-utils@4.9.1(eslint@10.7.0)': - dependencies: - eslint: 10.7.0 - eslint-visitor-keys: 3.4.3 - - '@eslint-community/regexpp@4.12.2': {} - - '@eslint/config-array@0.23.5': - dependencies: - '@eslint/object-schema': 3.0.5 - debug: 4.4.3 - minimatch: 10.2.5 - transitivePeerDependencies: - - supports-color - - '@eslint/config-helpers@0.6.0': - dependencies: - '@eslint/core': 1.2.1 - - '@eslint/core@1.2.1': - dependencies: - '@types/json-schema': 7.0.15 - - '@eslint/js@10.0.1(eslint@10.7.0)': - optionalDependencies: - eslint: 10.7.0 - - '@eslint/object-schema@3.0.5': {} - - '@eslint/plugin-kit@0.7.2': - dependencies: - '@eslint/core': 1.2.1 - levn: 0.4.1 - - '@fontsource-variable/jetbrains-mono@5.3.0': {} - - '@humanfs/core@0.19.2': - dependencies: - '@humanfs/types': 0.15.0 - - '@humanfs/node@0.16.8': - dependencies: - '@humanfs/core': 0.19.2 - '@humanfs/types': 0.15.0 - '@humanwhocodes/retry': 0.4.3 - - '@humanfs/types@0.15.0': {} - - '@humanwhocodes/module-importer@1.0.1': {} - - '@humanwhocodes/retry@0.4.3': {} - - '@jridgewell/gen-mapping@0.3.13': - dependencies: - '@jridgewell/sourcemap-codec': 1.5.5 - '@jridgewell/trace-mapping': 0.3.31 - - '@jridgewell/remapping@2.3.5': - dependencies: - '@jridgewell/gen-mapping': 0.3.13 - '@jridgewell/trace-mapping': 0.3.31 - - '@jridgewell/resolve-uri@3.1.2': {} - - '@jridgewell/sourcemap-codec@1.5.5': {} - - '@jridgewell/trace-mapping@0.3.31': - dependencies: - '@jridgewell/resolve-uri': 3.1.2 - '@jridgewell/sourcemap-codec': 1.5.5 - - '@rolldown/pluginutils@1.0.0-rc.3': {} - - '@rollup/rollup-android-arm-eabi@4.62.2': - optional: true - - '@rollup/rollup-android-arm64@4.62.2': - optional: true - - '@rollup/rollup-darwin-arm64@4.62.2': - optional: true - - '@rollup/rollup-darwin-x64@4.62.2': - optional: true - - '@rollup/rollup-freebsd-arm64@4.62.2': - optional: true - - '@rollup/rollup-freebsd-x64@4.62.2': - optional: true - - '@rollup/rollup-linux-arm-gnueabihf@4.62.2': - optional: true - - '@rollup/rollup-linux-arm-musleabihf@4.62.2': - optional: true - - '@rollup/rollup-linux-arm64-gnu@4.62.2': - optional: true - - '@rollup/rollup-linux-arm64-musl@4.62.2': - optional: true - - '@rollup/rollup-linux-loong64-gnu@4.62.2': - optional: true - - '@rollup/rollup-linux-loong64-musl@4.62.2': - optional: true - - '@rollup/rollup-linux-ppc64-gnu@4.62.2': - optional: true - - '@rollup/rollup-linux-ppc64-musl@4.62.2': - optional: true - - '@rollup/rollup-linux-riscv64-gnu@4.62.2': - optional: true - - '@rollup/rollup-linux-riscv64-musl@4.62.2': - optional: true - - '@rollup/rollup-linux-s390x-gnu@4.62.2': - optional: true - - '@rollup/rollup-linux-x64-gnu@4.62.2': - optional: true - - '@rollup/rollup-linux-x64-musl@4.62.2': - optional: true - - '@rollup/rollup-openbsd-x64@4.62.2': - optional: true - - '@rollup/rollup-openharmony-arm64@4.62.2': - optional: true - - '@rollup/rollup-win32-arm64-msvc@4.62.2': - optional: true - - '@rollup/rollup-win32-ia32-msvc@4.62.2': - optional: true - - '@rollup/rollup-win32-x64-gnu@4.62.2': - optional: true - - '@rollup/rollup-win32-x64-msvc@4.62.2': - optional: true - - '@tauri-apps/api@2.11.1': {} - - '@tauri-apps/cli-darwin-arm64@2.11.4': - optional: true - - '@tauri-apps/cli-darwin-x64@2.11.4': - optional: true - - '@tauri-apps/cli-linux-arm-gnueabihf@2.11.4': - optional: true - - '@tauri-apps/cli-linux-arm64-gnu@2.11.4': - optional: true - - '@tauri-apps/cli-linux-arm64-musl@2.11.4': - optional: true - - '@tauri-apps/cli-linux-riscv64-gnu@2.11.4': - optional: true - - '@tauri-apps/cli-linux-x64-gnu@2.11.4': - optional: true - - '@tauri-apps/cli-linux-x64-musl@2.11.4': - optional: true - - '@tauri-apps/cli-win32-arm64-msvc@2.11.4': - optional: true - - '@tauri-apps/cli-win32-ia32-msvc@2.11.4': - optional: true - - '@tauri-apps/cli-win32-x64-msvc@2.11.4': - optional: true - - '@tauri-apps/cli@2.11.4': - optionalDependencies: - '@tauri-apps/cli-darwin-arm64': 2.11.4 - '@tauri-apps/cli-darwin-x64': 2.11.4 - '@tauri-apps/cli-linux-arm-gnueabihf': 2.11.4 - '@tauri-apps/cli-linux-arm64-gnu': 2.11.4 - '@tauri-apps/cli-linux-arm64-musl': 2.11.4 - '@tauri-apps/cli-linux-riscv64-gnu': 2.11.4 - '@tauri-apps/cli-linux-x64-gnu': 2.11.4 - '@tauri-apps/cli-linux-x64-musl': 2.11.4 - '@tauri-apps/cli-win32-arm64-msvc': 2.11.4 - '@tauri-apps/cli-win32-ia32-msvc': 2.11.4 - '@tauri-apps/cli-win32-x64-msvc': 2.11.4 - - '@tauri-apps/plugin-dialog@2.7.2': - dependencies: - '@tauri-apps/api': 2.11.1 - - '@types/babel__core@7.20.5': - dependencies: - '@babel/parser': 7.29.7 - '@babel/types': 7.29.7 - '@types/babel__generator': 7.27.0 - '@types/babel__template': 7.4.4 - '@types/babel__traverse': 7.28.0 - - '@types/babel__generator@7.27.0': - dependencies: - '@babel/types': 7.29.7 - - '@types/babel__template@7.4.4': - dependencies: - '@babel/parser': 7.29.7 - '@babel/types': 7.29.7 - - '@types/babel__traverse@7.28.0': - dependencies: - '@babel/types': 7.29.7 - - '@types/chai@5.2.3': - dependencies: - '@types/deep-eql': 4.0.2 - assertion-error: 2.0.1 - - '@types/deep-eql@4.0.2': {} - - '@types/esrecurse@4.3.1': {} - - '@types/estree@1.0.9': {} - - '@types/json-schema@7.0.15': {} - - '@types/react-dom@19.2.3(@types/react@19.2.17)': - dependencies: - '@types/react': 19.2.17 - - '@types/react@19.2.17': - dependencies: - csstype: 3.2.3 - - '@typescript-eslint/eslint-plugin@8.65.0(@typescript-eslint/parser@8.65.0(eslint@10.7.0)(typescript@5.9.3))(eslint@10.7.0)(typescript@5.9.3)': - dependencies: - '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 8.65.0(eslint@10.7.0)(typescript@5.9.3) - '@typescript-eslint/scope-manager': 8.65.0 - '@typescript-eslint/type-utils': 8.65.0(eslint@10.7.0)(typescript@5.9.3) - '@typescript-eslint/utils': 8.65.0(eslint@10.7.0)(typescript@5.9.3) - '@typescript-eslint/visitor-keys': 8.65.0 - eslint: 10.7.0 - ignore: 7.0.6 - natural-compare: 1.4.0 - ts-api-utils: 2.5.0(typescript@5.9.3) - typescript: 5.9.3 - transitivePeerDependencies: - - supports-color - - '@typescript-eslint/parser@8.65.0(eslint@10.7.0)(typescript@5.9.3)': - dependencies: - '@typescript-eslint/scope-manager': 8.65.0 - '@typescript-eslint/types': 8.65.0 - '@typescript-eslint/typescript-estree': 8.65.0(typescript@5.9.3) - '@typescript-eslint/visitor-keys': 8.65.0 - debug: 4.4.3 - eslint: 10.7.0 - typescript: 5.9.3 - transitivePeerDependencies: - - supports-color - - '@typescript-eslint/project-service@8.65.0(typescript@5.9.3)': - dependencies: - '@typescript-eslint/tsconfig-utils': 8.65.0(typescript@5.9.3) - '@typescript-eslint/types': 8.65.0 - debug: 4.4.3 - typescript: 5.9.3 - transitivePeerDependencies: - - supports-color - - '@typescript-eslint/scope-manager@8.65.0': - dependencies: - '@typescript-eslint/types': 8.65.0 - '@typescript-eslint/visitor-keys': 8.65.0 - - '@typescript-eslint/tsconfig-utils@8.65.0(typescript@5.9.3)': - dependencies: - typescript: 5.9.3 - - '@typescript-eslint/type-utils@8.65.0(eslint@10.7.0)(typescript@5.9.3)': - dependencies: - '@typescript-eslint/types': 8.65.0 - '@typescript-eslint/typescript-estree': 8.65.0(typescript@5.9.3) - '@typescript-eslint/utils': 8.65.0(eslint@10.7.0)(typescript@5.9.3) - debug: 4.4.3 - eslint: 10.7.0 - ts-api-utils: 2.5.0(typescript@5.9.3) - typescript: 5.9.3 - transitivePeerDependencies: - - supports-color - - '@typescript-eslint/types@8.65.0': {} - - '@typescript-eslint/typescript-estree@8.65.0(typescript@5.9.3)': - dependencies: - '@typescript-eslint/project-service': 8.65.0(typescript@5.9.3) - '@typescript-eslint/tsconfig-utils': 8.65.0(typescript@5.9.3) - '@typescript-eslint/types': 8.65.0 - '@typescript-eslint/visitor-keys': 8.65.0 - debug: 4.4.3 - minimatch: 10.2.5 - semver: 7.8.5 - tinyglobby: 0.2.17 - ts-api-utils: 2.5.0(typescript@5.9.3) - typescript: 5.9.3 - transitivePeerDependencies: - - supports-color - - '@typescript-eslint/utils@8.65.0(eslint@10.7.0)(typescript@5.9.3)': - dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@10.7.0) - '@typescript-eslint/scope-manager': 8.65.0 - '@typescript-eslint/types': 8.65.0 - '@typescript-eslint/typescript-estree': 8.65.0(typescript@5.9.3) - eslint: 10.7.0 - typescript: 5.9.3 - transitivePeerDependencies: - - supports-color - - '@typescript-eslint/visitor-keys@8.65.0': - dependencies: - '@typescript-eslint/types': 8.65.0 - eslint-visitor-keys: 5.0.1 - - '@vitejs/plugin-react@5.2.0(vite@7.3.6)': - dependencies: - '@babel/core': 7.29.7 - '@babel/plugin-transform-react-jsx-self': 7.29.7(@babel/core@7.29.7) - '@babel/plugin-transform-react-jsx-source': 7.29.7(@babel/core@7.29.7) - '@rolldown/pluginutils': 1.0.0-rc.3 - '@types/babel__core': 7.20.5 - react-refresh: 0.18.0 - vite: 7.3.6 - transitivePeerDependencies: - - supports-color - - '@vitest/expect@3.2.7': - dependencies: - '@types/chai': 5.2.3 - '@vitest/spy': 3.2.7 - '@vitest/utils': 3.2.7 - chai: 5.3.3 - tinyrainbow: 2.0.0 - - '@vitest/mocker@3.2.7(vite@7.3.6)': - dependencies: - '@vitest/spy': 3.2.7 - estree-walker: 3.0.3 - magic-string: 0.30.21 - optionalDependencies: - vite: 7.3.6 - - '@vitest/pretty-format@3.2.7': - dependencies: - tinyrainbow: 2.0.0 - - '@vitest/runner@3.2.7': - dependencies: - '@vitest/utils': 3.2.7 - pathe: 2.0.3 - strip-literal: 3.1.0 - - '@vitest/snapshot@3.2.7': - dependencies: - '@vitest/pretty-format': 3.2.7 - magic-string: 0.30.21 - pathe: 2.0.3 - - '@vitest/spy@3.2.7': - dependencies: - tinyspy: 4.0.4 - - '@vitest/utils@3.2.7': - dependencies: - '@vitest/pretty-format': 3.2.7 - loupe: 3.2.1 - tinyrainbow: 2.0.0 - - acorn-jsx@5.3.2(acorn@8.17.0): - dependencies: - acorn: 8.17.0 - - acorn@8.17.0: {} - - ajv@6.15.0: - dependencies: - fast-deep-equal: 3.1.3 - fast-json-stable-stringify: 2.1.0 - json-schema-traverse: 0.4.1 - uri-js: 4.4.1 - - assertion-error@2.0.1: {} - - balanced-match@4.0.4: {} - - baseline-browser-mapping@2.10.44: {} - - brace-expansion@5.0.7: - dependencies: - balanced-match: 4.0.4 - - browserslist@4.28.6: - dependencies: - baseline-browser-mapping: 2.10.44 - caniuse-lite: 1.0.30001806 - electron-to-chromium: 1.5.393 - node-releases: 2.0.51 - update-browserslist-db: 1.2.3(browserslist@4.28.6) - - cac@6.7.14: {} - - caniuse-lite@1.0.30001806: {} - - chai@5.3.3: - dependencies: - assertion-error: 2.0.1 - check-error: 2.1.3 - deep-eql: 5.0.2 - loupe: 3.2.1 - pathval: 2.0.1 - - check-error@2.1.3: {} - - convert-source-map@2.0.0: {} - - cross-spawn@7.0.6: - dependencies: - path-key: 3.1.1 - shebang-command: 2.0.0 - which: 2.0.2 - - csstype@3.2.3: {} - - debug@4.4.3: - dependencies: - ms: 2.1.3 - - deep-eql@5.0.2: {} - - deep-is@0.1.4: {} - - electron-to-chromium@1.5.393: {} - - es-module-lexer@1.7.0: {} - - esbuild@0.28.1: - optionalDependencies: - '@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 - - escalade@3.2.0: {} - - escape-string-regexp@4.0.0: {} - - eslint-scope@9.1.2: - dependencies: - '@types/esrecurse': 4.3.1 - '@types/estree': 1.0.9 - esrecurse: 4.3.0 - estraverse: 5.3.0 - - eslint-visitor-keys@3.4.3: {} - - eslint-visitor-keys@5.0.1: {} - - eslint@10.7.0: - dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@10.7.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.8 - '@humanwhocodes/module-importer': 1.0.1 - '@humanwhocodes/retry': 0.4.3 - '@types/estree': 1.0.9 - ajv: 6.15.0 - cross-spawn: 7.0.6 - debug: 4.4.3 - 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.3 - fast-deep-equal: 3.1.3 - file-entry-cache: 8.0.0 - find-up: 5.0.0 - glob-parent: 6.0.2 - ignore: 5.3.2 - imurmurhash: 0.1.4 - is-glob: 4.0.3 - json-stable-stringify-without-jsonify: 1.0.1 - minimatch: 10.2.5 - natural-compare: 1.4.0 - optionator: 0.9.4 - transitivePeerDependencies: - - supports-color - - espree@11.2.0: - dependencies: - acorn: 8.17.0 - acorn-jsx: 5.3.2(acorn@8.17.0) - eslint-visitor-keys: 5.0.1 - - esquery@1.7.0: - dependencies: - estraverse: 5.3.0 - - esrecurse@4.3.0: - dependencies: - estraverse: 5.3.0 - - estraverse@5.3.0: {} - - estree-walker@3.0.3: - dependencies: - '@types/estree': 1.0.9 - - esutils@2.0.3: {} - - expect-type@1.4.0: {} - - fast-deep-equal@3.1.3: {} - - fast-json-stable-stringify@2.1.0: {} - - fast-levenshtein@2.0.6: {} - - fdir@6.5.0(picomatch@4.0.5): - optionalDependencies: - picomatch: 4.0.5 - - file-entry-cache@8.0.0: - dependencies: - flat-cache: 4.0.1 - - find-up@5.0.0: - dependencies: - locate-path: 6.0.0 - path-exists: 4.0.0 - - flat-cache@4.0.1: - dependencies: - flatted: 3.4.2 - keyv: 4.5.4 - - flatted@3.4.2: {} - - fsevents@2.3.3: - optional: true - - gensync@1.0.0-beta.2: {} - - glob-parent@6.0.2: - dependencies: - is-glob: 4.0.3 - - ignore@5.3.2: {} - - ignore@7.0.6: {} - - imurmurhash@0.1.4: {} - - is-extglob@2.1.1: {} - - is-glob@4.0.3: - dependencies: - is-extglob: 2.1.1 - - isexe@2.0.0: {} - - js-tokens@4.0.0: {} - - js-tokens@9.0.1: {} - - jsesc@3.1.0: {} - - json-buffer@3.0.1: {} - - json-schema-traverse@0.4.1: {} - - json-stable-stringify-without-jsonify@1.0.1: {} - - json5@2.2.3: {} - - keyv@4.5.4: - dependencies: - json-buffer: 3.0.1 - - levn@0.4.1: - dependencies: - prelude-ls: 1.2.1 - type-check: 0.4.0 - - locate-path@6.0.0: - dependencies: - p-locate: 5.0.0 - - loupe@3.2.1: {} - - lru-cache@5.1.1: - dependencies: - yallist: 3.1.1 - - lucide-react@1.25.0(react@19.2.7): - dependencies: - react: 19.2.7 - - magic-string@0.30.21: - dependencies: - '@jridgewell/sourcemap-codec': 1.5.5 - - minimatch@10.2.5: - dependencies: - brace-expansion: 5.0.7 - - ms@2.1.3: {} - - nanoid@3.3.16: {} - - natural-compare@1.4.0: {} - - node-releases@2.0.51: {} - - optionator@0.9.4: - dependencies: - deep-is: 0.1.4 - fast-levenshtein: 2.0.6 - levn: 0.4.1 - prelude-ls: 1.2.1 - type-check: 0.4.0 - word-wrap: 1.2.5 - - p-limit@3.1.0: - dependencies: - yocto-queue: 0.1.0 - - p-locate@5.0.0: - dependencies: - p-limit: 3.1.0 - - path-exists@4.0.0: {} - - path-key@3.1.1: {} - - pathe@2.0.3: {} - - pathval@2.0.1: {} - - picocolors@1.1.1: {} - - picomatch@4.0.5: {} - - postcss@8.5.20: - dependencies: - nanoid: 3.3.16 - picocolors: 1.1.1 - source-map-js: 1.2.1 - - prelude-ls@1.2.1: {} - - prettier@3.9.5: {} - - punycode@2.3.1: {} - - react-dom@19.2.7(react@19.2.7): - dependencies: - react: 19.2.7 - scheduler: 0.27.0 - - react-refresh@0.18.0: {} - - react@19.2.7: {} - - 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.3 - - scheduler@0.27.0: {} - - semver@6.3.1: {} - - semver@7.8.5: {} - - shebang-command@2.0.0: - dependencies: - shebang-regex: 3.0.0 - - shebang-regex@3.0.0: {} - - siginfo@2.0.0: {} - - source-map-js@1.2.1: {} - - stackback@0.0.2: {} - - std-env@3.10.0: {} - - strip-literal@3.1.0: - dependencies: - js-tokens: 9.0.1 - - tinybench@2.9.0: {} - - tinyexec@0.3.2: {} - - tinyglobby@0.2.17: - dependencies: - fdir: 6.5.0(picomatch@4.0.5) - picomatch: 4.0.5 - - tinypool@1.1.1: {} - - tinyrainbow@2.0.0: {} - - tinyspy@4.0.4: {} - - ts-api-utils@2.5.0(typescript@5.9.3): - dependencies: - typescript: 5.9.3 - - type-check@0.4.0: - dependencies: - prelude-ls: 1.2.1 - - typescript-eslint@8.65.0(eslint@10.7.0)(typescript@5.9.3): - dependencies: - '@typescript-eslint/eslint-plugin': 8.65.0(@typescript-eslint/parser@8.65.0(eslint@10.7.0)(typescript@5.9.3))(eslint@10.7.0)(typescript@5.9.3) - '@typescript-eslint/parser': 8.65.0(eslint@10.7.0)(typescript@5.9.3) - '@typescript-eslint/typescript-estree': 8.65.0(typescript@5.9.3) - '@typescript-eslint/utils': 8.65.0(eslint@10.7.0)(typescript@5.9.3) - eslint: 10.7.0 - typescript: 5.9.3 - transitivePeerDependencies: - - supports-color - - typescript@5.9.3: {} - - update-browserslist-db@1.2.3(browserslist@4.28.6): - dependencies: - browserslist: 4.28.6 - escalade: 3.2.0 - picocolors: 1.1.1 - - uri-js@4.4.1: - dependencies: - punycode: 2.3.1 - - vite-node@3.2.4: - dependencies: - cac: 6.7.14 - debug: 4.4.3 - es-module-lexer: 1.7.0 - pathe: 2.0.3 - vite: 7.3.6 - transitivePeerDependencies: - - '@types/node' - - jiti - - less - - lightningcss - - sass - - sass-embedded - - stylus - - sugarss - - supports-color - - terser - - tsx - - yaml - - vite@7.3.6: - dependencies: - esbuild: 0.28.1 - fdir: 6.5.0(picomatch@4.0.5) - picomatch: 4.0.5 - postcss: 8.5.20 - rollup: 4.62.2 - tinyglobby: 0.2.17 - optionalDependencies: - fsevents: 2.3.3 - - vitest@3.2.7: - dependencies: - '@types/chai': 5.2.3 - '@vitest/expect': 3.2.7 - '@vitest/mocker': 3.2.7(vite@7.3.6) - '@vitest/pretty-format': 3.2.7 - '@vitest/runner': 3.2.7 - '@vitest/snapshot': 3.2.7 - '@vitest/spy': 3.2.7 - '@vitest/utils': 3.2.7 - chai: 5.3.3 - debug: 4.4.3 - expect-type: 1.4.0 - magic-string: 0.30.21 - pathe: 2.0.3 - picomatch: 4.0.5 - std-env: 3.10.0 - tinybench: 2.9.0 - tinyexec: 0.3.2 - tinyglobby: 0.2.17 - tinypool: 1.1.1 - tinyrainbow: 2.0.0 - vite: 7.3.6 - vite-node: 3.2.4 - why-is-node-running: 2.3.0 - transitivePeerDependencies: - - jiti - - less - - lightningcss - - msw - - sass - - sass-embedded - - stylus - - sugarss - - supports-color - - terser - - tsx - - yaml - - which@2.0.2: - dependencies: - isexe: 2.0.0 - - why-is-node-running@2.3.0: - dependencies: - siginfo: 2.0.0 - stackback: 0.0.2 - - word-wrap@1.2.5: {} - - yallist@3.1.1: {} - - yocto-queue@0.1.0: {} diff --git a/release.cmd b/release.cmd new file mode 100644 index 0000000..fdbc198 --- /dev/null +++ b/release.cmd @@ -0,0 +1,8 @@ +@echo off +where pwsh >nul 2>nul +if errorlevel 1 ( + powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0scripts\prepare-release.ps1" -Publish %* +) else ( + pwsh -NoProfile -File "%~dp0scripts\prepare-release.ps1" -Publish %* +) +exit /b %errorlevel% diff --git a/scripts/audit-windows-smoke.ps1 b/scripts/audit-windows-smoke.ps1 index 885b191..f0cd71c 100644 --- a/scripts/audit-windows-smoke.ps1 +++ b/scripts/audit-windows-smoke.ps1 @@ -2,8 +2,9 @@ param( [ValidateSet("PlanOnly", "Capture")] [string]$Mode = "PlanOnly", [string]$DataRoot = "C:\ProgramData\ProxyWarden", - [string]$ProxiFyreRoot = "C:\Tools\ProxiFyre", - [string]$SingBoxRoot = "C:\Program Files\ProxyWarden\sing-box", + [string]$AppRoot = "C:\Program Files\ProxyWarden", + [string]$ProxiFyreRoot = "C:\Program Files\ProxyWarden\components\ProxiFyre", + [string]$SingBoxRoot = "C:\Program Files\ProxyWarden\components\sing-box", [string]$ForeignServiceName = "", [string]$OutputPath = "" ) @@ -110,10 +111,35 @@ function Get-SecretFindingCategories { return $findings } +function Get-InternalStateEvidence { + param([string]$Root) + + $categories = [ordered]@{ + cutoverJournal = ".proxywarden-cutover" + cutoverQuarantine = ".proxywarden-quarantine" + packageStaging = ".proxywarden-package-staging" + privilegedJobs = ".proxywarden-privileged-jobs" + serviceLogs = ".proxywarden-service-logs" + singBoxCleanupTombstone = ".proxywarden-sing-box-cleanup" + } + + $result = @() + foreach ($entry in $categories.GetEnumerator()) { + $path = Join-Path $Root $entry.Value + $item = Get-Item -LiteralPath $path -Force -ErrorAction SilentlyContinue + $result += [ordered]@{ + category = $entry.Key + present = $null -ne $item + itemType = if ($null -eq $item) { $null } elseif ($item.PSIsContainer) { "directory" } else { "file" } + } + } + return $result +} + try { - $quotedServiceFixture = '"C:\Program Files\ProxyWarden\sing-box\ProxyWardenSingBox.exe" -service' + $quotedServiceFixture = '"C:\Program Files\ProxyWarden\components\sing-box\ProxyWardenSingBox.exe" -service' $quotedExecutable = Get-ServiceExecutablePath -PathName $quotedServiceFixture - if (-not (Test-PathUnderRoot -Path $quotedExecutable -Root "C:\Program Files\ProxyWarden\sing-box")) { + if (-not (Test-PathUnderRoot -Path $quotedExecutable -Root "C:\Program Files\ProxyWarden\components\sing-box")) { throw "Quoted service PathName ownership self-test failed." } @@ -122,11 +148,12 @@ try { serviceNames = @("ProxiFyreService", "ProxyWardenSingBox") foreignServiceName = $ForeignServiceName roots = [ordered]@{ + app = [IO.Path]::GetFullPath($AppRoot) data = [IO.Path]::GetFullPath($DataRoot) proxifyre = [IO.Path]::GetFullPath($ProxiFyreRoot) singbox = [IO.Path]::GetFullPath($SingBoxRoot) } - checks = @("service-state-and-path", "managed-root-membership", "file-metadata", "secret-category-scan") + checks = @("service-state-and-path", "managed-root-membership", "file-metadata", "secret-category-scan", "internal-state-presence-only") } if ($Mode -eq "PlanOnly") { @@ -167,6 +194,7 @@ try { ownership = $ownership files = @(Get-FileEvidence -Root $DataRoot) secretFindingCategories = @(Get-SecretFindingCategories -Root $DataRoot) + internalState = @(Get-InternalStateEvidence -Root $AppRoot) } $report | ConvertTo-Json -Depth 8 | Set-Content -LiteralPath $outputFullPath -Encoding UTF8 @@ -175,6 +203,7 @@ try { serviceCount = @($services | Where-Object found).Count fileCount = @($report.files).Count secretFindingCount = @($report.secretFindingCategories).Count + internalStateCategoryCount = @($report.internalState).Count } } catch { New-Result -Success $false -Action "audit-windows-smoke.$($Mode.ToLowerInvariant())" -Changed $false -Message $_.Exception.Message -Details @{} diff --git a/scripts/check-runtime-powershell-boundary.ps1 b/scripts/check-runtime-powershell-boundary.ps1 new file mode 100644 index 0000000..c3f93f3 --- /dev/null +++ b/scripts/check-runtime-powershell-boundary.ps1 @@ -0,0 +1,253 @@ +[CmdletBinding()] +param( + [switch]$CheckOnly +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = "Stop" + +$RepoRoot = [IO.Path]::GetFullPath((Join-Path $PSScriptRoot "..")) +$AllowedPowerShellFiles = @( + "scripts/audit-windows-smoke.ps1", + "scripts/check-runtime-powershell-boundary.ps1", + "scripts/prepare-release.ps1", + "scripts/update-component-bundle.ps1" +) +$ExpectedNsisFlags = @( + "--nsis-uninstall-managed", + "--nsis-verify-upgrade" +) +$IgnoredPathPattern = '^(?:\.git|node_modules|dist|releases|src-tauri/target)(?:/|$)' + +function Get-RelativeRepoPath { + param([string]$Path) + + $rootUri = [Uri]($RepoRoot.TrimEnd("\", "/") + [IO.Path]::DirectorySeparatorChar) + $pathUri = [Uri][IO.Path]::GetFullPath($Path) + [Uri]::UnescapeDataString($rootUri.MakeRelativeUri($pathUri).ToString()).Replace("\", "/") +} + +function New-Violation { + param( + [string]$Rule, + [string]$Path, + [string]$Message, + [int]$Line = 0 + ) + + [ordered]@{ + rule = $Rule + path = $Path + line = $Line + message = $Message + } +} + +function Get-ProductionLines { + param([string]$Path) + + $lines = @(Get-Content -LiteralPath $Path) + for ($index = 0; $index -lt $lines.Count; $index++) { + if ($lines[$index] -match '^\s*#\s*\[\s*cfg\s*\(\s*test\s*\)\s*\]') { + if ($index -eq 0) { return @() } + return @($lines[0..($index - 1)]) + } + } + return $lines +} + +function Write-Result { + param( + [bool]$Success, + [string]$Message, + [object[]]$Violations, + [int]$PowerShellFileCount, + [int]$ProductionFileCount, + [string[]]$ObservedNsisFlags + ) + + [ordered]@{ + success = $Success + action = "runtime-powershell-boundary.check" + changed = $false + message = $Message + details = [ordered]@{ + allowlistedPowerShellFiles = $AllowedPowerShellFiles + scannedPowerShellFileCount = $PowerShellFileCount + scannedProductionFileCount = $ProductionFileCount + expectedNsisFlags = $ExpectedNsisFlags + observedNsisFlags = $ObservedNsisFlags + violations = $Violations + } + } | ConvertTo-Json -Depth 8 +} + +$violations = New-Object System.Collections.Generic.List[object] +$powerShellFileCount = 0 +$productionFileCount = 0 +$observedNsisFlags = @() + +try { + if (-not $CheckOnly) { + [void]$violations.Add((New-Violation ` + -Rule "check-only-required" ` + -Path "scripts/check-runtime-powershell-boundary.ps1" ` + -Message "Invoke this read-only boundary as -CheckOnly.")) + } + + $powerShellFiles = @( + Get-ChildItem -LiteralPath $RepoRoot -Recurse -File | + Where-Object { $_.Extension -in @(".ps1", ".psm1", ".psd1") } | + ForEach-Object { + [ordered]@{ + fullPath = $_.FullName + relativePath = Get-RelativeRepoPath -Path $_.FullName + } + } | + Where-Object { $_.relativePath -notmatch $IgnoredPathPattern } | + Sort-Object relativePath + ) + $powerShellFileCount = $powerShellFiles.Count + + foreach ($file in $powerShellFiles) { + if ($file.relativePath -notin $AllowedPowerShellFiles) { + [void]$violations.Add((New-Violation ` + -Rule "unexpected-powershell-file" ` + -Path $file.relativePath ` + -Message "PowerShell is allowed only for the exact build/release/QA allowlist.")) + } + } + foreach ($allowedPath in $AllowedPowerShellFiles) { + if ($allowedPath -notin $powerShellFiles.relativePath) { + [void]$violations.Add((New-Violation ` + -Rule "missing-allowlisted-tool" ` + -Path $allowedPath ` + -Message "Required build/release/QA tool is missing.")) + } + } + + $forbiddenRuntimeFiles = @( + "src-tauri/src/elevated_scripts.rs", + "src-tauri/src/helper.rs", + "src-tauri/src/powershell.rs", + "src-tauri/src/proxifyre_scripts.rs", + "src-tauri/bundled/cleanup/uninstall-managed-components.ps1" + ) + foreach ($relativePath in $forbiddenRuntimeFiles) { + if (Test-Path -LiteralPath (Join-Path $RepoRoot $relativePath.Replace("/", "\"))) { + [void]$violations.Add((New-Violation ` + -Rule "legacy-runtime-file" ` + -Path $relativePath ` + -Message "Legacy runtime PowerShell owner must be deleted after the native cutover.")) + } + } + + $tauriConfigPath = Join-Path $RepoRoot "src-tauri\tauri.conf.json" + if ((Get-Content -LiteralPath $tauriConfigPath -Raw) -match '(?i)bundled[\\/]cleanup') { + [void]$violations.Add((New-Violation ` + -Rule "bundled-cleanup-resource" ` + -Path "src-tauri/tauri.conf.json" ` + -Message "The installer must not package the displaced PowerShell cleanup resource.")) + } + + $productionFiles = @( + Get-ChildItem -LiteralPath (Join-Path $RepoRoot "src-tauri\src") -Recurse -File -Filter "*.rs" + Get-ChildItem -LiteralPath (Join-Path $RepoRoot "src-tauri\bundled\installer-hooks") -Recurse -File | Where-Object { $_.Extension -in @(".nsh", ".nsi") } + ) + $productionFileCount = $productionFiles.Count + $rules = @( + [ordered]@{ name = "powershell-process"; pattern = '(?i)(?:command_no_window|Command::new).*\b(?:powershell|pwsh)(?:\.exe)?\b' }, + [ordered]@{ name = "powershell-command-line"; pattern = '(?i)\b(?:powershell|pwsh)(?:\.exe)?\b\s+-[A-Za-z]' }, + [ordered]@{ name = "powershell-policy-bypass"; pattern = '(?i)-ExecutionPolicy\b' }, + [ordered]@{ name = "powershell-script-path"; pattern = '(?i)\.ps1\b' }, + [ordered]@{ name = "powershell-runtime-helper"; pattern = '(?i)\b(?:run|write)_powershell_(?:command|file|script)\b' }, + [ordered]@{ name = "legacy-module-declaration"; pattern = '(?i)\b(?:pub\s+)?mod\s+(?:elevated_scripts|helper|powershell|proxifyre_scripts)\s*;' }, + [ordered]@{ name = "legacy-module-reexport"; pattern = '(?i)\bpub\s+use\s+crate::(?:elevated_scripts|helper|powershell|proxifyre_scripts)\b' } + ) + + $productionTextParts = New-Object System.Collections.Generic.List[string] + $rustTextParts = New-Object System.Collections.Generic.List[string] + foreach ($file in $productionFiles) { + $relativePath = Get-RelativeRepoPath -Path $file.FullName + $lines = @(Get-ProductionLines -Path $file.FullName) + for ($index = 0; $index -lt $lines.Count; $index++) { + $line = [string]$lines[$index] + [void]$productionTextParts.Add($line) + if ($file.Extension -ieq ".rs") { + [void]$rustTextParts.Add($line) + } + foreach ($rule in $rules) { + if ($line -match $rule.pattern) { + [void]$violations.Add((New-Violation ` + -Rule $rule.name ` + -Path $relativePath ` + -Line ($index + 1) ` + -Message "Production code still contains a PowerShell runtime boundary.")) + } + } + } + } + + $productionText = $productionTextParts -join "`n" + $rustText = $rustTextParts -join "`n" + $observedNsisFlags = @( + [regex]::Matches($productionText, '--nsis-[a-z0-9-]+', [Text.RegularExpressions.RegexOptions]::IgnoreCase) | + ForEach-Object { $_.Value.ToLowerInvariant() } | + Sort-Object -Unique + ) + foreach ($flag in $ExpectedNsisFlags) { + if (-not $rustText.Contains($flag)) { + [void]$violations.Add((New-Violation ` + -Rule "missing-nsis-runtime-mode" ` + -Path "src-tauri/src" ` + -Message "Rust early-mode parser is missing fixed NSIS mode: $flag")) + } + } + foreach ($flag in $observedNsisFlags) { + if ($flag -notin $ExpectedNsisFlags) { + [void]$violations.Add((New-Violation ` + -Rule "unexpected-nsis-mode" ` + -Path "src-tauri" ` + -Message "Unexpected reserved NSIS early mode: $flag")) + } + } + + $hookPath = Join-Path $RepoRoot "src-tauri\bundled\installer-hooks\proxywarden-hooks.nsh" + $hookText = Get-Content -LiteralPath $hookPath -Raw + foreach ($flag in $ExpectedNsisFlags) { + if (-not $hookText.Contains($flag)) { + [void]$violations.Add((New-Violation ` + -Rule "missing-nsis-hook-mode" ` + -Path "src-tauri/bundled/installer-hooks/proxywarden-hooks.nsh" ` + -Message "Installer hook does not call fixed early mode: $flag")) + } + } + + $success = $violations.Count -eq 0 + $message = if ($success) { + "Runtime PowerShell boundary is clean." + } else { + "Runtime PowerShell boundary has $($violations.Count) violation(s)." + } + Write-Result ` + -Success $success ` + -Message $message ` + -Violations $violations.ToArray() ` + -PowerShellFileCount $powerShellFileCount ` + -ProductionFileCount $productionFileCount ` + -ObservedNsisFlags $observedNsisFlags + if (-not $success) { exit 1 } +} catch { + $failure = New-Violation ` + -Rule "checker-error" ` + -Path "scripts/check-runtime-powershell-boundary.ps1" ` + -Message $_.Exception.Message + Write-Result ` + -Success $false ` + -Message "Runtime PowerShell boundary check could not complete." ` + -Violations @($failure) ` + -PowerShellFileCount $powerShellFileCount ` + -ProductionFileCount $productionFileCount ` + -ObservedNsisFlags $observedNsisFlags + exit 1 +} diff --git a/scripts/install-control-app.ps1 b/scripts/install-control-app.ps1 deleted file mode 100644 index a49e460..0000000 --- a/scripts/install-control-app.ps1 +++ /dev/null @@ -1,79 +0,0 @@ -param( - [string]$InstallRoot = "C:\Program Files\ProxyWarden\ControlApp", - [string]$DataRoot = "C:\ProgramData\ProxyWarden", - [switch]$PlanOnly, - [switch]$Force -) - -$ErrorActionPreference = "Stop" - -function New-Result { - param( - [bool]$Success, - [string]$Action, - [bool]$Changed, - [string]$Message, - [hashtable]$Details = @{} - ) - - [ordered]@{ - success = $Success - action = $Action - changed = $Changed - message = $Message - details = $Details - } | ConvertTo-Json -Depth 6 -} - -function Test-IsAdministrator { - $identity = [Security.Principal.WindowsIdentity]::GetCurrent() - $principal = [Security.Principal.WindowsPrincipal]::new($identity) - $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) -} - -function Ensure-Directory { - param([string]$Path) - if (-not (Test-Path -LiteralPath $Path)) { - New-Item -ItemType Directory -Path $Path -Force | Out-Null - return $true - } - return $false -} - -try { - $details = @{ - installRoot = $InstallRoot - dataRoot = $DataRoot - planOnly = [bool]$PlanOnly - } - - if ($PlanOnly) { - New-Result -Success $true -Action "install-control-app" -Changed $false -Message "Control App install plan is ready." -Details $details - exit 0 - } - - if (-not (Test-IsAdministrator)) { - New-Result -Success $false -Action "install-control-app" -Changed $false -Message "Administrator rights are required." -Details $details - exit 1 - } - - $changed = $false - $changed = (Ensure-Directory -Path $InstallRoot) -or $changed - $changed = (Ensure-Directory -Path (Join-Path $DataRoot "config")) -or $changed - $changed = (Ensure-Directory -Path (Join-Path $DataRoot "state")) -or $changed - $changed = (Ensure-Directory -Path (Join-Path $DataRoot "generated")) -or $changed - - $markerPath = Join-Path $InstallRoot "install-control-app.marker.json" - if ((-not (Test-Path -LiteralPath $markerPath)) -or $Force) { - @{ component = "control-app"; installedAt = (Get-Date).ToString("o") } | - ConvertTo-Json -Depth 4 | - Set-Content -LiteralPath $markerPath -Encoding UTF8 - $changed = $true - } - - $details.markerPath = $markerPath - New-Result -Success $true -Action "install-control-app" -Changed $changed -Message "Control App directories are installed." -Details $details -} catch { - New-Result -Success $false -Action "install-control-app" -Changed $false -Message $_.Exception.Message - exit 1 -} diff --git a/scripts/install-proxyfier.ps1 b/scripts/install-proxyfier.ps1 deleted file mode 100644 index 74cdc63..0000000 --- a/scripts/install-proxyfier.ps1 +++ /dev/null @@ -1,96 +0,0 @@ -param( - [string]$InstallRoot = "C:\Program Files\ProxyWarden\components\ProxiFyre", - [string]$PackagePath = "", - [string]$ServiceName = "ProxiFyreService", - [switch]$PlanOnly, - [switch]$Force -) - -$ErrorActionPreference = "Stop" - -function New-Result { - param( - [bool]$Success, - [string]$Action, - [bool]$Changed, - [string]$Message, - [hashtable]$Details = @{} - ) - - [ordered]@{ - success = $Success - action = $Action - changed = $Changed - message = $Message - details = $Details - } | ConvertTo-Json -Depth 6 -} - -function Test-IsAdministrator { - $identity = [Security.Principal.WindowsIdentity]::GetCurrent() - $principal = [Security.Principal.WindowsPrincipal]::new($identity) - $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) -} - -function Backup-File { - param([string]$Path) - if (Test-Path -LiteralPath $Path) { - $backup = "$Path.bak" - Copy-Item -LiteralPath $Path -Destination $backup -Force - return $backup - } - return $null -} - -try { - $details = @{ - installRoot = $InstallRoot - packagePath = $PackagePath - serviceName = $ServiceName - planOnly = [bool]$PlanOnly - } - - if ($PlanOnly) { - New-Result -Success $true -Action "install-proxyfier" -Changed $false -Message "Proxyfier install plan is ready." -Details $details - exit 0 - } - - if (-not (Test-IsAdministrator)) { - New-Result -Success $false -Action "install-proxyfier" -Changed $false -Message "Administrator rights are required." -Details $details - exit 1 - } - - if ([string]::IsNullOrWhiteSpace($PackagePath) -or -not (Test-Path -LiteralPath $PackagePath)) { - New-Result -Success $false -Action "install-proxyfier" -Changed $false -Message "PackagePath is required and must point to a local ProxiFyre package." -Details $details - exit 2 - } - - $changed = $false - if (-not (Test-Path -LiteralPath $InstallRoot)) { - New-Item -ItemType Directory -Path $InstallRoot -Force | Out-Null - $changed = $true - } - - $configPath = Join-Path $InstallRoot "app-config.json" - $backupPath = Backup-File -Path $configPath - if ($backupPath) { - $details.backupPath = $backupPath - } - - $markerPath = Join-Path $InstallRoot "install-proxyfier.marker.json" - if ((-not (Test-Path -LiteralPath $markerPath)) -or $Force) { - @{ - component = "proxyfier" - packagePath = $PackagePath - serviceName = $ServiceName - installedAt = (Get-Date).ToString("o") - } | ConvertTo-Json -Depth 4 | Set-Content -LiteralPath $markerPath -Encoding UTF8 - $changed = $true - } - - $details.markerPath = $markerPath - New-Result -Success $true -Action "install-proxyfier" -Changed $changed -Message "Proxyfier install boundary completed." -Details $details -} catch { - New-Result -Success $false -Action "install-proxyfier" -Changed $false -Message $_.Exception.Message - exit 1 -} diff --git a/scripts/install-singbox.ps1 b/scripts/install-singbox.ps1 deleted file mode 100644 index f2e6cb3..0000000 --- a/scripts/install-singbox.ps1 +++ /dev/null @@ -1,270 +0,0 @@ -param( - [string]$InstallRoot = "C:\Program Files\ProxyWarden\components\sing-box", - [string]$ServiceName = "ProxyWardenSingBox", - [string]$ConfigSource = "C:\ProgramData\ProxyWarden\generated\sing-box-config.json", - [switch]$PlanOnly, - [switch]$Force, - [switch]$Uninstall -) - -$ErrorActionPreference = "Stop" - -$SingBoxReleaseApi = "https://api.github.com/repos/SagerNet/sing-box/releases/latest" -$WinSwReleaseApi = "https://api.github.com/repos/winsw/winsw/releases/latest" -$WrapperFile = "$ServiceName.exe" -$ConfigFile = "config.json" - -function New-Result { - param( - [bool]$Success, - [string]$Action, - [bool]$Changed, - [string]$Message, - [hashtable]$Details = @{} - ) - - [ordered]@{ - success = $Success - action = $Action - changed = $Changed - message = $Message - details = $Details - } | ConvertTo-Json -Depth 8 -} - -function Test-IsAdministrator { - $identity = [Security.Principal.WindowsIdentity]::GetCurrent() - $principal = [Security.Principal.WindowsPrincipal]::new($identity) - $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) -} - -function Get-NativeArchitecture { - $processor = Get-CimInstance Win32_Processor | Select-Object -First 1 - if ($null -ne $processor -and $processor.Architecture -eq 12) { return "arm64" } - if ([Environment]::Is64BitOperatingSystem) { return "amd64" } - return "386" -} - -function Get-WinSwArchitecture { - param([string]$Arch) - if ($Arch -eq "arm64") { return "arm64" } - if ($Arch -eq "386") { return "x86" } - return "x64" -} - -function Invoke-Download { - param([string]$Uri, [string]$Path) - Invoke-WebRequest -UseBasicParsing -Uri $Uri -OutFile $Path -Headers @{ "User-Agent" = "proxywarden" } -} - -function Select-Asset { - param( - [object[]]$Assets, - [string]$Pattern, - [string]$Label - ) - - $asset = $Assets | Where-Object { $_.name -match $Pattern } | Select-Object -First 1 - if ($null -eq $asset) { - throw "Не найден release asset для $Label по шаблону $Pattern." - } - return $asset -} - -function Test-SafeInstallRoot { - param([string]$Path) - $full = [System.IO.Path]::GetFullPath($Path).TrimEnd("\") - $leaf = Split-Path -Leaf $full - $parent = Split-Path -Parent $full - if ($leaf -ne "sing-box") { return $false } - return $parent -match "\\ProxyWarden\\components$|\\proxywarden\\components$|\\ProxyWarden$|\\proxywarden$" -} - -function Backup-File { - param([string]$Path) - if (Test-Path -LiteralPath $Path) { - $backup = "$Path.bak" - Copy-Item -LiteralPath $Path -Destination $backup -Force - return $backup - } - return $null -} - -function Write-Utf8NoBomFile { - param( - [string]$Path, - [string]$Value - ) - - $encoding = New-Object System.Text.UTF8Encoding $false - [System.IO.File]::WriteAllText($Path, $Value, $encoding) -} - -function Write-WinSwConfig { - param( - [string]$Root, - [string]$Name - ) - - $xmlPath = Join-Path $Root "$Name.xml" - $logDir = Join-Path $Root "logs" - New-Item -ItemType Directory -Path $logDir -Force | Out-Null - $xml = @" - - $Name - ProxyWarden Local sing-box - Local sing-box runtime managed by ProxyWarden. - %BASE%\sing-box.exe - run -c "%BASE%\config.json" - %BASE%\logs - - 10485760 - 4 - - - -"@ - Write-Utf8NoBomFile -Path $xmlPath -Value $xml - return $xmlPath -} - -function Stop-And-Uninstall-Service { - param( - [string]$Root, - [string]$Name - ) - - $wrapper = Join-Path $Root "$Name.exe" - $service = Get-Service -Name $Name -ErrorAction SilentlyContinue - if ($null -ne $service -and $service.Status -ne "Stopped") { - Stop-Service -Name $Name -Force -ErrorAction SilentlyContinue - $service = Get-Service -Name $Name -ErrorAction SilentlyContinue - if ($null -ne $service) { - try { $service.WaitForStatus("Stopped", [TimeSpan]::FromSeconds(15)) } catch {} - } - } - - if (Test-Path -LiteralPath $wrapper) { - Push-Location $Root - try { & $wrapper uninstall | Out-Null } finally { Pop-Location } - } - - $service = Get-Service -Name $Name -ErrorAction SilentlyContinue - if ($null -ne $service) { - sc.exe delete $Name | Out-Null - } -} - -try { - [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 - $installRootFull = [System.IO.Path]::GetFullPath($InstallRoot) - $details = @{ - installRoot = $installRootFull - serviceName = $ServiceName - configSource = $ConfigSource - singboxReleaseApi = $SingBoxReleaseApi - winswReleaseApi = $WinSwReleaseApi - planOnly = [bool]$PlanOnly - uninstall = [bool]$Uninstall - } - - if ($PlanOnly) { - $details.items = @( - @{ id = "sing-box-binary"; name = "sing-box.exe"; source = $SingBoxReleaseApi; target = (Join-Path $installRootFull "sing-box.exe") }, - @{ id = "winsw-wrapper"; name = $WrapperFile; source = $WinSwReleaseApi; target = (Join-Path $installRootFull $WrapperFile) }, - @{ id = "windows-service"; name = $ServiceName; target = "Windows Service" }, - @{ id = "config"; name = $ConfigFile; source = $ConfigSource; target = (Join-Path $installRootFull $ConfigFile) } - ) - New-Result -Success $true -Action "install-singbox.plan" -Changed $false -Message "Local sing-box install plan is ready." -Details $details - exit 0 - } - - if (-not (Test-IsAdministrator)) { - New-Result -Success $false -Action "install-singbox" -Changed $false -Message "Administrator rights are required." -Details $details - exit 1 - } - - if ($Uninstall) { - if (-not (Test-SafeInstallRoot -Path $installRootFull)) { - New-Result -Success $false -Action "uninstall-singbox" -Changed $false -Message "Unsafe InstallRoot for recursive uninstall." -Details $details - exit 2 - } - - Stop-And-Uninstall-Service -Root $installRootFull -Name $ServiceName - if (Test-Path -LiteralPath $installRootFull) { - Remove-Item -LiteralPath $installRootFull -Recurse -Force - } - New-Result -Success $true -Action "uninstall-singbox" -Changed $true -Message "Local sing-box service and install folder were removed." -Details $details - exit 0 - } - - $changed = $false - New-Item -ItemType Directory -Path $installRootFull -Force | Out-Null - $workDir = Join-Path ([System.IO.Path]::GetTempPath()) ("proxywarden-singbox-" + [guid]::NewGuid().ToString("N")) - $extractDir = Join-Path $workDir "extract" - New-Item -ItemType Directory -Path $extractDir -Force | Out-Null - - try { - $arch = Get-NativeArchitecture - $winswArch = Get-WinSwArchitecture -Arch $arch - $details.architecture = $arch - $details.winswArchitecture = $winswArch - - $singboxRelease = Invoke-RestMethod -Uri $SingBoxReleaseApi -Headers @{ "User-Agent" = "proxywarden" } - $singboxAsset = Select-Asset $singboxRelease.assets "windows-$arch\.zip$" "sing-box" - $singboxZip = Join-Path $workDir $singboxAsset.name - Invoke-Download $singboxAsset.browser_download_url $singboxZip - Expand-Archive -LiteralPath $singboxZip -DestinationPath $extractDir -Force - $singboxExe = Get-ChildItem -LiteralPath $extractDir -Recurse -Filter "sing-box.exe" | Select-Object -First 1 - if ($null -eq $singboxExe) { throw "В архиве sing-box не найден sing-box.exe." } - Copy-Item -LiteralPath $singboxExe.FullName -Destination (Join-Path $installRootFull "sing-box.exe") -Force - $changed = $true - - $winswRelease = Invoke-RestMethod -Uri $WinSwReleaseApi -Headers @{ "User-Agent" = "proxywarden" } - $winswAsset = Select-Asset $winswRelease.assets "WinSW-$winswArch\.exe$" "WinSW" - Invoke-Download $winswAsset.browser_download_url (Join-Path $installRootFull $WrapperFile) - $changed = $true - - $configTarget = Join-Path $installRootFull $ConfigFile - $backupPath = Backup-File -Path $configTarget - if ($backupPath) { $details.backupPath = $backupPath } - if (Test-Path -LiteralPath $ConfigSource) { - Copy-Item -LiteralPath $ConfigSource -Destination $configTarget -Force - } elseif (-not (Test-Path -LiteralPath $configTarget)) { - Write-Utf8NoBomFile -Path $configTarget -Value '{"log":{"level":"info","timestamp":true},"inbounds":[],"outbounds":[{"type":"direct","tag":"direct"}],"route":{"final":"direct"}}' - } - - $xmlPath = Write-WinSwConfig -Root $installRootFull -Name $ServiceName - $details.configPath = $configTarget - $details.wrapperConfigPath = $xmlPath - - if ($Force) { - Stop-And-Uninstall-Service -Root $installRootFull -Name $ServiceName - } - - Push-Location $installRootFull - try { - $service = Get-Service -Name $ServiceName -ErrorAction SilentlyContinue - if ($null -eq $service) { - & ".\$WrapperFile" install - if ($LASTEXITCODE -ne 0) { throw "WinSW install завершился с кодом $LASTEXITCODE." } - $changed = $true - } - & ".\$WrapperFile" start - if ($LASTEXITCODE -ne 0) { - Start-Service -Name $ServiceName -ErrorAction Stop - } - } finally { - Pop-Location - } - } finally { - if (Test-Path -LiteralPath $workDir) { - Remove-Item -LiteralPath $workDir -Recurse -Force -ErrorAction SilentlyContinue - } - } - - New-Result -Success $true -Action "install-singbox" -Changed $changed -Message "Local sing-box service is installed and started." -Details $details -} catch { - New-Result -Success $false -Action "install-singbox" -Changed $false -Message $_.Exception.Message - exit 1 -} diff --git a/scripts/prepare-release.check.mjs b/scripts/prepare-release.check.mjs new file mode 100644 index 0000000..9b75c8a --- /dev/null +++ b/scripts/prepare-release.check.mjs @@ -0,0 +1,262 @@ +import assert from "node:assert/strict"; +import { spawnSync } from "node:child_process"; +import { + mkdtempSync, + mkdirSync, + readFileSync, + writeFileSync, + rmSync, +} from "node:fs"; +import { tmpdir } from "node:os"; +import { dirname, join, resolve, sep } from "node:path"; +import { fileURLToPath } from "node:url"; +import { test } from "node:test"; + +const source = readFileSync( + join(dirname(fileURLToPath(import.meta.url)), "prepare-release.ps1"), + "utf8", +).replace(/^\ufeff/, ""); +const entry = source.lastIndexOf("try {\n Push-Location $RepoRoot"); +const crlfEntry = source.lastIndexOf("try {\r\n Push-Location $RepoRoot"); +const entryOffset = Math.max(entry, crlfEntry); +assert.ok(entryOffset > 0); + +function run(cwd, command, args, ok = true) { + const result = spawnSync(command, args, { + cwd, + encoding: "utf8", + timeout: 60000, + windowsHide: true, + }); + if (ok) + assert.equal( + result.status, + 0, + `${command}: ${result.stdout}\n${result.stderr}`, + ); + return result; +} + +function fixture(t, build = "") { + const root = mkdtempSync(join(tmpdir(), "proxywarden-release-test-")); + t.after(() => { + assert.ok(resolve(root).startsWith(resolve(tmpdir()) + sep)); + assert.ok(root.includes("proxywarden-release-test-")); + rmSync(root, { recursive: true, force: true }); + }); + const repo = join(root, "repo"); + mkdirSync(repo); + const write = (path, text) => { + mkdirSync(dirname(join(repo, path)), { recursive: true }); + writeFileSync(join(repo, path), text); + }; + const git = (...args) => run(repo, "git", args).stdout.trim(); + write("package.json", '{"name":"proxywarden","version":"1.2.0"}\n'); + write( + "package-lock.json", + '{"name":"proxywarden","version":"1.2.0","packages":{"":{"name":"proxywarden","version":"1.2.0"}}}\n', + ); + write("src-tauri/tauri.conf.json", '{"version":"1.2.0"}\n'); + write( + "src-tauri/Cargo.toml", + '[package]\nname = "proxywarden"\nversion = "1.2.0"\n', + ); + write( + "src-tauri/Cargo.lock", + '[[package]]\nname = "proxywarden"\nversion = "1.2.0"\n', + ); + write(".gitignore", "node_modules/\nsrc-tauri/target/\nreleases/\n"); + for (const cli of [ + "typescript/bin/tsc", + "vite/bin/vite.js", + "@tauri-apps/cli/tauri.js", + "prettier/bin/prettier.cjs", + "eslint/bin/eslint.js", + "vitest/vitest.mjs", + ]) + write(`node_modules/${cli}`, "fixture"); + // Replace only expensive checks/build in this isolated copy. Git/version/artifact/push code is real. + const stub = ` +function Invoke-ReleaseChecks {} +function Invoke-ReleaseBuild { + ${build} + $output = Join-Path $BundleRoot 'nsis' + New-Item -ItemType Directory -Path $output -Force | Out-Null + [IO.File]::WriteAllText((Join-Path $output "ProxyWarden_$($targetVersion)_x64-setup.exe"), 'test artifact') +} +`; + write( + "scripts/prepare-release.ps1", + "\ufeff" + source.slice(0, entryOffset) + stub + source.slice(entryOffset), + ); + git("init", "-b", "master"); + git("config", "user.name", "Release Test"); + git("config", "user.email", "release-test@example.invalid"); + git("config", "core.autocrlf", "false"); + git("add", "."); + git("commit", "-m", "initial"); + const remote = join(root, "origin.git"); + run(root, "git", ["init", "--bare", remote]); + git("remote", "add", "origin", remote); + git("push", "-u", "origin", "master"); + const release = (...args) => + run( + repo, + "pwsh", + [ + "-NoProfile", + "-File", + "scripts/prepare-release.ps1", + "-Publish", + ...args, + ], + false, + ); + const manifest = () => + JSON.parse( + readFileSync( + join(repo, "releases/proxywarden-v1.2.1/release-manifest.json"), + "utf8", + ).replace(/^\ufeff/, ""), + ); + return { root, repo, remote, git, write, release, manifest }; +} + +test("PlanOnly is offline and leaves versions/index/refs unchanged", (t) => { + const f = fixture(t); + f.git("remote", "set-url", "origin", join(f.root, "absent.git")); + const before = f.git("status", "--porcelain"); + const head = f.git("rev-parse", "HEAD"); + const result = f.release("-PlanOnly"); + assert.equal(result.status, 0, result.stderr); + const plan = JSON.parse(result.stdout); + assert.equal(plan.changed, false); + assert.equal(plan.details.targetVersion, "1.2.1"); + assert.equal(f.git("status", "--porcelain"), before); + assert.equal(f.git("rev-parse", "HEAD"), head); +}); + +test("release commits exact dirty source, versions both locks, tags and atomically pushes", (t) => { + const f = fixture(t); + f.write("feature.txt", "new feature"); + const result = f.release("-Version", "1.2.1"); + assert.equal(result.status, 0, result.stdout + result.stderr); + const head = f.git("rev-parse", "HEAD"); + assert.equal(f.git("rev-parse", "v1.2.1^{commit}"), head); + assert.equal( + f.git("ls-remote", "origin", "refs/heads/master").split(/\s/)[0], + head, + ); + assert.equal(f.git("status", "--porcelain"), ""); + assert.equal(f.manifest().gitCommit, head); + assert.equal(f.manifest().gitRelease.status, "pushed"); + assert.equal(f.manifest().artifacts.length, 1); + assert.match( + readFileSync(join(f.repo, "src-tauri/Cargo.lock"), "utf8"), + /version = "1.2.1"/, + ); + const repeat = f.release("-Version", "1.2.1"); + assert.notEqual(repeat.status, 0); + assert.equal(f.git("rev-parse", "HEAD"), head); +}); + +test("failed build creates no commit/tag/push and preserves existing staging", (t) => { + const f = fixture(t, "throw 'Synthetic build failure'"); + f.write("staged.txt", "staged"); + f.git("add", "staged.txt"); + const index = f.git("write-tree"), + head = f.git("rev-parse", "HEAD"); + assert.notEqual(f.release("-Version", "1.2.1").status, 0); + assert.equal(f.git("write-tree"), index); + assert.equal(f.git("rev-parse", "HEAD"), head); + assert.equal(f.git("tag", "--list"), ""); +}); + +test("source edit during build refuses to tag an artifact from another tree", (t) => { + const f = fixture( + t, + "[IO.File]::WriteAllText((Join-Path $RepoRoot 'concurrent.txt'), 'changed during build')", + ); + const head = f.git("rev-parse", "HEAD"); + const result = f.release("-Version", "1.2.1"); + assert.notEqual(result.status, 0); + assert.match(result.stderr, /changed during the build/); + assert.match(result.stderr, /concurrent\.txt/); + assert.equal(f.git("rev-parse", "HEAD"), head); + assert.equal(f.git("tag", "--list"), ""); +}); + +test("failed atomic push keeps artifacts and resumes without rebuilding", (t) => { + const f = fixture(t); + const hook = join(f.remote, "hooks/pre-receive"); + writeFileSync(hook, "#!/bin/sh\nexit 1\n"); + const before = f.git("ls-remote", "origin", "refs/heads/master"); + const failed = f.release("-Version", "1.2.1"); + assert.notEqual(failed.status, 0); + assert.equal(f.manifest().gitRelease.status, "pending-push"); + assert.equal(f.git("ls-remote", "origin", "refs/heads/master"), before); + assert.equal(f.git("ls-remote", "origin", "refs/tags/v1.2.1"), ""); + const commit = f.git("rev-parse", "HEAD"); + rmSync(hook); + const resumed = f.release("-Version", "1.2.1", "-Resume"); + assert.equal(resumed.status, 0, resumed.stdout + resumed.stderr); + assert.equal(f.git("rev-parse", "HEAD"), commit); + assert.equal(f.manifest().gitRelease.status, "pushed"); + f.write( + "releases/proxywarden-v1.2.1/artifacts/nsis/ProxyWarden_1.2.1_x64-setup.exe", + "tampered", + ); + assert.notEqual(f.release("-Version", "1.2.1", "-Resume").status, 0); +}); + +test("remote-only version tag and diverged branch are refused before version edits", (t) => { + const f = fixture(t); + f.git("tag", "v1.2.1"); + f.git("push", "origin", "refs/tags/v1.2.1"); + f.git("tag", "-d", "v1.2.1"); + const version = readFileSync(join(f.repo, "package.json"), "utf8"); + assert.notEqual(f.release("-Version", "1.2.1").status, 0); + assert.equal(readFileSync(join(f.repo, "package.json"), "utf8"), version); + const clone = join(f.root, "other"); + run(f.root, "git", ["clone", "--branch", "master", f.remote, clone]); + run(clone, "git", ["config", "user.name", "Other"]); + run(clone, "git", ["config", "user.email", "other@example.invalid"]); + writeFileSync(join(clone, "remote-change.txt"), "remote"); + run(clone, "git", ["add", "."]); + run(clone, "git", ["commit", "-m", "remote change"]); + run(clone, "git", ["push"]); + const result = f.release("-Version", "1.2.2"); + assert.notEqual(result.status, 0); + assert.match(result.stderr, /Integrate them before releasing/); + assert.equal(readFileSync(join(f.repo, "package.json"), "utf8"), version); +}); + +test("invalid Windows versions and mismatched Cargo.lock fail without mutations", (t) => { + const f = fixture(t); + for (const version of ["01.2.3", "1.2.65536", "1.2.3-rc.1"]) + assert.notEqual(f.release("-Version", version, "-PlanOnly").status, 0); + f.write( + "src-tauri/Cargo.lock", + '[[package]]\nname = "proxywarden"\nversion = "0.0.0"\n', + ); + const result = f.release("-PlanOnly"); + assert.notEqual(result.status, 0); + assert.match(result.stderr, /Version mismatch/); +}); + +test("unreachable origin reports the Git cause and diagnostic command without a PowerShell stack", (t) => { + const f = fixture(t); + f.git("remote", "set-url", "origin", join(f.root, "absent.git")); + const head = f.git("rev-parse", "HEAD"); + const version = readFileSync(join(f.repo, "package.json"), "utf8"); + const result = f.release("-Version", "1.2.1"); + assert.notEqual(result.status, 0); + assert.match(result.stderr, /git ls-remote origin/); + assert.match(result.stderr, /does not appear to be a git repository/); + assert.doesNotMatch( + result.stderr, + /prepare-release\.ps1:\d|ScriptStackTrace|Line \|/, + ); + assert.equal(f.git("rev-parse", "HEAD"), head); + assert.equal(readFileSync(join(f.repo, "package.json"), "utf8"), version); +}); diff --git a/scripts/prepare-release.ps1 b/scripts/prepare-release.ps1 index 0758c3b..7971f13 100644 --- a/scripts/prepare-release.ps1 +++ b/scripts/prepare-release.ps1 @@ -1,4 +1,4 @@ -param( +param( [string]$Version = "", [ValidateSet("", "patch", "minor", "major")] [string]$Bump = "", @@ -6,9 +6,12 @@ param( [switch]$SkipTests, [switch]$SkipBuild, [switch]$PlanOnly, + [switch]$Publish, + [switch]$Resume, [switch]$Force ) +Set-StrictMode -Version Latest $ErrorActionPreference = "Stop" $RepoRoot = [System.IO.Path]::GetFullPath((Join-Path $PSScriptRoot "..")) @@ -16,7 +19,11 @@ $PackageJsonPath = Join-Path $RepoRoot "package.json" $PackageLockPath = Join-Path $RepoRoot "package-lock.json" $TauriConfigPath = Join-Path $RepoRoot "src-tauri\tauri.conf.json" $CargoTomlPath = Join-Path $RepoRoot "src-tauri\Cargo.toml" +$CargoLockPath = Join-Path $RepoRoot "src-tauri\Cargo.lock" $BundleRoot = Join-Path $RepoRoot "src-tauri\target\release\bundle" +$RuntimeBoundaryCheckPath = Join-Path $RepoRoot "scripts\check-runtime-powershell-boundary.ps1" +$ComponentBundleScriptPath = Join-Path $RepoRoot "scripts\update-component-bundle.ps1" +$WindowsAuditScriptPath = Join-Path $RepoRoot "scripts\audit-windows-smoke.ps1" function Write-Utf8NoBomFile { param( @@ -143,9 +150,12 @@ function Set-PackageLockVersions { function Assert-Semver { param([string]$Value) - if ($Value -notmatch "^\d+\.\d+\.\d+$") { + if ($Value -notmatch '^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$') { throw "Version '$Value' is not supported. Use numeric SemVer like 0.1.0." } + foreach ($part in $Value.Split('.')) { + if ([long]$part -gt 65535) { throw "Version components must be between 0 and 65535 for Windows." } + } } function ConvertTo-VersionParts { @@ -206,6 +216,9 @@ function Get-CargoPackageVersion { function Get-VersionState { $packageLock = Get-PackageLockVersions + $cargoLock = Get-Content -Raw -LiteralPath $CargoLockPath + $cargoMatch = [regex]::Match($cargoLock, '(?m)^name = "proxywarden"\r?\nversion = "([^"]+)"') + if (-not $cargoMatch.Success) { throw 'Cannot find ProxyWarden in Cargo.lock.' } [ordered]@{ packageJson = [string](Get-FirstJsonVersion -Path $PackageJsonPath -Label "package.json") @@ -213,6 +226,7 @@ function Get-VersionState { packageLockRoot = [string]$packageLock.packageLockRoot tauriConfig = [string](Get-FirstJsonVersion -Path $TauriConfigPath -Label "tauri.conf.json") cargoToml = [string](Get-CargoPackageVersion) + cargoLock = $cargoMatch.Groups[1].Value } } @@ -223,7 +237,8 @@ function Get-CurrentVersion { $state.packageLock, $state.packageLockRoot, $state.tauriConfig, - $state.cargoToml + $state.cargoToml, + $state.cargoLock ) | Select-Object -Unique) if ($versions.Count -ne 1) { @@ -247,23 +262,24 @@ function Resolve-TargetVersion { return Get-NextVersion -Current $Current -Kind $Bump } - if ($PlanOnly -or -not [Environment]::UserInteractive) { + if ($PlanOnly) { return Get-NextVersion -Current $Current -Kind "patch" } + if (-not [Environment]::UserInteractive) { throw "Specify -Version or -Bump in non-interactive mode." } $patch = Get-NextVersion -Current $Current -Kind "patch" $minor = Get-NextVersion -Current $Current -Kind "minor" $major = Get-NextVersion -Current $Current -Kind "major" Write-Host "" - Write-Host "Current version: $Current" - Write-Host "Choose release version:" + Write-Host "Текущая версия: $Current" + Write-Host "Выбери номер или введи версию, например $patch :" Write-Host " 1) patch $patch" Write-Host " 2) minor $minor" Write-Host " 3) major $major" - Write-Host " 4) custom" - Write-Host " 5) keep current $Current" - $choice = Read-Host "Selection [1]" + Write-Host " 4) другая версия" + Write-Host " 5) текущая $Current (если ещё не выпущена)" + $choice = Read-Host "Версия [1]" if ([string]::IsNullOrWhiteSpace($choice)) { $choice = "1" } @@ -277,7 +293,7 @@ function Resolve-TargetVersion { return $custom } "5" { return $Current } - default { throw "Unknown selection '$choice'." } + default { Assert-Semver -Value $choice.Trim(); return $choice.Trim() } } } @@ -313,6 +329,9 @@ function Set-ManifestVersions { Set-PackageLockVersions -TargetVersion $TargetVersion Set-FirstJsonVersion -Path $TauriConfigPath -TargetVersion $TargetVersion -Label "tauri.conf.json" Set-CargoPackageVersion -TargetVersion $TargetVersion + $lock = Get-Content -Raw -LiteralPath $CargoLockPath + $lock = Replace-RegexGroup -Content $lock -Pattern '(?m)^name = "proxywarden"\r?\nversion = "(?[^"]+)"' -GroupName "value" -Value $TargetVersion -Label "ProxyWarden version in Cargo.lock" + Write-Utf8NoBomFile -Path $CargoLockPath -Value $lock } function Get-FullPath { @@ -354,6 +373,7 @@ function New-ReleaseDirectory { $releaseDir = Join-Path $root "proxywarden-v$TargetVersion" if (Test-Path -LiteralPath $releaseDir) { + if ($Publish -or -not $Force) { throw "Release directory already exists: $releaseDir. Use -Resume for a failed push, or choose another version." } if (-not (Test-IsSubPath -Parent $root -Child $releaseDir)) { throw "Refusing to remove release directory outside OutputRoot: $releaseDir" } @@ -412,17 +432,51 @@ function Invoke-ReleaseBuild { return } - Invoke-NativeCommand -Name "Frontend build" -FilePath "npm" -Arguments @("run", "build") + Invoke-NativeCommand -Name "Frontend types" -FilePath "node" -Arguments @("node_modules/typescript/bin/tsc", "--noEmit") if (-not $SkipTests) { - Invoke-NativeCommand -Name "Rust tests" -FilePath "cargo" -Arguments @("test") -WorkingDirectory (Join-Path $RepoRoot "src-tauri") + Invoke-NativeCommand -Name "Frontend formatting" -FilePath "node" -Arguments @("node_modules/prettier/bin/prettier.cjs", "--check", "src/**/*.{ts,tsx,css}") + Invoke-NativeCommand -Name "Frontend lint" -FilePath "node" -Arguments @("node_modules/eslint/bin/eslint.js", "src") + Invoke-NativeCommand -Name "Frontend tests" -FilePath "node" -Arguments @("node_modules/vitest/vitest.mjs", "run") + Invoke-NativeCommand -Name "Rust formatting" -FilePath "cargo" -Arguments @("fmt", "--all", "--", "--check") -WorkingDirectory (Join-Path $RepoRoot "src-tauri") + Invoke-NativeCommand -Name "Rust lint" -FilePath "cargo" -Arguments @("clippy", "--locked", "--all-targets", "--all-features", "--", "-D", "warnings") -WorkingDirectory (Join-Path $RepoRoot "src-tauri") + Invoke-NativeCommand -Name "Rust tests" -FilePath "cargo" -Arguments @("test", "--locked", "--all-targets") -WorkingDirectory (Join-Path $RepoRoot "src-tauri") } else { Write-Host "" Write-Host "Skipping Rust tests because -SkipTests was provided." } + Invoke-NativeCommand -Name "Frontend build" -FilePath "node" -Arguments @("node_modules/vite/bin/vite.js", "build") Clear-ReleaseBundleOutput - Invoke-NativeCommand -Name "Tauri release build" -FilePath "npm" -Arguments @("run", "tauri", "--", "build") + # Use a temporary config file: JSON command-line quoting differs between Windows PowerShell and pwsh. + $config = Join-Path ([IO.Path]::GetTempPath()) ("proxywarden-build-" + [guid]::NewGuid().ToString('N') + '.json') + try { + Write-Utf8NoBomFile -Path $config -Value '{"build":{"beforeBuildCommand":""}}' + Invoke-NativeCommand -Name "Tauri release build" -FilePath "node" -Arguments @("node_modules/@tauri-apps/cli/tauri.js", "build", "--config", $config, "--bundles", "nsis") + } finally { if (Test-Path -LiteralPath $config) { Remove-Item -LiteralPath $config } } +} + +function Invoke-ScriptCheck { + param( + [string]$Name, + [string]$ScriptPath, + [hashtable]$Parameters + ) + + Write-Host "" + Write-Host "==> $Name" + $output = & $ScriptPath @Parameters + $succeeded = $? + $output | Write-Output + if (-not $succeeded) { + throw "$Name failed." + } +} + +function Invoke-ReleaseChecks { + Invoke-ScriptCheck -Name "Runtime PowerShell boundary" -ScriptPath $RuntimeBoundaryCheckPath -Parameters @{ CheckOnly = $true } + Invoke-ScriptCheck -Name "Offline component bundle" -ScriptPath $ComponentBundleScriptPath -Parameters @{ CheckOnly = $true } + Invoke-ScriptCheck -Name "Windows smoke evidence plan" -ScriptPath $WindowsAuditScriptPath -Parameters @{ Mode = "PlanOnly" } } function Get-ArtifactVersionPattern { @@ -503,7 +557,7 @@ function Get-GitValue { param([string[]]$Arguments) try { - $value = & git @Arguments 2>$null + $value = & git --no-optional-locks @Arguments 2>$null if ($LASTEXITCODE -eq 0) { return ($value -join [Environment]::NewLine).Trim() } @@ -511,11 +565,176 @@ function Get-GitValue { return "" } +function Get-GitFailureMessage { + param([string]$Operation, [int]$ExitCode, [string]$Diagnostic) + $reason = if ($Diagnostic -match 'Too many authentication failures') { + 'SSH-сервер отклонил слишком много попыток входа. Укажи правильный ключ и IdentitiesOnly yes для этого Git-сервера.' + } elseif ($Diagnostic -match 'Permission denied \(publickey|Authentication failed|could not read Username|terminal prompts disabled') { + 'Сервер Git отклонил вход. Проверь SSH-ключ или HTTPS-аутентификацию и доступ к репозиторию.' + } elseif ($Diagnostic -match 'Host key verification failed|REMOTE HOST IDENTIFICATION HAS CHANGED') { + 'Не подтверждён SSH-ключ сервера. Проверь его отпечаток перед повторным подключением.' + } elseif ($Diagnostic -match 'Could not resolve|Connection timed out|Connection refused|Network is unreachable|connect to host.*Permission denied|Failed to connect') { + 'Не удалось подключиться к Git-серверу. Проверь сеть/VPN, адрес и порт origin.' + } elseif ($Diagnostic -match 'not found|does not appear to be a git repository') { + 'Репозиторий недоступен по адресу origin. Проверь URL и права доступа.' + } else { + "Git не выполнил операцию $Operation (код $ExitCode)." + } + $details = "$Diagnostic".Trim() -replace '(https?://)[^/\s@]+@', '$1[redacted]@' -replace '(https?://[^\s?#]+)[?#][^\s]*', '$1' + if ($details.Length -gt 2500) { $details = $details.Substring(0, 2500) + '...' } + $next = if ($Operation -in @('ls-remote', 'fetch')) { + 'Проверка origin завершилась до изменения версии, сборки, commit, tag и push. Для диагностики запусти: git ls-remote origin' + } else { 'Подробности ответа Git приведены ниже.' } + return "$reason`n$next`n`nОтвет Git:`n$details" +} + +function Invoke-Git { + param([string[]]$Arguments) + $stderrPath = Join-Path ([IO.Path]::GetTempPath()) ("proxywarden-git-" + [guid]::NewGuid().ToString('N') + '.log') + $previousPreference = $ErrorActionPreference + try { + # Windows PowerShell wraps redirected stderr as NativeCommandError; preserve it, + # then classify by the actual exit code instead of losing the original cause. + $ErrorActionPreference = 'Continue' + $output = & git @Arguments 2>$stderrPath + $exitCode = $LASTEXITCODE + $ErrorActionPreference = $previousPreference + [string]$diagnostic = '' + if (Test-Path -LiteralPath $stderrPath) { $diagnostic = [string](Get-Content -Raw -LiteralPath $stderrPath) } + if ($exitCode -ne 0) { throw (Get-GitFailureMessage -Operation $Arguments[0] -ExitCode $exitCode -Diagnostic $diagnostic) } + if (-not [string]::IsNullOrWhiteSpace($diagnostic)) { Write-Host $diagnostic.Trim() } + return ([string]($output -join "`n")).Trim() + } finally { + $ErrorActionPreference = $previousPreference + if (Test-Path -LiteralPath $stderrPath) { Remove-Item -LiteralPath $stderrPath } + } +} + +function Get-SourceTree { + # Snapshot tracked + non-ignored new files without touching the user's staging area. + $previousIndex = $env:GIT_INDEX_FILE + $index = Join-Path ([IO.Path]::GetTempPath()) ("proxywarden-index-" + [guid]::NewGuid().ToString('N')) + try { + $env:GIT_INDEX_FILE = $index + Invoke-Git @('read-tree', 'HEAD') | Out-Null + Invoke-Git @('add', '-A', '--', '.') | Out-Null + return Invoke-Git @('write-tree') + } finally { + $env:GIT_INDEX_FILE = $previousIndex + foreach ($path in @($index, "$index.lock")) { + if (Test-Path -LiteralPath $path) { Remove-Item -LiteralPath $path } + } + } +} + +function Get-ReleasePath { + param([string]$TargetVersion) + $root = if ([IO.Path]::IsPathRooted($OutputRoot)) { $OutputRoot } else { Join-Path $RepoRoot $OutputRoot } + return [IO.Path]::GetFullPath((Join-Path $root "proxywarden-v$TargetVersion")) +} + +function Test-GitTag { + param([string]$Tag) + & git show-ref --verify --quiet "refs/tags/$Tag" + if ($LASTEXITCODE -eq 0) { return $true } + if ($LASTEXITCODE -ne 1) { throw "Cannot inspect local tag $Tag." } + return $false +} + +function Get-ReleaseGitContext { + param([string]$TargetVersion) + $branch = Invoke-Git @('symbolic-ref', '--quiet', '--short', 'HEAD') + $headCommit = Invoke-Git @('rev-parse', 'HEAD') + foreach ($marker in @('MERGE_HEAD', 'CHERRY_PICK_HEAD', 'REVERT_HEAD', 'rebase-merge', 'rebase-apply')) { + $path = Invoke-Git @('rev-parse', '--git-path', $marker) + if (Test-Path -LiteralPath $path) { throw "Finish the active Git operation before releasing ($marker)." } + } + if (Invoke-Git @('diff', '--name-only', '--diff-filter=U')) { throw 'Resolve Git conflicts before releasing.' } + Invoke-Git @('var', 'GIT_AUTHOR_IDENT') | Out-Null + Invoke-Git @('var', 'GIT_COMMITTER_IDENT') | Out-Null + $remote = Invoke-Git @('remote', 'get-url', '--push', 'origin') + $tag = "v$TargetVersion" + if (-not $Resume -and (Test-GitTag $tag)) { throw "Tag $tag already exists. Use -Version $TargetVersion -Resume for a failed push, or choose another version." } + $remoteTag = Invoke-Git @('ls-remote', '--tags', 'origin', "refs/tags/$tag", "refs/tags/$tag^{}") + if (-not $Resume -and $remoteTag) { throw "Remote tag $tag already exists. Choose another version." } + $remoteBranch = Invoke-Git @('ls-remote', '--heads', 'origin', "refs/heads/$branch") + if ($remoteBranch) { + Invoke-Git @('fetch', '--no-tags', 'origin', "refs/heads/$branch") | Out-Null + & git merge-base --is-ancestor FETCH_HEAD HEAD + if ($LASTEXITCODE -ne 0) { throw "The origin/$branch branch has changes not in HEAD. Integrate them before releasing; automatic merge is not performed." } + } + return @{ branch = $branch; head = $headCommit; remote = $remote; tag = $tag } +} + +function Complete-ReleaseGit { + param([hashtable]$Context, [string]$SourceTree, [string]$TargetVersion) + $currentTree = Get-SourceTree + if ((Invoke-Git @('rev-parse', 'HEAD')) -ne $Context.head -or + (Invoke-Git @('symbolic-ref', '--quiet', '--short', 'HEAD')) -ne $Context.branch -or + $currentTree -ne $SourceTree) { + $changed = Invoke-Git @('-c', 'core.quotepath=false', 'diff', '--name-only', $SourceTree, $currentTree) + throw "Source files or HEAD changed during the build. No release commit/tag was created.`nИсходники изменились во время сборки. Повтори сборку после завершения правок.`nИзменённые файлы:`n$changed" + } + if ((Invoke-Git @('rev-parse', 'HEAD^{tree}')) -ne $SourceTree) { + Invoke-Git @('add', '-A', '--', '.') | Out-Null + if ((Invoke-Git @('write-tree')) -ne $SourceTree) { throw 'Staged source changed. Rebuild before releasing.' } + Invoke-Git @('commit', '-m', "Release v$TargetVersion") | Write-Host + } + if ((Invoke-Git @('rev-parse', 'HEAD^{tree}')) -ne $SourceTree -or + (Get-SourceTree) -ne $SourceTree) { throw 'A Git hook changed source files. Rebuild before tagging.' } + return Invoke-Git @('rev-parse', 'HEAD') +} + +function Push-Release { + param([hashtable]$Context, [string]$Commit) + if ((Invoke-Git @('rev-parse', 'HEAD')) -ne $Commit -or + (Invoke-Git @('symbolic-ref', '--quiet', '--short', 'HEAD')) -ne $Context.branch -or + (Invoke-Git @('remote', 'get-url', '--push', 'origin')) -ne $Context.remote) { + throw 'HEAD, branch or origin changed before push.' + } + if (Test-GitTag $Context.tag) { + if ((Invoke-Git @('rev-parse', "$($Context.tag)^{commit}")) -ne $Commit) { throw 'Existing tag points to another commit.' } + } else { + Invoke-Git @('tag', '-a', $Context.tag, $Commit, '-m', "ProxyWarden $($Context.tag)") | Out-Null + } + # One atomic push; never force or push unrelated tags. A failure leaves a resumable local release. + Invoke-Git @('push', '--atomic', 'origin', "${Commit}:refs/heads/$($Context.branch)", "refs/tags/$($Context.tag):refs/tags/$($Context.tag)") | Write-Host +} + +function Resume-Release { + param([string]$TargetVersion, [hashtable]$Context) + $releaseDir = Get-ReleasePath $TargetVersion + $manifestPath = Join-Path $releaseDir 'release-manifest.json' + $manifest = Read-JsonFile $manifestPath + if (-not $manifest.PSObject.Properties['gitRelease'] -or -not $manifest.gitRelease) { + throw 'This folder has no completed release commit. Resume only retries a failed push; choose a new version and rebuild.' + } + if ($manifest.version -ne $TargetVersion -or $manifest.gitRelease.branch -ne $Context.branch -or + $manifest.gitRelease.remote -ne $Context.remote -or $manifest.gitRelease.tag -ne $Context.tag -or + $manifest.gitRelease.status -notin @('pending-push', 'pushed') -or + $manifest.gitCommit -ne $Context.head -or + (Invoke-Git @('rev-parse', 'HEAD^{tree}')) -ne $manifest.gitRelease.sourceTree -or + (Get-SourceTree) -ne $manifest.gitRelease.sourceTree) { + throw 'This release no longer matches HEAD/source/origin. Resume refused; use a new version.' + } + if (@($manifest.artifacts).Count -eq 0) { throw 'No artifacts to resume.' } + foreach ($artifact in $manifest.artifacts) { + $path = [IO.Path]::GetFullPath((Join-Path $releaseDir $artifact.path)) + if (-not (Test-IsSubPath $releaseDir $path) -or + (Get-FileHash -LiteralPath $path -Algorithm SHA256).Hash -ne $artifact.sha256) { throw 'Release artifact checksum mismatch.' } + } + Push-Release -Context $Context -Commit $manifest.gitCommit + $manifest.gitRelease.status = 'pushed' + Write-JsonFile -Path $manifestPath -Value $manifest + Write-Host "Релиз отправлен. Файлы для сайта: $releaseDir" +} + function Write-ReleaseMetadata { param( [string]$ReleaseDir, [string]$TargetVersion, - [object[]]$Artifacts + [object[]]$Artifacts, + [object]$GitRelease = $null ) $artifactDir = Join-Path $ReleaseDir "artifacts" @@ -534,6 +753,8 @@ function Write-ReleaseMetadata { source = "local" gitCommit = Get-GitValue -Arguments @("rev-parse", "HEAD") gitStatus = Get-GitValue -Arguments @("status", "--short") + gitRelease = $GitRelease + windowsAcceptance = "not-verified-by-this-command" artifacts = @($artifactItems) } @@ -560,7 +781,7 @@ See `SHA256SUMS.txt`. ## Release boundary -This release contains the ProxyWarden Control App only. ProxiFyre and Local sing-box remain explicit user-managed components. +The ProxyWarden installer contains pinned offline payloads for ProxiFyre, Windows Packet Filter, VC++ Runtime, sing-box, WinSW, and WebView2. Installing, updating, starting, stopping, or removing routing components remains an explicit user action. "@ @@ -590,16 +811,31 @@ function New-PlanResult { releaseDirectory = (Join-Path $outputRootFull "proxywarden-v$Target") skipTests = [bool]$SkipTests skipBuild = [bool]$SkipBuild + publish = [bool]$Publish + resume = [bool]$Resume + git = [ordered]@{ + branch = Get-GitValue @('symbolic-ref', '--quiet', '--short', 'HEAD') + remote = 'origin' + tag = "v$Target" + includedChanges = Get-GitValue @('status', '--short') + commitAfterSuccessfulBuild = [bool]$Publish + atomicPush = [bool]$Publish + } manifests = @( $PackageJsonPath, $PackageLockPath, $TauriConfigPath, - $CargoTomlPath + $CargoTomlPath, + $CargoLockPath ) commands = @( - "npm run build", - "cd src-tauri; cargo test", - "npm run tauri -- build" + ".\scripts\check-runtime-powershell-boundary.ps1 -CheckOnly", + ".\scripts\update-component-bundle.ps1 -CheckOnly", + ".\scripts\audit-windows-smoke.ps1 -Mode PlanOnly", + "node: TypeScript, Prettier, ESLint, Vitest, Vite", + "cargo fmt / clippy --locked / test --locked --all-targets", + "node: Tauri build --bundles nsis", + "if -Publish: commit source, annotated version tag, atomic branch+tag push to origin" ) } } | ConvertTo-Json -Depth 8 @@ -608,6 +844,15 @@ function New-PlanResult { try { Push-Location $RepoRoot + if ($Resume -and (-not $Publish -or -not $Version -or $Bump)) { throw '-Resume requires -Publish -Version X.Y.Z.' } + if ($Version -and $Bump) { throw 'Use either -Version or -Bump.' } + if ($Publish -and -not $PlanOnly -and ($SkipTests -or $SkipBuild -or $Force)) { throw 'A published release requires checks and a fresh build; SkipTests, SkipBuild and Force are not allowed.' } + if ($Publish -and -not $PlanOnly -and -not $Resume) { + Write-Host 'В релиз войдут все изменения Git ниже (кроме игнорируемых файлов).' + Write-Host 'После успешной сборки: commit, тег версии и push текущей ветки в origin.' + Write-Host 'Файлы установщика останутся локально для загрузки на сайт.' + Write-Host (Invoke-Git @('status', '--short')) + } $currentVersion = Get-CurrentVersion $targetVersion = Resolve-TargetVersion -Current $currentVersion Assert-Semver -Value $targetVersion @@ -625,12 +870,34 @@ try { Write-Host "Preparing ProxyWarden release $targetVersion..." Write-Host "Repository: $RepoRoot" + $gitContext = $null + if ($Publish) { + $gitContext = Get-ReleaseGitContext $targetVersion + if ($Resume) { Resume-Release -TargetVersion $targetVersion -Context $gitContext; return } + } + $releasePath = Get-ReleasePath $targetVersion + if ((Test-Path -LiteralPath $releasePath) -and ($Publish -or -not $Force)) { + throw "Release directory already exists: $releasePath. Use -Resume for a failed push, or choose another version." + } + if ($Publish -and (Test-IsSubPath $RepoRoot $releasePath)) { + & git check-ignore --quiet -- (Join-Path $releasePath 'release-manifest.json') + if ($LASTEXITCODE -ne 0) { throw 'OutputRoot must be ignored by Git, or outside the repository.' } + } + if (-not $SkipBuild) { + Get-Command node, cargo -ErrorAction Stop | Out-Null + foreach ($cli in @('typescript/bin/tsc', 'vite/bin/vite.js', '@tauri-apps/cli/tauri.js', 'prettier/bin/prettier.cjs', 'eslint/bin/eslint.js', 'vitest/vitest.mjs')) { + if (-not (Test-Path -LiteralPath (Join-Path $RepoRoot "node_modules/$cli"))) { throw 'Frontend dependencies are missing. Run npm ci once, then retry release.' } + } + } + Invoke-ReleaseChecks + Set-ManifestVersions -TargetVersion $targetVersion $afterUpdateVersion = Get-CurrentVersion if ($afterUpdateVersion -ne $targetVersion) { throw "Version update failed. Current version is $afterUpdateVersion." } + $sourceTree = if ($Publish) { Get-SourceTree } else { $null } Invoke-ReleaseBuild $releaseDir = New-ReleaseDirectory -TargetVersion $targetVersion @@ -638,11 +905,24 @@ try { Write-Checksums -ReleaseDir $releaseDir -Files $artifacts | Out-Null Write-ReleaseMetadata -ReleaseDir $releaseDir -TargetVersion $targetVersion -Artifacts $artifacts + if ($Publish) { + $commit = Complete-ReleaseGit -Context $gitContext -SourceTree $sourceTree -TargetVersion $targetVersion + $gitRelease = [ordered]@{ branch = $gitContext.branch; remote = $gitContext.remote; tag = $gitContext.tag; sourceTree = $sourceTree; status = 'pending-push' } + Write-ReleaseMetadata -ReleaseDir $releaseDir -TargetVersion $targetVersion -Artifacts $artifacts -GitRelease $gitRelease + try { Push-Release -Context $gitContext -Commit $commit } + catch { throw "Push failed; local release is preserved. Retry: .\release.cmd -Version $targetVersion -Resume. $($_.Exception.Message)" } + $gitRelease.status = 'pushed' + Write-ReleaseMetadata -ReleaseDir $releaseDir -TargetVersion $targetVersion -Artifacts $artifacts -GitRelease $gitRelease + } + Write-Host "" Write-Host "Release folder is ready:" Write-Host $releaseDir Write-Host "" - Write-Host "Upload the files from the release folder to GitHub release v$targetVersion." + Write-Host 'Загрузи EXE из artifacts\nsis на сайт. SHA256SUMS.txt содержит контрольную сумму.' +} catch { + [Console]::Error.WriteLine("`nРелиз не завершён.`n" + $_.Exception.Message) + exit 1 } finally { Pop-Location } diff --git a/scripts/update-component-bundle.ps1 b/scripts/update-component-bundle.ps1 new file mode 100644 index 0000000..c6913d7 --- /dev/null +++ b/scripts/update-component-bundle.ps1 @@ -0,0 +1,1917 @@ +[CmdletBinding()] +param( + [string]$OutputDir = '', + [switch]$PlanOnly, + [switch]$CheckOnly, + [switch]$UseFrozenReleaseEvidence, + [ValidateSet('None', 'Download', 'Validation', 'Promotion')] + [string]$SimulateFailure = 'None' +) + +$ErrorActionPreference = 'Stop' +Set-StrictMode -Version Latest +$ProgressPreference = 'SilentlyContinue' + +if ([string]::IsNullOrEmpty($OutputDir)) { + $OutputDir = Join-Path $PSScriptRoot '..\src-tauri\bundled\components' +} elseif ([string]::IsNullOrWhiteSpace($OutputDir)) { + throw 'OutputDir must not be whitespace.' +} + +$ExpectedComponents = @( + [PSCustomObject]@{ + id = 'proxifyre'; version = '2.4.0'; installRole = 'proxifyre-runtime'; assetArch = 'x64' + assetName = 'ProxiFyre-v2.4.0-x64-signed.zip'; licenseId = 'AGPL-3.0-only'; licensePath = 'proxifyre/LICENSE' + policyType = 'githubReleaseDigest'; sourceUrl = 'https://github.com/wiresock/proxifyre/releases/download/v2.4.0/ProxiFyre-v2.4.0-x64-signed.zip' + sha256 = 'eab65fd7d8eeb716abedb5614618c641de3f9eb8326b99cee1da787141e30cac'; size = 1519694L + fileVersion = '2.4.0'; productVersion = '2.4.0' + licenseSha256 = '8486a10c4393cee1c25392769ddd3b2d6c242d6ec7928e1414efff7dfb2f07ef'; licenseSize = 34523L + } + [PSCustomObject]@{ + id = 'windows-packet-filter'; version = '3.6.2'; installRole = 'packet-filter-driver'; assetArch = 'x64' + assetName = 'Windows.Packet.Filter.3.6.2.1.x64.msi'; licenseId = 'MIT'; licensePath = 'windows-packet-filter/LICENSE' + policyType = 'githubReleaseDigest'; sourceUrl = 'https://github.com/wiresock/ndisapi/releases/download/v3.6.2/Windows.Packet.Filter.3.6.2.1.x64.msi' + sha256 = '9c388c0b7f189f7fa98720bae2caecf7d64f30910838b80b438ecf8956b8502c'; size = 819200L + fileVersion = '3.6.2.1'; productVersion = '3.6.2.1' + licenseSha256 = 'b12f4cfcce43cef59100cf8c4eaf67ae5246c9a047e00f6059655f6694030efe'; licenseSize = 1070L + } + [PSCustomObject]@{ + id = 'vc-runtime'; version = '14.51.36247.0'; installRole = 'vc-runtime-prerequisite'; assetArch = 'x64' + assetName = 'VC_redist.x64.exe'; licenseId = 'LicenseRef-Microsoft-Visual-Cpp-v14-Redistributable-2026' + licensePath = 'vc-runtime/LICENSE.docx'; policyType = 'buildTimeOnlyAuthenticode' + sourceUrl = 'https://aka.ms/vs/18/release/14.51.36247/VC_redist.x64.exe' + sha256 = '843068991daaa1f73ad9f6239bce4d0f6a07a51f18c37ea2a867e9beca71295c'; size = 18731856L + fileVersion = '14.51.36247.0'; productVersion = '14.51.36247.0' + licenseSha256 = '08651651a7602fc7c0e2763de0fde1ff9f868df2780597cd1775ee9d6441c783'; licenseSize = 39553L + } + [PSCustomObject]@{ + id = 'sing-box'; version = '1.13.19'; installRole = 'sing-box-runtime'; assetArch = 'x64' + assetName = 'sing-box-1.13.19-windows-amd64.zip'; licenseId = 'LicenseRef-Sing-Box-Project' + licensePath = 'sing-box/LICENSE'; policyType = 'githubReleaseDigest' + sourceUrl = 'https://github.com/SagerNet/sing-box/releases/download/v1.13.19/sing-box-1.13.19-windows-amd64.zip' + sha256 = 'e011a4def2f5e2b143ed54adb2b1a20a6be407806ab4442f3667f1dd817a2c8d'; size = 21046252L + fileVersion = $null; productVersion = $null + licenseSha256 = '650d5e3b99a446fb38e820fa87a49562e0c79eab868fff58618ac487a58e554c'; licenseSize = 791L + } + [PSCustomObject]@{ + id = 'winsw'; version = '2.12.0'; installRole = 'sing-box-service-wrapper'; assetArch = 'anycpu' + assetName = 'WinSW.NET461.exe'; licenseId = 'MIT'; licensePath = 'winsw/LICENSE.txt' + policyType = 'bundledOnlyNoIndependentProof' + sourceUrl = 'https://github.com/winsw/winsw/releases/download/v2.12.0/WinSW.NET461.exe' + sha256 = 'b5066b7bbdfba1293e5d15cda3caaea88fbeab35bd5b38c41c913d492aadfc4f'; size = 655872L + fileVersion = '2.12.0.0'; productVersion = '2.12.0+eef5bade59fca0254e387ac73ed7625ba6aa7147' + licenseSha256 = '1cdf703c10a70e5973bf3acf2a5eeabe7746237155b92db2034aeae26fdf7802'; licenseSize = 1158L + } +) + +function ConvertTo-ResultJson([object]$Value) { + $Value | ConvertTo-Json -Depth 20 +} + +function Assert-ExactProperties( + [object]$Value, + [string[]]$Required, + [string[]]$Optional, + [string]$Label +) { + if ($null -eq $Value) { + throw "$Label is missing." + } + + $names = @($Value.PSObject.Properties.Name) + foreach ($name in $Required) { + if ($names -cnotcontains $name) { + throw "$Label is missing required property '$name'." + } + } + foreach ($name in $names) { + if (($Required -cnotcontains $name) -and ($Optional -cnotcontains $name)) { + throw "$Label contains unknown property '$name'." + } + } +} + +function Test-StableNumericVersion([object]$Value) { + if ($Value -isnot [string] -or $Value -notmatch '^[0-9]{1,10}(\.[0-9]{1,10}){1,3}$') { + return $false + } + return $true +} + +function Test-StableProductVersion([object]$Value) { + if ($Value -isnot [string]) { + return $false + } + $parts = $Value.Split('+') + if ($parts.Count -eq 1) { + return (Test-StableNumericVersion $Value) + } + if ($parts.Count -ne 2 -or -not (Test-StableNumericVersion $parts[0])) { + return $false + } + return $parts[1] -match '^[A-Za-z0-9-]+(?:\.[A-Za-z0-9-]+)*$' -and $parts[1].Length -le 128 +} + +function Test-WindowsReservedName([string]$Segment) { + $stem = $Segment.Split('.')[0].ToUpperInvariant() + if (@('CON', 'PRN', 'AUX', 'NUL') -contains $stem) { + return $true + } + return $stem -match '^(COM|LPT)[1-9]$' +} + +function Assert-SafeRelativePath([object]$Value, [string]$Label) { + if ($Value -isnot [string] -or $Value.Length -eq 0 -or $Value.Length -gt 512) { + throw "$Label is not a safe relative path." + } + if ($Value.Contains('\') -or $Value.StartsWith('/') -or $Value.EndsWith('/')) { + throw "$Label is not a safe relative path." + } + foreach ($segment in $Value.Split('/')) { + if ( + $segment.Length -eq 0 -or + $segment.Length -gt 128 -or + $segment -in @('.', '..') -or + $segment.EndsWith('.') -or + (Test-WindowsReservedName $segment) -or + $segment -notmatch '^[A-Za-z0-9._-]+$' + ) { + throw "$Label is not a safe relative path." + } + } +} + +function Assert-PlainHttpsUrl([object]$Value, [string]$Label) { + if ($Value -isnot [string]) { + throw "$Label must be an HTTPS URL." + } + $uri = $null + if (-not [Uri]::TryCreate($Value, [UriKind]::Absolute, [ref]$uri)) { + throw "$Label must be an HTTPS URL." + } + if ( + $uri.Scheme -ne 'https' -or + [string]::IsNullOrWhiteSpace($uri.Host) -or + -not [string]::IsNullOrEmpty($uri.UserInfo) -or + -not [string]::IsNullOrEmpty($uri.Query) -or + -not [string]::IsNullOrEmpty($uri.Fragment) -or + $Value -notmatch '^https://[^/:@]+(?:/|$)' + ) { + throw "$Label must be a plain HTTPS URL." + } + return $uri +} + +function Test-CatalogPattern([string]$Pattern, [string]$Value) { + $parts = $Pattern.Split('*') + if ($parts.Count -eq 1) { + return [string]::Equals($Pattern, $Value, [StringComparison]::Ordinal) + } + if ($parts.Count -ne 2) { + return $false + } + return $Value.StartsWith($parts[0], [StringComparison]::Ordinal) -and + $Value.EndsWith($parts[1], [StringComparison]::Ordinal) -and + $Value.Length -ge ($parts[0].Length + $parts[1].Length) +} + +function Assert-CatalogPattern([object]$Value, [string]$Label) { + if ( + $Value -isnot [string] -or + $Value.Length -eq 0 -or + $Value.Length -gt 160 -or + @($Value.ToCharArray() | Where-Object { $_ -eq '*' }).Count -gt 1 -or + $Value -notmatch '^[A-Za-z0-9._+*-]+$' + ) { + throw "$Label is invalid." + } +} + +function Assert-StringArray([object]$Value, [string]$Label) { + if ($Value -isnot [Array]) { + throw "$Label must be an array." + } + $items = @($Value) + if ($items.Count -eq 0) { + throw "$Label must not be empty." + } + $seen = @{} + foreach ($item in $items) { + if ($item -isnot [string] -or [string]::IsNullOrWhiteSpace($item) -or $item.Trim() -ne $item) { + throw "$Label contains an invalid value." + } + if ($seen.ContainsKey($item)) { + throw "$Label contains a duplicate value." + } + $seen[$item] = $true + } +} + +function Assert-TrustPolicy([object]$Component, [Uri]$SourceUri, [string]$AssetName) { + $policy = $Component.updateTrustPolicy + if ($null -eq $policy -or $policy.PSObject.Properties.Name -cnotcontains 'type') { + throw "updateTrustPolicy is missing for $($Component.id)." + } + + switch -CaseSensitive ($policy.type) { + 'githubReleaseDigest' { + Assert-ExactProperties $policy @('type', 'repository', 'tagPattern', 'assetPattern', 'requireStable') @('authenticodePublishers') "updateTrustPolicy for $($Component.id)" + if ($policy.repository -isnot [string] -or $policy.repository -notmatch '^[A-Za-z0-9._-]+/[A-Za-z0-9._-]+$') { + throw "GitHub repository is invalid for $($Component.id)." + } + Assert-CatalogPattern $policy.tagPattern 'tagPattern' + Assert-CatalogPattern $policy.assetPattern 'assetPattern' + if ($policy.requireStable -isnot [bool] -or -not $policy.requireStable) { + throw "GitHub release policy must require a stable release." + } + if ($policy.PSObject.Properties.Name -ccontains 'authenticodePublishers') { + Assert-StringArray $policy.authenticodePublishers 'authenticodePublishers' + } + if ($SourceUri.Host -ne 'github.com') { + throw "GitHub release source must use github.com." + } + $segments = @($SourceUri.AbsolutePath.Trim('/').Split('/')) + if ( + $segments.Count -ne 6 -or + $segments[2] -cne 'releases' -or + $segments[3] -cne 'download' -or + -not [string]::Equals("$($segments[0])/$($segments[1])", $policy.repository, [StringComparison]::OrdinalIgnoreCase) -or + $segments[5] -cne $AssetName -or + -not (Test-CatalogPattern $policy.tagPattern $segments[4]) -or + -not (Test-CatalogPattern $policy.assetPattern $AssetName) + ) { + throw "GitHub source does not match trust policy for $($Component.id)." + } + } + 'buildTimeOnlyAuthenticode' { + Assert-ExactProperties $policy @('type', 'allowedSourceHosts', 'assetPattern', 'publishers') @() "updateTrustPolicy for $($Component.id)" + Assert-StringArray $policy.allowedSourceHosts 'allowedSourceHosts' + Assert-StringArray $policy.publishers 'publishers' + Assert-CatalogPattern $policy.assetPattern 'assetPattern' + $hosts = @($policy.allowedSourceHosts | ForEach-Object { $_.ToLowerInvariant() }) + if ($hosts -notcontains $SourceUri.Host.ToLowerInvariant() -or -not (Test-CatalogPattern $policy.assetPattern $AssetName)) { + throw "Authenticode source does not match trust policy for $($Component.id)." + } + } + 'bundledOnlyNoIndependentProof' { + Assert-ExactProperties $policy @('type', 'reason') @() "updateTrustPolicy for $($Component.id)" + if ( + $policy.reason -isnot [string] -or + [string]::IsNullOrWhiteSpace($policy.reason) -or + $policy.reason.Trim() -ne $policy.reason -or + $policy.reason.Length -gt 240 -or + $policy.reason.IndexOfAny([char[]]@(0..31)) -ge 0 + ) { + throw "Bundled-only trust reason is invalid for $($Component.id)." + } + } + default { + throw "Unknown update trust policy for $($Component.id)." + } + } +} + +function Assert-PinnedTrustPolicy([object]$Component) { + $policy = $Component.updateTrustPolicy + switch -CaseSensitive ($Component.id) { + 'proxifyre' { + if ( + $policy.type -cne 'githubReleaseDigest' -or + $policy.repository -cne 'wiresock/proxifyre' -or + $policy.tagPattern -cne 'v*' -or + $policy.assetPattern -cne 'ProxiFyre-v*-x64-signed.zip' -or + @($policy.authenticodePublishers).Count -ne 1 -or + @($policy.authenticodePublishers)[0] -cne 'The Anti-Cloud Corporation' + ) { throw 'Pinned ProxiFyre trust policy mismatch.' } + } + 'windows-packet-filter' { + if ( + $policy.type -cne 'githubReleaseDigest' -or + $policy.repository -cne 'wiresock/ndisapi' -or + $policy.tagPattern -cne 'v*' -or + $policy.assetPattern -cne 'Windows.Packet.Filter.*.x64.msi' -or + @($policy.authenticodePublishers).Count -ne 1 -or + @($policy.authenticodePublishers)[0] -cne 'The Anti-Cloud Corporation' + ) { throw 'Pinned Windows Packet Filter trust policy mismatch.' } + } + 'sing-box' { + if ( + $policy.type -cne 'githubReleaseDigest' -or + $policy.repository -cne 'SagerNet/sing-box' -or + $policy.tagPattern -cne 'v*' -or + $policy.assetPattern -cne 'sing-box-*-windows-amd64.zip' -or + $policy.PSObject.Properties.Name -ccontains 'authenticodePublishers' + ) { throw 'Pinned sing-box trust policy mismatch.' } + } + 'vc-runtime' { + if ( + $policy.type -cne 'buildTimeOnlyAuthenticode' -or + @($policy.allowedSourceHosts).Count -ne 1 -or + @($policy.allowedSourceHosts)[0] -cne 'aka.ms' -or + $policy.assetPattern -cne 'VC_redist.x64.exe' -or + @($policy.publishers).Count -ne 1 -or + @($policy.publishers)[0] -cne 'Microsoft Corporation' + ) { throw 'Pinned VC runtime trust policy mismatch.' } + } + 'winsw' { + $expectedReason = 'The official v2.12.0 asset is unsigned and has no independent release digest; runtime network update is disabled.' + if ($policy.type -cne 'bundledOnlyNoIndependentProof' -or $policy.reason -cne $expectedReason) { + throw 'Pinned WinSW trust policy mismatch.' + } + } + default { throw "Unknown component id '$($Component.id)'." } + } +} + +function Assert-OfficialSource([object]$Component, [Uri]$SourceUri, [string]$AssetName) { + $repositories = @{ + 'proxifyre' = 'wiresock/proxifyre' + 'windows-packet-filter' = 'wiresock/ndisapi' + 'sing-box' = 'SagerNet/sing-box' + 'winsw' = 'winsw/winsw' + } + if ($Component.id -ceq 'vc-runtime') { + if (@('aka.ms', 'download.visualstudio.microsoft.com') -notcontains $SourceUri.Host.ToLowerInvariant()) { + throw 'VC runtime source is not an approved Microsoft host.' + } + return + } + if (-not $repositories.ContainsKey($Component.id) -or $SourceUri.Host -ne 'github.com') { + throw "Component source is not an official GitHub source for $($Component.id)." + } + $segments = @($SourceUri.AbsolutePath.Trim('/').Split('/')) + if ( + $segments.Count -ne 6 -or + -not [string]::Equals("$($segments[0])/$($segments[1])", $repositories[$Component.id], [StringComparison]::OrdinalIgnoreCase) -or + $segments[2] -cne 'releases' -or + $segments[3] -cne 'download' -or + $segments[4].TrimStart('v') -cne $Component.version -or + $segments[5] -cne $AssetName + ) { + throw "Component source is not its pinned official release for $($Component.id)." + } +} + +function Assert-NoReparseTree([string]$Root) { + $items = @((Get-Item -LiteralPath $Root -Force)) + @(Get-ChildItem -LiteralPath $Root -Recurse -Force) + foreach ($item in $items) { + if (($item.Attributes -band [IO.FileAttributes]::ReparsePoint) -ne 0) { + throw "Bundle contains a reparse point: $($item.Name)" + } + } +} + +function Remove-SafeGeneratedDirectory( + [string]$Path, + [string]$ExpectedParent, + [string]$LeafPattern +) { + if (-not (Test-Path -LiteralPath $Path)) { + return + } + $fullPath = [IO.Path]::GetFullPath($Path) + $fullParent = [IO.Path]::GetFullPath((Split-Path -Parent $fullPath)).TrimEnd('\', '/') + $expectedFullParent = [IO.Path]::GetFullPath($ExpectedParent).TrimEnd('\', '/') + $leaf = Split-Path -Leaf $fullPath + if ( + -not [string]::Equals($fullParent, $expectedFullParent, [StringComparison]::OrdinalIgnoreCase) -or + $leaf -notmatch $LeafPattern -or + -not (Test-Path -LiteralPath $fullPath -PathType Container) + ) { + throw "Refusing to remove an unexpected generated directory: $leaf" + } + Assert-NoReparseTree $fullPath + Remove-Item -LiteralPath $fullPath -Recurse -Force +} + +function Test-SafeEmptyDirectory([string]$Path) { + if (-not (Test-Path -LiteralPath $Path -PathType Container)) { + return $false + } + Assert-NoReparseTree $Path + return $null -eq (Get-ChildItem -LiteralPath $Path -Force | Select-Object -First 1) +} + +function Remove-SafeEmptyDirectory([string]$Path, [string]$ExpectedPath) { + $fullPath = [IO.Path]::GetFullPath($Path) + $expectedFullPath = [IO.Path]::GetFullPath($ExpectedPath) + if (-not [string]::Equals($fullPath, $expectedFullPath, [StringComparison]::OrdinalIgnoreCase)) { + throw 'Refusing to remove an unexpected empty directory.' + } + if (-not (Test-SafeEmptyDirectory $fullPath)) { + throw 'Refusing to remove a directory that is not a safe empty placeholder.' + } + Remove-Item -LiteralPath $fullPath -Force +} + +function Get-RelativeBundlePath([string]$Root, [string]$Path) { + $prefix = $Root.TrimEnd('\', '/') + [IO.Path]::DirectorySeparatorChar + if (-not $Path.StartsWith($prefix, [StringComparison]::OrdinalIgnoreCase)) { + throw 'Bundle entry escaped its root.' + } + return $Path.Substring($prefix.Length).Replace('\', '/') +} + +function Test-ComponentBundle([string]$Root) { + $resolvedRoot = [IO.Path]::GetFullPath($Root) + if (-not (Test-Path -LiteralPath $resolvedRoot -PathType Container)) { + throw "Component bundle does not exist: $resolvedRoot" + } + Assert-ValidatedParent (Split-Path -Parent $resolvedRoot) + Assert-NoReparseTree $resolvedRoot + + $catalogPath = Join-Path $resolvedRoot 'catalog.json' + if (-not (Test-Path -LiteralPath $catalogPath -PathType Leaf)) { + throw 'catalog.json is missing.' + } + try { + $catalog = Get-Content -Raw -LiteralPath $catalogPath | ConvertFrom-Json + } catch { + throw "catalog.json is invalid: $($_.Exception.Message)" + } + Assert-ExactProperties $catalog @('schemaVersion', 'targetArch', 'components') @() 'catalog' + $schemaIsInteger = $catalog.schemaVersion -is [Int32] -or $catalog.schemaVersion -is [Int64] + if ( + -not $schemaIsInteger -or + [Int64]$catalog.schemaVersion -ne 1 -or + $catalog.targetArch -isnot [string] -or + $catalog.targetArch -cne 'x64' + ) { + throw 'Unsupported component catalog schema or target architecture.' + } + + $components = @($catalog.components) + if ($components.Count -ne $ExpectedComponents.Count) { + throw 'Catalog must contain exactly five components.' + } + $seenIds = @{} + $seenRoles = @{} + $seenAssets = @{} + $seenLicenses = @{} + $expectedFiles = @{ 'catalog.json' = $true } + + foreach ($component in $components) { + Assert-ExactProperties $component @( + 'id', 'version', 'assetPath', 'assetArch', 'effectiveTarget', 'sha256', 'size', + 'sourceUrl', 'license', 'installRole', 'updateTrustPolicy' + ) @('fileVersion', 'productVersion') "component" + $expected = @($ExpectedComponents | Where-Object { $_.id -ceq $component.id }) + if ($expected.Count -ne 1) { + throw "Unknown or duplicate component id '$($component.id)'." + } + if ($seenIds.ContainsKey($component.id)) { + throw "Duplicate component id '$($component.id)'." + } + $seenIds[$component.id] = $true + + if ( + $component.version -cne $expected[0].version -or + $component.installRole -cne $expected[0].installRole -or + $component.assetArch -cne $expected[0].assetArch -or + $component.effectiveTarget -cne 'x64' + ) { + throw "Catalog identity does not match the pinned x64 baseline for $($component.id)." + } + if ($seenRoles.ContainsKey($component.installRole)) { + throw "Duplicate installRole '$($component.installRole)'." + } + $seenRoles[$component.installRole] = $true + if (-not (Test-StableNumericVersion $component.version)) { + throw "Invalid version for $($component.id)." + } + if ($component.PSObject.Properties.Name -ccontains 'fileVersion') { + if (-not (Test-StableNumericVersion $component.fileVersion)) { + throw "Invalid fileVersion for $($component.id)." + } + } + if ($component.PSObject.Properties.Name -ccontains 'productVersion') { + if (-not (Test-StableProductVersion $component.productVersion)) { + throw "Invalid productVersion for $($component.id)." + } + } + $hasFileVersion = $component.PSObject.Properties.Name -ccontains 'fileVersion' + $hasProductVersion = $component.PSObject.Properties.Name -ccontains 'productVersion' + if ( + ($null -eq $expected[0].fileVersion -and $hasFileVersion) -or + ($null -ne $expected[0].fileVersion -and (-not $hasFileVersion -or $component.fileVersion -cne $expected[0].fileVersion)) -or + ($null -eq $expected[0].productVersion -and $hasProductVersion) -or + ($null -ne $expected[0].productVersion -and (-not $hasProductVersion -or $component.productVersion -cne $expected[0].productVersion)) + ) { + throw "Version metadata does not match the pinned baseline for $($component.id)." + } + + Assert-SafeRelativePath $component.assetPath 'assetPath' + if ( + $component.assetPath.Split('/')[0] -cne $component.id -or + $component.assetPath.Split('/')[-1] -cne $expected[0].assetName + ) { + throw "assetPath must be inside the $($component.id) directory." + } + if ($seenAssets.ContainsKey($component.assetPath)) { + throw "Duplicate asset path '$($component.assetPath)'." + } + $seenAssets[$component.assetPath] = $true + + Assert-ExactProperties $component.license @('id', 'path') @() "license for $($component.id)" + if ( + $component.license.id -isnot [string] -or + $component.license.id -cnotmatch '^[A-Za-z0-9.+_-]{1,96}$' -or + $component.license.id -cne $expected[0].licenseId + ) { + throw "Invalid license id for $($component.id)." + } + Assert-SafeRelativePath $component.license.path 'license.path' + if ( + $component.license.path.Split('/')[0] -cne $component.id -or + $component.license.path -cne $expected[0].licensePath -or + $component.license.path -ceq $component.assetPath + ) { + throw "license.path must be inside the $($component.id) directory." + } + if ($seenLicenses.ContainsKey($component.license.path)) { + throw "Duplicate license path '$($component.license.path)'." + } + $seenLicenses[$component.license.path] = $true + + if ( + $component.sha256 -isnot [string] -or + $component.sha256 -cnotmatch '^[0-9a-f]{64}$' -or + $component.sha256 -cne $expected[0].sha256 + ) { + throw "Invalid SHA-256 for $($component.id)." + } + $sizeIsInteger = $component.size -is [Int32] -or $component.size -is [Int64] + if ( + -not $sizeIsInteger -or + [Int64]$component.size -le 0 -or + [Int64]$component.size -ne $expected[0].size + ) { + throw "Invalid size for $($component.id)." + } + $size = [Int64]$component.size + $sourceUri = Assert-PlainHttpsUrl $component.sourceUrl 'sourceUrl' + if ($component.sourceUrl -cne $expected[0].sourceUrl) { + throw "sourceUrl does not match the pinned baseline for $($component.id)." + } + $assetName = $component.assetPath.Split('/')[-1] + if ([Uri]::UnescapeDataString($sourceUri.Segments[-1].Trim('/')) -cne $assetName) { + throw "sourceUrl filename does not match assetPath for $($component.id)." + } + Assert-OfficialSource $component $sourceUri $assetName + Assert-TrustPolicy $component $sourceUri $assetName + if ($component.updateTrustPolicy.type -cne $expected[0].policyType) { + throw "Trust policy type does not match the pinned baseline for $($component.id)." + } + Assert-PinnedTrustPolicy $component + + $assetFullPath = [IO.Path]::GetFullPath((Join-Path $resolvedRoot $component.assetPath.Replace('/', '\'))) + $licenseFullPath = [IO.Path]::GetFullPath((Join-Path $resolvedRoot $component.license.path.Replace('/', '\'))) + if (-not (Test-Path -LiteralPath $assetFullPath -PathType Leaf)) { + throw "Asset is missing for $($component.id)." + } + if (-not (Test-Path -LiteralPath $licenseFullPath -PathType Leaf)) { + throw "License is missing or empty for $($component.id)." + } + $licenseItem = Get-Item -LiteralPath $licenseFullPath + $licenseHash = (Get-FileHash -LiteralPath $licenseFullPath -Algorithm SHA256).Hash.ToLowerInvariant() + if ($licenseItem.Length -ne $expected[0].licenseSize -or $licenseHash -cne $expected[0].licenseSha256) { + throw "License hash or size mismatch for $($component.id)." + } + Assert-LocalLicenseIdentity $component.id $licenseFullPath + $asset = Get-Item -LiteralPath $assetFullPath + if ($asset.Length -ne $size) { + throw "Asset size mismatch for $($component.id)." + } + $actualHash = (Get-FileHash -LiteralPath $assetFullPath -Algorithm SHA256).Hash.ToLowerInvariant() + if ($actualHash -cne $component.sha256) { + throw "Asset SHA-256 mismatch for $($component.id)." + } + Assert-LocalPackageIdentity $component.id $assetFullPath + $expectedFiles[$component.assetPath] = $true + $expectedFiles[$component.license.path] = $true + } + + $actualFiles = @{} + foreach ($file in Get-ChildItem -LiteralPath $resolvedRoot -Recurse -File -Force) { + $relative = Get-RelativeBundlePath $resolvedRoot $file.FullName + Assert-SafeRelativePath $relative 'bundle entry' + $actualFiles[$relative] = $true + } + $missing = @($expectedFiles.Keys | Where-Object { -not $actualFiles.ContainsKey($_) }) + $extra = @($actualFiles.Keys | Where-Object { -not $expectedFiles.ContainsKey($_) }) + if ($missing.Count -gt 0 -or $extra.Count -gt 0) { + throw "Bundle file set mismatch (missing: $($missing.Count), extra: $($extra.Count))." + } + return $catalog +} + +function Assert-ValidatedParent([string]$ParentPath) { + if (-not (Test-Path -LiteralPath $ParentPath -PathType Container)) { + throw "Output parent directory does not exist: $ParentPath" + } + $current = Get-Item -LiteralPath $ParentPath -Force + while ($null -ne $current) { + if (($current.Attributes -band [IO.FileAttributes]::ReparsePoint) -ne 0) { + throw "Output parent contains a reparse point: $($current.FullName)" + } + $current = $current.Parent + } +} + +function Get-RetryDelaySeconds([object]$Response, [int]$TransientFailures) { + $delaySeconds = [Math]::Pow(2, $TransientFailures - 1) + $retryAfter = $Response.Headers.RetryAfter + if ($null -ne $retryAfter) { + if ($null -ne $retryAfter.Delta) { + $delaySeconds = $retryAfter.Delta.TotalSeconds + } elseif ($null -ne $retryAfter.Date) { + $delaySeconds = ($retryAfter.Date.UtcDateTime - [DateTime]::UtcNow).TotalSeconds + } + } + return [Math]::Min(30, [Math]::Max(0, [Math]::Ceiling($delaySeconds))) +} + +function Invoke-JsonApi([string]$Uri) { + $parsed = Assert-PlainHttpsUrl $Uri 'API URL' + if ($parsed.Host -ne 'api.github.com') { + throw 'Only the official GitHub API is allowed.' + } + + Add-Type -AssemblyName System.Net.Http + $handler = [Net.Http.HttpClientHandler]::new() + $handler.AllowAutoRedirect = $false + $handler.AutomaticDecompression = [Net.DecompressionMethods]::GZip -bor [Net.DecompressionMethods]::Deflate + $client = [Net.Http.HttpClient]::new($handler) + $client.Timeout = [Threading.Timeout]::InfiniteTimeSpan + $deadline = [Threading.CancellationTokenSource]::new([TimeSpan]::FromSeconds(60)) + [void]$client.DefaultRequestHeaders.UserAgent.ParseAdd('proxywarden-component-bundle-updater') + [void]$client.DefaultRequestHeaders.Accept.ParseAdd('application/vnd.github+json') + [void]$client.DefaultRequestHeaders.Add('X-GitHub-Api-Version', '2022-11-28') + $response = $null + try { + try { + $transientFailures = 0 + while ($true) { + $response = $client.GetAsync( + $parsed, + [Net.Http.HttpCompletionOption]::ResponseHeadersRead, + $deadline.Token + ).GetAwaiter().GetResult() + $statusCode = [int]$response.StatusCode + if ($statusCode -in @(301, 302, 303, 307, 308)) { + throw 'GitHub API redirect was rejected for api.github.com.' + } + if ($statusCode -eq 408 -or $statusCode -eq 429 -or ($statusCode -ge 500 -and $statusCode -le 599)) { + $transientFailures++ + if ($transientFailures -ge 3) { + throw "GitHub API failed with transient HTTP status $statusCode after three attempts at api.github.com." + } + $delaySeconds = Get-RetryDelaySeconds $response $transientFailures + $response.Dispose() + $response = $null + [void]([Threading.Tasks.Task]::Delay([TimeSpan]::FromSeconds($delaySeconds), $deadline.Token).GetAwaiter().GetResult()) + continue + } + if ($statusCode -lt 200 -or $statusCode -gt 299) { + throw "GitHub API failed with HTTP status $statusCode at api.github.com." + } + $contentLength = $response.Content.Headers.ContentLength + if ($null -ne $contentLength -and [Int64]$contentLength -gt 1048576) { + throw 'GitHub API response exceeded 1 MiB at api.github.com.' + } + $input = $response.Content.ReadAsStreamAsync().GetAwaiter().GetResult() + $output = [IO.MemoryStream]::new() + try { + $buffer = [byte[]]::new(32768) + $total = 0L + while (($count = $input.ReadAsync($buffer, 0, $buffer.Length, $deadline.Token).GetAwaiter().GetResult()) -gt 0) { + $total += $count + if ($total -gt 1048576) { + throw 'GitHub API response exceeded 1 MiB at api.github.com.' + } + $output.Write($buffer, 0, $count) + } + $body = [Text.UTF8Encoding]::new($false, $true).GetString($output.ToArray()).TrimStart([char]0xfeff) + } finally { + $output.Dispose() + $input.Dispose() + } + try { + return $body | ConvertFrom-Json + } catch { + throw 'GitHub API returned invalid JSON from api.github.com.' + } + } + } catch { + if ($_.Exception.Message.StartsWith('GitHub API ', [StringComparison]::Ordinal)) { + throw + } + throw 'GitHub API request failed for api.github.com.' + } + } finally { + if ($null -ne $response) { $response.Dispose() } + $deadline.Dispose() + $client.Dispose() + $handler.Dispose() + } +} + +function Get-PinnedRelease([string]$Repository, [string]$Tag) { + $release = Invoke-JsonApi "https://api.github.com/repos/$Repository/releases/tags/$Tag" + if ($release.tag_name -cne $Tag -or [bool]$release.draft -or [bool]$release.prerelease) { + throw "GitHub release $Repository/$Tag is not the expected stable release." + } + return $release +} + +function Get-ReleaseEvidence([string]$Repository, [string]$Tag, [bool]$UseFrozen) { + if (-not $UseFrozen) { + return Get-PinnedRelease $Repository $Tag + } + + $asset = switch ("$Repository@$Tag") { + 'wiresock/proxifyre@v2.4.0' { + $releaseId = 356296939L + $publishedAt = '2026-07-19T08:51:37Z' + $releaseHtmlUrl = 'https://github.com/wiresock/proxifyre/releases/tag/v2.4.0' + [PSCustomObject]@{ + id = 482601136L + url = 'https://api.github.com/repos/wiresock/proxifyre/releases/assets/482601136' + name = 'ProxiFyre-v2.4.0-x64-signed.zip' + browser_download_url = 'https://github.com/wiresock/proxifyre/releases/download/v2.4.0/ProxiFyre-v2.4.0-x64-signed.zip' + size = 1519694L + digest = 'sha256:eab65fd7d8eeb716abedb5614618c641de3f9eb8326b99cee1da787141e30cac' + } + } + 'wiresock/ndisapi@v3.6.2' { + $releaseId = 256618257L + $publishedAt = '2025-10-23T09:12:20Z' + $releaseHtmlUrl = 'https://github.com/wiresock/ndisapi/releases/tag/v3.6.2' + [PSCustomObject]@{ + id = 307688568L + url = 'https://api.github.com/repos/wiresock/ndisapi/releases/assets/307688568' + name = 'Windows.Packet.Filter.3.6.2.1.x64.msi' + browser_download_url = 'https://github.com/wiresock/ndisapi/releases/download/v3.6.2/Windows.Packet.Filter.3.6.2.1.x64.msi' + size = 819200L + digest = 'sha256:9c388c0b7f189f7fa98720bae2caecf7d64f30910838b80b438ecf8956b8502c' + } + } + 'SagerNet/sing-box@v1.13.19' { + $releaseId = 371636056L + $publishedAt = '2026-08-17T09:47:06Z' + $releaseHtmlUrl = 'https://github.com/SagerNet/sing-box/releases/tag/v1.13.19' + [PSCustomObject]@{ + id = 517910532L + url = 'https://api.github.com/repos/SagerNet/sing-box/releases/assets/517910532' + name = 'sing-box-1.13.19-windows-amd64.zip' + browser_download_url = 'https://github.com/SagerNet/sing-box/releases/download/v1.13.19/sing-box-1.13.19-windows-amd64.zip' + size = 21046252L + digest = 'sha256:e011a4def2f5e2b143ed54adb2b1a20a6be407806ab4442f3667f1dd817a2c8d' + } + } + 'winsw/winsw@v2.12.0' { + $releaseId = 90528888L + $publishedAt = '2023-01-28T16:22:38Z' + $releaseHtmlUrl = 'https://github.com/winsw/winsw/releases/tag/v2.12.0' + [PSCustomObject]@{ + id = 93386826L + url = 'https://api.github.com/repos/winsw/winsw/releases/assets/93386826' + name = 'WinSW.NET461.exe' + browser_download_url = 'https://github.com/winsw/winsw/releases/download/v2.12.0/WinSW.NET461.exe' + size = 655872L + digest = $null + } + } + default { throw 'Frozen release evidence does not cover the requested repository and tag.' } + } + return [PSCustomObject]@{ + id = $releaseId + url = "https://api.github.com/repos/$Repository/releases/$releaseId" + html_url = $releaseHtmlUrl + tag_name = $Tag + draft = $false + prerelease = $false + published_at = $publishedAt + assets = @($asset) + } +} + +function Get-ReleaseAsset([object]$Release, [string]$Name) { + $matches = @($Release.assets | Where-Object { $_.name -ceq $Name }) + if ($matches.Count -ne 1) { + throw "Expected exactly one release asset named $Name." + } + return $matches[0] +} + +function Test-AllowedRedirect([Uri]$InitialUri, [Uri]$NextUri) { + if ( + $NextUri.Scheme -ne 'https' -or + -not [string]::IsNullOrEmpty($NextUri.UserInfo) -or + -not $NextUri.IsDefaultPort -or + -not [string]::IsNullOrEmpty($NextUri.Fragment) + ) { + return $false + } + $initialHost = $InitialUri.Host.ToLowerInvariant() + $nextHost = $NextUri.Host.ToLowerInvariant() + switch ($initialHost) { + 'github.com' { return @('github.com', 'release-assets.githubusercontent.com') -contains $nextHost } + 'release-assets.githubusercontent.com' { return $nextHost -eq 'release-assets.githubusercontent.com' } + 'raw.githubusercontent.com' { return $nextHost -eq 'raw.githubusercontent.com' } + 'aka.ms' { return @('aka.ms', 'download.visualstudio.microsoft.com') -contains $nextHost } + 'download.visualstudio.microsoft.com' { return $nextHost -eq 'download.visualstudio.microsoft.com' } + 'visualstudio.microsoft.com' { return $nextHost -eq 'visualstudio.microsoft.com' } + default { return $false } + } +} + +function Save-Download([string]$Uri, [string]$Path, [Int64]$MaxBytes) { + $initialUri = Assert-PlainHttpsUrl $Uri 'Download URL' + if ($MaxBytes -le 0) { + throw 'Download size limit must be positive.' + } + $parent = Split-Path -Parent $Path + [void](New-Item -ItemType Directory -Path $parent -Force) + $partial = "$Path.part" + Add-Type -AssemblyName System.Net.Http + $handler = [Net.Http.HttpClientHandler]::new() + $handler.AllowAutoRedirect = $false + $handler.AutomaticDecompression = [Net.DecompressionMethods]::GZip -bor [Net.DecompressionMethods]::Deflate + $client = [Net.Http.HttpClient]::new($handler) + $client.Timeout = [Threading.Timeout]::InfiniteTimeSpan + $deadline = [Threading.CancellationTokenSource]::new([TimeSpan]::FromSeconds(240)) + [void]$client.DefaultRequestHeaders.UserAgent.ParseAdd('proxywarden-component-bundle-updater') + [void]$client.DefaultRequestHeaders.Accept.ParseAdd('application/octet-stream,*/*') + $currentUri = $initialUri + $response = $null + try { + $redirectCount = 0 + $transientFailures = 0 + while ($true) { + $response = $client.GetAsync( + $currentUri, + [Net.Http.HttpCompletionOption]::ResponseHeadersRead, + $deadline.Token + ).GetAwaiter().GetResult() + $statusCode = [int]$response.StatusCode + if ($statusCode -in @(301, 302, 303, 307, 308)) { + if ($redirectCount -ge 5 -or $null -eq $response.Headers.Location) { + throw "Download exceeded the redirect limit: $Uri" + } + $nextUri = if ($response.Headers.Location.IsAbsoluteUri) { + $response.Headers.Location + } else { + [Uri]::new($currentUri, $response.Headers.Location) + } + if (-not (Test-AllowedRedirect $currentUri $nextUri)) { + throw "Download redirect target is not allowed: $($nextUri.Host)" + } + $response.Dispose() + $response = $null + $currentUri = $nextUri + $redirectCount++ + continue + } + if ($statusCode -eq 408 -or $statusCode -eq 429 -or ($statusCode -ge 500 -and $statusCode -le 599)) { + $transientFailures++ + if ($transientFailures -ge 3) { + throw "Download failed with transient HTTP status $statusCode after three attempts." + } + $delaySeconds = Get-RetryDelaySeconds $response $transientFailures + $response.Dispose() + $response = $null + [void]([Threading.Tasks.Task]::Delay([TimeSpan]::FromSeconds($delaySeconds), $deadline.Token).GetAwaiter().GetResult()) + continue + } + [void]$response.EnsureSuccessStatusCode() + $contentLength = $response.Content.Headers.ContentLength + if ($null -ne $contentLength -and [Int64]$contentLength -gt $MaxBytes) { + throw "Download exceeds the size limit: $Uri" + } + $input = $response.Content.ReadAsStreamAsync().GetAwaiter().GetResult() + $output = [IO.File]::Open($partial, [IO.FileMode]::CreateNew, [IO.FileAccess]::Write, [IO.FileShare]::None) + try { + $buffer = [byte[]]::new(65536) + $total = 0L + while (($count = $input.ReadAsync($buffer, 0, $buffer.Length, $deadline.Token).GetAwaiter().GetResult()) -gt 0) { + $total += $count + if ($total -gt $MaxBytes) { + throw "Download exceeds the size limit: $Uri" + } + $output.Write($buffer, 0, $count) + if ($script:InjectDownloadFailure) { + $script:InjectDownloadFailure = $false + throw 'Simulated bundle download failure after a partial write.' + } + } + } finally { + $output.Dispose() + $input.Dispose() + } + break + } + if (-not (Test-Path -LiteralPath $partial -PathType Leaf) -or (Get-Item -LiteralPath $partial).Length -le 0) { + throw "Downloaded file is empty: $Uri" + } + Move-Item -LiteralPath $partial -Destination $Path + } finally { + if ($null -ne $response) { $response.Dispose() } + $deadline.Dispose() + $client.Dispose() + $handler.Dispose() + Remove-Item -LiteralPath $partial -Force -ErrorAction SilentlyContinue + } +} + +function Save-GitHubDigestAsset( + [object]$Release, + [string]$Name, + [string]$ExpectedUrl, + [string]$Destination, + [string]$FrozenHash, + [Int64]$FrozenSize +) { + $asset = Get-ReleaseAsset $Release $Name + if ($asset.browser_download_url -cne $ExpectedUrl) { + throw "Official asset URL changed for $Name." + } + $digestProperty = $asset.PSObject.Properties['digest'] + if ($null -eq $digestProperty -or $digestProperty.Value -cnotmatch '^sha256:([0-9a-f]{64})$') { + throw "GitHub did not provide an independent SHA-256 digest for $Name." + } + $expectedHash = $Matches[1] + if ($expectedHash -cne $FrozenHash -or [Int64]$asset.size -ne $FrozenSize) { + throw "GitHub release identity does not match the frozen baseline for $Name." + } + Save-Download $ExpectedUrl $Destination $FrozenSize + $item = Get-Item -LiteralPath $Destination + if ($item.Length -ne [Int64]$asset.size) { + throw "GitHub asset size mismatch for $Name." + } + $actualHash = (Get-FileHash -LiteralPath $Destination -Algorithm SHA256).Hash.ToLowerInvariant() + if ($actualHash -cne $expectedHash) { + throw "GitHub digest mismatch for $Name." + } + return [PSCustomObject]@{ hash = $actualHash; size = $item.Length; url = $ExpectedUrl } +} + +function Save-PinnedAsset( + [string]$Uri, + [string]$Destination, + [string]$ExpectedHash, + [Int64]$ExpectedSize +) { + Save-Download $Uri $Destination $ExpectedSize + $item = Get-Item -LiteralPath $Destination + $actualHash = (Get-FileHash -LiteralPath $Destination -Algorithm SHA256).Hash.ToLowerInvariant() + if ($item.Length -ne $ExpectedSize -or $actualHash -cne $ExpectedHash) { + throw "Pinned asset identity mismatch for $($item.Name)." + } + return [PSCustomObject]@{ hash = $actualHash; size = $item.Length; url = $Uri } +} + +function Assert-AuthenticodePublisher([string]$Path, [string]$Publisher) { + $signature = Get-AuthenticodeSignature -LiteralPath $Path + if ($signature.Status -ne [Management.Automation.SignatureStatus]::Valid -or $null -eq $signature.SignerCertificate) { + throw "Authenticode signature is not valid for $(Split-Path -Leaf $Path)." + } + $subject = $signature.SignerCertificate.Subject + $escapedPublisher = [Regex]::Escape($Publisher) + if ( + -not [Regex]::IsMatch($subject, "(?:^|,\s*)CN=$escapedPublisher(?:,|$)", [Text.RegularExpressions.RegexOptions]::IgnoreCase) -or + -not [Regex]::IsMatch($subject, "(?:^|,\s*)O=$escapedPublisher(?:,|$)", [Text.RegularExpressions.RegexOptions]::IgnoreCase) + ) { + throw "Authenticode publisher mismatch for $(Split-Path -Leaf $Path)." + } +} + +function Assert-Unsigned([string]$Path) { + $signature = Get-AuthenticodeSignature -LiteralPath $Path + if ($signature.Status.ToString() -ne 'NotSigned') { + throw "Expected an unsigned pinned asset: $(Split-Path -Leaf $Path)." + } +} + +function Assert-ManagedAnyCpu([string]$Path) { + $bytes = [IO.File]::ReadAllBytes($Path) + if ($bytes.Length -lt 256 -or $bytes[0] -ne 0x4d -or $bytes[1] -ne 0x5a) { + throw 'WinSW is not a valid PE file.' + } + $peOffset = [BitConverter]::ToInt32($bytes, 0x3c) + if ( + $peOffset -lt 0 -or $peOffset + 256 -gt $bytes.Length -or + [BitConverter]::ToUInt32($bytes, $peOffset) -ne 0x00004550 -or + [BitConverter]::ToUInt16($bytes, $peOffset + 4) -ne 0x014c + ) { + throw 'WinSW has an invalid PE header.' + } + $sectionCount = [BitConverter]::ToUInt16($bytes, $peOffset + 6) + $optionalSize = [BitConverter]::ToUInt16($bytes, $peOffset + 20) + $optionalOffset = $peOffset + 24 + if ([BitConverter]::ToUInt16($bytes, $optionalOffset) -ne 0x010b) { + throw 'WinSW must use the audited PE32 AnyCPU layout.' + } + $clrDirectoryOffset = $optionalOffset + 96 + (14 * 8) + if ($clrDirectoryOffset + 8 -gt $optionalOffset + $optionalSize) { + throw 'WinSW PE header has no CLR directory.' + } + $clrRva = [BitConverter]::ToUInt32($bytes, $clrDirectoryOffset) + if ($clrRva -eq 0) { + throw 'WinSW is not a managed assembly.' + } + $sectionOffset = $optionalOffset + $optionalSize + $clrFileOffset = $null + for ($index = 0; $index -lt $sectionCount; $index++) { + $offset = $sectionOffset + ($index * 40) + if ($offset + 40 -gt $bytes.Length) { throw 'WinSW PE section table is truncated.' } + $virtualSize = [BitConverter]::ToUInt32($bytes, $offset + 8) + $virtualAddress = [BitConverter]::ToUInt32($bytes, $offset + 12) + $rawSize = [BitConverter]::ToUInt32($bytes, $offset + 16) + $rawOffset = [BitConverter]::ToUInt32($bytes, $offset + 20) + $mappedSize = [Math]::Max([UInt64]$virtualSize, [UInt64]$rawSize) + if ([UInt64]$clrRva -ge [UInt64]$virtualAddress -and [UInt64]$clrRva -lt ([UInt64]$virtualAddress + $mappedSize)) { + $clrFileOffset = [Int64]$rawOffset + ([Int64]$clrRva - [Int64]$virtualAddress) + break + } + } + if ($null -eq $clrFileOffset -or $clrFileOffset + 20 -gt $bytes.Length) { + throw 'WinSW CLR header is outside the PE sections.' + } + $flags = [BitConverter]::ToUInt32($bytes, [int]$clrFileOffset + 16) + $ilOnly = ($flags -band 0x00000001) -ne 0 + $requires32Bit = ($flags -band 0x00000002) -ne 0 + $prefers32Bit = ($flags -band 0x00020000) -ne 0 + if (-not $ilOnly -or $requires32Bit -or $prefers32Bit) { + throw 'WinSW must be ILOnly AnyCPU without 32-bit preference flags.' + } + $metadataText = [Text.Encoding]::UTF8.GetString($bytes) + if ($metadataText.IndexOf('.NETFramework,Version=v4.6.1', [StringComparison]::Ordinal) -lt 0) { + throw 'WinSW must target the audited .NET Framework 4.6.1 runtime.' + } +} + +function Assert-PeBytesMachineX64([byte[]]$Bytes, [string]$Label) { + $bytes = $Bytes + if ($bytes.Length -lt 128 -or $bytes[0] -ne 0x4d -or $bytes[1] -ne 0x5a) { + throw "File is not a valid PE image: $Label" + } + $peOffset = [BitConverter]::ToInt32($bytes, 0x3c) + if ( + $peOffset -lt 0 -or $peOffset + 26 -gt $bytes.Length -or + [BitConverter]::ToUInt32($bytes, $peOffset) -ne 0x00004550 -or + [BitConverter]::ToUInt16($bytes, $peOffset + 4) -ne 0x8664 + ) { + throw "PE image is not x64: $Label" + } +} + +function Assert-PeMachineX64([string]$Path) { + Assert-PeBytesMachineX64 ([IO.File]::ReadAllBytes($Path)) (Split-Path -Leaf $Path) +} + +function Get-ZipEntryBytes([string]$Path, [string]$LeafName) { + Add-Type -AssemblyName System.IO.Compression.FileSystem + $archive = [IO.Compression.ZipFile]::OpenRead($Path) + try { + $matches = @($archive.Entries | Where-Object { $_.Name -ceq $LeafName }) + if ($matches.Count -ne 1) { + throw "Archive must contain exactly one $LeafName." + } + $input = $matches[0].Open() + $output = [IO.MemoryStream]::new() + try { + $input.CopyTo($output) + return ,$output.ToArray() + } finally { + $output.Dispose() + $input.Dispose() + } + } finally { + $archive.Dispose() + } +} + +function Get-ZipFullEntryBytes([string]$Path, [string]$FullName, [Int64]$MaxBytes) { + Add-Type -AssemblyName System.IO.Compression.FileSystem + $archive = [IO.Compression.ZipFile]::OpenRead($Path) + try { + $matches = @($archive.Entries | Where-Object { $_.FullName.Replace('\', '/') -ceq $FullName }) + if ($matches.Count -ne 1 -or $matches[0].Length -le 0 -or $matches[0].Length -gt $MaxBytes) { + throw "Document archive entry is missing or too large: $FullName" + } + $input = $matches[0].Open() + $output = [IO.MemoryStream]::new() + try { + $input.CopyTo($output) + return ,$output.ToArray() + } finally { + $output.Dispose() + $input.Dispose() + } + } finally { + $archive.Dispose() + } +} + +function Invoke-PinnedGit( + [string[]]$Arguments, + [string]$EmptyConfigPath, + [string]$Operation +) { + $git = Get-Command git.exe -CommandType Application -ErrorAction Stop | Select-Object -First 1 + $environmentValues = @{ + GIT_CONFIG_GLOBAL = $EmptyConfigPath + GIT_CONFIG_SYSTEM = $EmptyConfigPath + GIT_CONFIG_NOSYSTEM = '1' + GIT_CONFIG_COUNT = '0' + GIT_TERMINAL_PROMPT = '0' + GCM_INTERACTIVE = 'Never' + GIT_LFS_SKIP_SMUDGE = '1' + GIT_PROTOCOL_FROM_USER = '0' + } + $previous = @{} + foreach ($name in $environmentValues.Keys) { + $previous[$name] = [PSCustomObject]@{ + exists = Test-Path -LiteralPath "Env:$name" + value = [Environment]::GetEnvironmentVariable($name, 'Process') + } + [Environment]::SetEnvironmentVariable($name, $environmentValues[$name], 'Process') + } + $previousPreference = $ErrorActionPreference + try { + $ErrorActionPreference = 'Continue' + $output = @(& $git.Source @Arguments 2>&1) + $exitCode = $LASTEXITCODE + if ($exitCode -ne 0) { + throw "Pinned Git license acquisition failed during $Operation." + } + return @($output | ForEach-Object { $_.ToString() }) + } finally { + $ErrorActionPreference = $previousPreference + foreach ($name in $environmentValues.Keys) { + if ($previous[$name].exists) { + [Environment]::SetEnvironmentVariable($name, $previous[$name].value, 'Process') + } else { + [Environment]::SetEnvironmentVariable($name, $null, 'Process') + } + } + } +} + +function Save-LicenseFromPinnedGit( + [string]$RepositoryUrl, + [string]$RepositoryKey, + [string]$Tag, + [string]$TagObject, + [string]$Commit, + [string]$LicenseName, + [string]$Destination, + [string]$WorkRoot +) { + $identity = "$RepositoryUrl|$RepositoryKey|$Tag|$TagObject|$Commit|$LicenseName" + $allowed = @( + 'https://github.com/wiresock/proxifyre.git|proxifyre|v2.4.0|dd1512840e1e3bc596b06b80eda4e2dcd6a9c9ed|dd1512840e1e3bc596b06b80eda4e2dcd6a9c9ed|LICENSE', + 'https://github.com/wiresock/ndisapi.git|ndisapi|v3.6.2|417b8734e844083a10236387fba705d94a2d6bc9|417b8734e844083a10236387fba705d94a2d6bc9|LICENSE', + 'https://github.com/SagerNet/sing-box.git|sing-box|v1.13.19|b5ebaa1fc0f2b94256180b95468e73ef53caa27d|b5ebaa1fc0f2b94256180b95468e73ef53caa27d|LICENSE', + 'https://github.com/winsw/winsw.git|winsw|v2.12.0|eef5bade59fca0254e387ac73ed7625ba6aa7147|eef5bade59fca0254e387ac73ed7625ba6aa7147|LICENSE.txt' + ) + if ($allowed -cnotcontains $identity) { + throw 'Pinned Git license identity is not allowlisted.' + } + Assert-NoReparseTree $WorkRoot + $emptyConfigPath = Join-Path $WorkRoot 'empty.gitconfig' + if (-not (Test-Path -LiteralPath $emptyConfigPath)) { + [IO.File]::WriteAllText($emptyConfigPath, '', [Text.UTF8Encoding]::new($false)) + } + $repoPath = Join-Path $WorkRoot "$RepositoryKey-repo" + $archivePath = Join-Path $WorkRoot "$RepositoryKey-license.zip" + if ((Test-Path -LiteralPath $repoPath) -or (Test-Path -LiteralPath $archivePath)) { + throw 'Pinned Git work path already exists.' + } + + $remoteTag = @(Invoke-PinnedGit @( + 'ls-remote', '--refs', '--', $RepositoryUrl, "refs/tags/$Tag" + ) $emptyConfigPath "verify remote $RepositoryKey tag") + $remoteTagLines = @($remoteTag | Where-Object { -not [string]::IsNullOrWhiteSpace($_) }) + $expectedRemoteTag = "$TagObject`trefs/tags/$Tag" + if ($remoteTagLines.Count -ne 1 -or $remoteTagLines[0].TrimEnd() -cne $expectedRemoteTag) { + throw "Pinned Git remote tag object mismatch for $RepositoryKey." + } + + [void](Invoke-PinnedGit @( + '-c', 'init.templateDir=', 'init', '--quiet', $repoPath + ) $emptyConfigPath "initialize $RepositoryKey") + [void](Invoke-PinnedGit @( + '-C', $repoPath, '-c', 'core.hooksPath=NUL', 'remote', 'add', 'origin', $RepositoryUrl + ) $emptyConfigPath "configure $RepositoryKey origin") + [void](Invoke-PinnedGit @( + '-C', $repoPath, '-c', 'core.hooksPath=NUL', '-c', 'protocol.file.allow=never', + '-c', 'http.sslBackend=schannel', 'fetch', '--quiet', '--depth', '1', + '--no-tags', 'origin', "+refs/tags/$Tag`:refs/tags/$Tag" + ) $emptyConfigPath "fetch exact $RepositoryKey tag") + Assert-NoReparseTree $repoPath + $localTag = @(Invoke-PinnedGit @('-C', $repoPath, 'rev-parse', '--verify', "refs/tags/$Tag") $emptyConfigPath "verify fetched $RepositoryKey tag") + $localTagValue = (@($localTag | Where-Object { -not [string]::IsNullOrWhiteSpace($_) }) -join '').Trim() + if ($localTagValue -cne $TagObject) { + throw "Pinned Git fetched tag object mismatch for $RepositoryKey." + } + $head = @(Invoke-PinnedGit @('-C', $repoPath, 'rev-parse', '--verify', "refs/tags/$Tag`^{commit}") $emptyConfigPath "verify $RepositoryKey commit") + $headValue = (@($head | Where-Object { -not [string]::IsNullOrWhiteSpace($_) }) -join '').Trim() + if ($headValue -cne $Commit) { + throw "Pinned Git commit mismatch for $RepositoryKey." + } + [void](Invoke-PinnedGit @( + '-C', $repoPath, '-c', 'core.hooksPath=NUL', 'archive', '--format=zip', + "--output=$archivePath", $Commit, '--', $LicenseName + ) $emptyConfigPath "archive $RepositoryKey license") + + if (-not (Test-Path -LiteralPath $archivePath -PathType Leaf)) { + throw "Pinned Git license archive is missing for $RepositoryKey." + } + Add-Type -AssemblyName System.IO.Compression.FileSystem + $archive = [IO.Compression.ZipFile]::OpenRead($archivePath) + try { + if ($archive.Entries.Count -ne 1) { + throw "Pinned Git archive must contain exactly one license for $RepositoryKey." + } + $entry = $archive.Entries[0] + $unixFileType = (($entry.ExternalAttributes -shr 16) -band 0xf000) + if ( + $entry.FullName -cne $LicenseName -or + [string]::IsNullOrEmpty($entry.Name) -or + $entry.Length -le 0 -or + $entry.Length -gt 1048576 -or + ($unixFileType -ne 0 -and $unixFileType -ne 0x8000) + ) { + throw "Pinned Git archive has an invalid license entry for $RepositoryKey." + } + $input = $entry.Open() + $output = [IO.File]::Open($Destination, [IO.FileMode]::CreateNew, [IO.FileAccess]::Write, [IO.FileShare]::None) + try { + $buffer = [byte[]]::new(32768) + $total = 0L + while (($count = $input.Read($buffer, 0, $buffer.Length)) -gt 0) { + $total += $count + if ($total -gt 1048576) { throw 'Pinned Git license exceeds the size limit.' } + $output.Write($buffer, 0, $count) + } + } finally { + $output.Dispose() + $input.Dispose() + } + } finally { + $archive.Dispose() + } +} + +function Assert-LocalLicenseIdentity([string]$ComponentId, [string]$Path) { + if ($ComponentId -ceq 'vc-runtime') { + $stream = $null + $reader = $null + try { + $settings = [Xml.XmlReaderSettings]::new() + $settings.DtdProcessing = [Xml.DtdProcessing]::Prohibit + $settings.XmlResolver = $null + $stream = [IO.MemoryStream]::new((Get-ZipFullEntryBytes $Path 'word/document.xml' 2097152), $false) + $reader = [Xml.XmlReader]::Create($stream, $settings) + $document = [Xml.XmlDocument]::new() + $document.XmlResolver = $null + $document.Load($reader) + } catch { + throw "VC runtime license is not the expected official DOCX: $($_.Exception.Message)" + } finally { + if ($null -ne $reader) { $reader.Dispose() } + if ($null -ne $stream) { $stream.Dispose() } + } + if ($null -eq $document.DocumentElement) { + throw 'VC runtime license DOCX has no document element.' + } + $text = [string]$document.DocumentElement.InnerText + if ([string]::IsNullOrWhiteSpace($text)) { + throw 'VC runtime license DOCX has no readable text.' + } + foreach ($marker in @('Visual C++', 'Redistributable', 'Runtime')) { + if ($text.IndexOf($marker, [StringComparison]::OrdinalIgnoreCase) -lt 0) { + throw "VC runtime license is missing the expected '$marker' marker." + } + } + return + } + + $content = Get-Content -Raw -LiteralPath $Path + $identityContent = [Regex]::Replace($content, '\s+', ' ') + switch -CaseSensitive ($ComponentId) { + 'proxifyre' { $markers = @('GNU AFFERO GENERAL PUBLIC LICENSE') } + 'windows-packet-filter' { $markers = @('MIT License') } + 'sing-box' { + $markers = @( + 'GNU GENERAL PUBLIC LICENSE', + 'In addition, no derivative work may use the name or imply association with this application without prior consent.' + ) + } + 'winsw' { $markers = @('MIT License') } + default { throw "Unknown license identity: $ComponentId" } + } + foreach ($marker in $markers) { + if ($identityContent.IndexOf($marker, [StringComparison]::OrdinalIgnoreCase) -lt 0) { + throw "License identity mismatch for $ComponentId." + } + } +} + +function Get-MsiSummaryTemplate([string]$Path) { + $installer = $null + $database = $null + $summary = $null + try { + $installer = New-Object -ComObject WindowsInstaller.Installer + $database = $installer.OpenDatabase($Path, 0) + $summary = $database.SummaryInformation(0) + return [string]$summary.Property(7) + } finally { + foreach ($value in @($summary, $database, $installer)) { + if ($null -ne $value -and [Runtime.InteropServices.Marshal]::IsComObject($value)) { + [void][Runtime.InteropServices.Marshal]::FinalReleaseComObject($value) + } + } + } +} + +function Assert-LocalPackageIdentity([string]$ComponentId, [string]$Path) { + switch ($ComponentId) { + 'proxifyre' { + Assert-ZipEntries $Path @('ProxiFyre.exe', 'socksify.dll') + Assert-PeBytesMachineX64 (Get-ZipEntryBytes $Path 'ProxiFyre.exe') 'ProxiFyre.exe' + Assert-PeBytesMachineX64 (Get-ZipEntryBytes $Path 'socksify.dll') 'socksify.dll' + } + 'windows-packet-filter' { + if ( + (Get-MsiProperty $Path 'ProductVersion') -cne '3.6.2.1' -or + (Get-MsiProperty $Path 'ProductName') -cne 'Windows Packet Filter x64' -or + (Get-MsiProperty $Path 'Manufacturer') -cne 'NT KERNEL' -or + (Get-MsiSummaryTemplate $Path) -cnotmatch '^x64;' + ) { throw 'Windows Packet Filter MSI local identity mismatch.' } + } + 'vc-runtime' { + Assert-FileVersion $Path '14.51.36247.0' '14.51.36247.0' 'Microsoft Visual C++ v14 Redistributable (x64) - 14.51.36247' + } + 'sing-box' { + Assert-ZipEntries $Path @('sing-box.exe') + Assert-PeBytesMachineX64 (Get-ZipEntryBytes $Path 'sing-box.exe') 'sing-box.exe' + } + 'winsw' { + Assert-ManagedAnyCpu $Path + Assert-FileVersion $Path '2.12.0.0' '2.12.0+eef5bade59fca0254e387ac73ed7625ba6aa7147' + } + default { throw "Unknown local package identity: $ComponentId" } + } +} + +function Assert-ZipEntries( + [string]$Path, + [string[]]$RequiredLeafNames, + [Int64]$MaxExpandedBytes = 536870912 +) { + Add-Type -AssemblyName System.IO.Compression.FileSystem + $archive = [IO.Compression.ZipFile]::OpenRead($Path) + try { + $leafNames = @() + $seenNames = @{} + $expandedBytes = 0L + if ($archive.Entries.Count -gt 10000) { + throw 'Archive contains too many entries.' + } + foreach ($entry in $archive.Entries) { + $name = $entry.FullName.Replace('\', '/') + $trimmedName = $name.TrimEnd('/') + $segments = @($trimmedName.Split('/')) + if ( + [string]::IsNullOrEmpty($trimmedName) -or + $name.StartsWith('/') -or + $name.Contains(':') -or + @($segments | Where-Object { + $_.Length -eq 0 -or $_ -in @('.', '..') -or $_.Length -gt 128 -or + $_.EndsWith('.') -or $_.EndsWith(' ') -or (Test-WindowsReservedName $_) -or + $_ -notmatch '^[A-Za-z0-9._+ -]+$' + }).Count -gt 0 + ) { + throw "Archive contains an unsafe entry: $name" + } + $normalized = $trimmedName.ToLowerInvariant() + if ($seenNames.ContainsKey($normalized)) { + throw "Archive contains a duplicate entry: $name" + } + $seenNames[$normalized] = $true + if ($entry.Length -lt 0 -or $expandedBytes -gt ($MaxExpandedBytes - $entry.Length)) { + throw 'Archive exceeds the expanded size limit.' + } + $expandedBytes += $entry.Length + if (-not [string]::IsNullOrEmpty($entry.Name)) { + $leafNames += $entry.Name + } + } + foreach ($required in $RequiredLeafNames) { + if (@($leafNames | Where-Object { $_ -ceq $required }).Count -ne 1) { + throw "Archive must contain exactly one $required." + } + } + } finally { + $archive.Dispose() + } +} + +function Get-UniqueFile([string]$Root, [string]$Name) { + $matches = @(Get-ChildItem -LiteralPath $Root -Recurse -File | Where-Object { $_.Name -ceq $Name }) + if ($matches.Count -ne 1) { + throw "Expected exactly one $Name in the archive." + } + return $matches[0].FullName +} + +function Get-MsiProperty([string]$Path, [string]$Name) { + $installer = $null + $database = $null + $view = $null + $record = $null + try { + $installer = New-Object -ComObject WindowsInstaller.Installer + $database = $installer.OpenDatabase($Path, 0) + $query = "SELECT ``Value`` FROM ``Property`` WHERE ``Property``='$Name'" + $view = $database.OpenView($query) + $view.Execute() + $record = $view.Fetch() + if ($null -eq $record) { + throw "MSI property is missing: $Name" + } + return [string]$record.StringData(1) + } finally { + foreach ($value in @($record, $view, $database, $installer)) { + if ($null -ne $value -and [Runtime.InteropServices.Marshal]::IsComObject($value)) { + [void][Runtime.InteropServices.Marshal]::FinalReleaseComObject($value) + } + } + } +} + +function Assert-FileVersion( + [string]$Path, + [string]$FileVersion, + [string]$ProductVersion, + [string]$ProductName = '', + [string]$CompanyName = '' +) { + $info = [Diagnostics.FileVersionInfo]::GetVersionInfo($Path) + if ($info.FileVersion.Trim() -cne $FileVersion -or $info.ProductVersion.Trim() -cne $ProductVersion) { + throw "Version metadata mismatch for $(Split-Path -Leaf $Path)." + } + if (-not [string]::IsNullOrEmpty($ProductName) -and $info.ProductName.Trim() -cne $ProductName) { + throw "Product name mismatch for $(Split-Path -Leaf $Path)." + } + if (-not [string]::IsNullOrEmpty($CompanyName) -and $info.CompanyName.Trim() -cne $CompanyName) { + throw "Company name mismatch for $(Split-Path -Leaf $Path)." + } +} + +function Write-DeterministicJson([object]$Value, [string]$Path) { + $json = ($Value | ConvertTo-Json -Depth 20).Replace("`r`n", "`n") + "`n" + [IO.File]::WriteAllText($Path, $json, [Text.UTF8Encoding]::new($false)) +} + +function Test-DirectoryContentEqual([string]$First, [string]$Second) { + if (-not (Test-Path -LiteralPath $First -PathType Container) -or -not (Test-Path -LiteralPath $Second -PathType Container)) { + return $false + } + try { + [void](Test-ComponentBundle $First) + [void](Test-ComponentBundle $Second) + } catch { + return $false + } + $firstFiles = @{} + foreach ($file in Get-ChildItem -LiteralPath $First -Recurse -File) { + $relative = Get-RelativeBundlePath ([IO.Path]::GetFullPath($First)) $file.FullName + $firstFiles[$relative] = "{0}:{1}" -f $file.Length, (Get-FileHash -LiteralPath $file.FullName -Algorithm SHA256).Hash + } + $secondFiles = @{} + foreach ($file in Get-ChildItem -LiteralPath $Second -Recurse -File) { + $relative = Get-RelativeBundlePath ([IO.Path]::GetFullPath($Second)) $file.FullName + $secondFiles[$relative] = "{0}:{1}" -f $file.Length, (Get-FileHash -LiteralPath $file.FullName -Algorithm SHA256).Hash + } + if ($firstFiles.Count -ne $secondFiles.Count) { + return $false + } + foreach ($name in $firstFiles.Keys) { + if (-not $secondFiles.ContainsKey($name) -or $firstFiles[$name] -ne $secondFiles[$name]) { + return $false + } + } + return $true +} + +if ($PlanOnly -and $CheckOnly) { + throw '-PlanOnly and -CheckOnly are mutually exclusive.' +} +if ($CheckOnly -and $UseFrozenReleaseEvidence) { + throw '-UseFrozenReleaseEvidence is not applicable to local-only CheckOnly validation.' +} +if (($PlanOnly -or $CheckOnly) -and $SimulateFailure -ne 'None') { + throw '-SimulateFailure is only available for the update path.' +} + +$resolvedOutputDir = [IO.Path]::GetFullPath($OutputDir) +if ($resolvedOutputDir -eq [IO.Path]::GetPathRoot($resolvedOutputDir)) { + throw 'OutputDir must not be a filesystem root.' +} +$canonicalOutputDir = [IO.Path]::GetFullPath((Join-Path $PSScriptRoot '..\src-tauri\bundled\components')) +$tempRoot = [IO.Path]::GetFullPath([IO.Path]::GetTempPath()).TrimEnd('\', '/') +$outputLeaf = Split-Path -Leaf $resolvedOutputDir +$isCanonicalOutput = [string]::Equals($resolvedOutputDir, $canonicalOutputDir, [StringComparison]::OrdinalIgnoreCase) +$isTestOutput = ( + [string]::Equals((Split-Path -Parent $resolvedOutputDir).TrimEnd('\', '/'), $tempRoot, [StringComparison]::OrdinalIgnoreCase) -and + $outputLeaf -match '^proxywarden-component-bundle-test-[0-9a-f]{32}$' +) +if (-not $isCanonicalOutput -and -not $isTestOutput) { + throw 'OutputDir must be the canonical bundle or an isolated ProxyWarden test directory under the system temp root.' +} +if ($SimulateFailure -ne 'None' -and -not $isTestOutput) { + throw '-SimulateFailure is allowed only with an isolated test OutputDir.' +} +$releaseEvidenceLabel = if ($UseFrozenReleaseEvidence) { 'frozen-audited-2026-08-17' } else { 'live-official-api' } + +if ($PlanOnly) { + ConvertTo-ResultJson ([ordered]@{ + mode = 'plan' + changed = $false + network = $false + writes = $false + releaseEvidence = $releaseEvidenceLabel + schemaVersion = 1 + targetArch = 'x64' + outputDir = $resolvedOutputDir + components = @($ExpectedComponents | ForEach-Object { [ordered]@{ id = $_.id; version = $_.version } }) + }) + return +} + +if ($CheckOnly) { + $catalog = Test-ComponentBundle $resolvedOutputDir + ConvertTo-ResultJson ([ordered]@{ + mode = 'check' + changed = $false + network = $false + writes = $false + releaseEvidence = 'local-bundle-only' + valid = $true + schemaVersion = $catalog.schemaVersion + targetArch = $catalog.targetArch + outputDir = $resolvedOutputDir + componentCount = @($catalog.components).Count + }) + return +} + +$outputParent = Split-Path -Parent $resolvedOutputDir +Assert-ValidatedParent $outputParent +if (Test-Path -LiteralPath $resolvedOutputDir) { + if (-not (Test-Path -LiteralPath $resolvedOutputDir -PathType Container)) { + throw 'OutputDir exists but is not a directory.' + } + Assert-NoReparseTree $resolvedOutputDir +} + +$operationId = [Guid]::NewGuid().ToString('N') +$stagingDir = Join-Path $outputParent ".proxywarden-components-staging-$operationId" +$backupDir = "$resolvedOutputDir.previous" +$retiredBackupDir = Join-Path $outputParent ".proxywarden-components-previous-$operationId" +$lockPath = "$resolvedOutputDir.update.lock" +$lock = $null +$lockOwned = $false +$activeWasEmptyPlaceholder = $false +$backupWasEmptyPlaceholder = $false +$activeMoved = $false +$oldBackupMoved = $false +$stagingMoved = $false +$promoted = $false +$preserveRecoveryArtifacts = $false +$script:InjectDownloadFailure = ($SimulateFailure -eq 'Download') + +try { + $lock = [IO.File]::Open($lockPath, [IO.FileMode]::CreateNew, [IO.FileAccess]::ReadWrite, [IO.FileShare]::None) + $lockOwned = $true + if (Test-Path -LiteralPath $resolvedOutputDir) { + if (Test-SafeEmptyDirectory $resolvedOutputDir) { + $activeWasEmptyPlaceholder = $true + } else { + [void](Test-ComponentBundle $resolvedOutputDir) + } + } + if (Test-Path -LiteralPath $backupDir) { + if (-not (Test-Path -LiteralPath $backupDir -PathType Container)) { + throw 'The previous bundle backup is not a directory.' + } + if (Test-SafeEmptyDirectory $backupDir) { + $backupWasEmptyPlaceholder = $true + } else { + [void](Test-ComponentBundle $backupDir) + } + } + [void](New-Item -ItemType Directory -Path $stagingDir) + + $proxifyreName = 'ProxiFyre-v2.4.0-x64-signed.zip' + $proxifyreUrl = "https://github.com/wiresock/proxifyre/releases/download/v2.4.0/$proxifyreName" + $proxifyrePath = Join-Path $stagingDir "proxifyre\$proxifyreName" + $proxifyreRelease = Get-ReleaseEvidence 'wiresock/proxifyre' 'v2.4.0' ([bool]$UseFrozenReleaseEvidence) + $proxifyreAsset = Save-GitHubDigestAsset $proxifyreRelease $proxifyreName $proxifyreUrl $proxifyrePath 'eab65fd7d8eeb716abedb5614618c641de3f9eb8326b99cee1da787141e30cac' 1519694 + + $packetFilterName = 'Windows.Packet.Filter.3.6.2.1.x64.msi' + $packetFilterUrl = "https://github.com/wiresock/ndisapi/releases/download/v3.6.2/$packetFilterName" + $packetFilterPath = Join-Path $stagingDir "windows-packet-filter\$packetFilterName" + $packetFilterRelease = Get-ReleaseEvidence 'wiresock/ndisapi' 'v3.6.2' ([bool]$UseFrozenReleaseEvidence) + $packetFilterAsset = Save-GitHubDigestAsset $packetFilterRelease $packetFilterName $packetFilterUrl $packetFilterPath '9c388c0b7f189f7fa98720bae2caecf7d64f30910838b80b438ecf8956b8502c' 819200 + + $vcName = 'VC_redist.x64.exe' + $vcUrl = 'https://aka.ms/vs/18/release/14.51.36247/VC_redist.x64.exe' + $vcPath = Join-Path $stagingDir "vc-runtime\$vcName" + $vcAsset = Save-PinnedAsset $vcUrl $vcPath '843068991daaa1f73ad9f6239bce4d0f6a07a51f18c37ea2a867e9beca71295c' 18731856 + + $singBoxName = 'sing-box-1.13.19-windows-amd64.zip' + $singBoxUrl = "https://github.com/SagerNet/sing-box/releases/download/v1.13.19/$singBoxName" + $singBoxPath = Join-Path $stagingDir "sing-box\$singBoxName" + $singBoxRelease = Get-ReleaseEvidence 'SagerNet/sing-box' 'v1.13.19' ([bool]$UseFrozenReleaseEvidence) + $singBoxAsset = Save-GitHubDigestAsset $singBoxRelease $singBoxName $singBoxUrl $singBoxPath 'e011a4def2f5e2b143ed54adb2b1a20a6be407806ab4442f3667f1dd817a2c8d' 21046252 + + $winswName = 'WinSW.NET461.exe' + $winswUrl = "https://github.com/winsw/winsw/releases/download/v2.12.0/$winswName" + $winswPath = Join-Path $stagingDir "winsw\$winswName" + $winswRelease = Get-ReleaseEvidence 'winsw/winsw' 'v2.12.0' ([bool]$UseFrozenReleaseEvidence) + $winswReleaseAsset = Get-ReleaseAsset $winswRelease $winswName + $winswDigestProperty = $winswReleaseAsset.PSObject.Properties['digest'] + if ( + $winswReleaseAsset.browser_download_url -cne $winswUrl -or + [Int64]$winswReleaseAsset.size -ne 655872 -or + ($null -ne $winswDigestProperty -and -not [string]::IsNullOrWhiteSpace([string]$winswDigestProperty.Value)) + ) { + throw 'Official WinSW asset identity changed.' + } + $winswAsset = Save-PinnedAsset $winswUrl $winswPath 'b5066b7bbdfba1293e5d15cda3caaea88fbeab35bd5b38c41c913d492aadfc4f' 655872 + + $licenseSources = Join-Path $stagingDir '.license-sources' + [void](New-Item -ItemType Directory -Path $licenseSources) + try { + Save-LicenseFromPinnedGit 'https://github.com/wiresock/proxifyre.git' 'proxifyre' 'v2.4.0' 'dd1512840e1e3bc596b06b80eda4e2dcd6a9c9ed' 'dd1512840e1e3bc596b06b80eda4e2dcd6a9c9ed' 'LICENSE' (Join-Path $stagingDir 'proxifyre\LICENSE') $licenseSources + Save-LicenseFromPinnedGit 'https://github.com/wiresock/ndisapi.git' 'ndisapi' 'v3.6.2' '417b8734e844083a10236387fba705d94a2d6bc9' '417b8734e844083a10236387fba705d94a2d6bc9' 'LICENSE' (Join-Path $stagingDir 'windows-packet-filter\LICENSE') $licenseSources + Save-LicenseFromPinnedGit 'https://github.com/SagerNet/sing-box.git' 'sing-box' 'v1.13.19' 'b5ebaa1fc0f2b94256180b95468e73ef53caa27d' 'b5ebaa1fc0f2b94256180b95468e73ef53caa27d' 'LICENSE' (Join-Path $stagingDir 'sing-box\LICENSE') $licenseSources + Save-LicenseFromPinnedGit 'https://github.com/winsw/winsw.git' 'winsw' 'v2.12.0' 'eef5bade59fca0254e387ac73ed7625ba6aa7147' 'eef5bade59fca0254e387ac73ed7625ba6aa7147' 'LICENSE.txt' (Join-Path $stagingDir 'winsw\LICENSE.txt') $licenseSources + } finally { + Remove-SafeGeneratedDirectory $licenseSources $stagingDir '^\.license-sources$' + } + Save-Download 'https://visualstudio.microsoft.com/wp-content/uploads/2025/10/Visual-C-V14-License-Redistributable_and_Runtime_ENU.docx' (Join-Path $stagingDir 'vc-runtime\LICENSE.docx') 5242880 + + Assert-ZipEntries $proxifyrePath @('ProxiFyre.exe', 'socksify.dll') + Assert-ZipEntries $singBoxPath @('sing-box.exe') + $verificationRoot = Join-Path $stagingDir '.verification' + [void](New-Item -ItemType Directory -Path $verificationRoot) + try { + $proxifyreExtract = Join-Path $verificationRoot 'proxifyre' + Expand-Archive -LiteralPath $proxifyrePath -DestinationPath $proxifyreExtract + $proxifyreExe = Get-UniqueFile $proxifyreExtract 'ProxiFyre.exe' + $socksifyDll = Get-UniqueFile $proxifyreExtract 'socksify.dll' + Assert-AuthenticodePublisher $proxifyreExe 'The Anti-Cloud Corporation' + Assert-AuthenticodePublisher $socksifyDll 'The Anti-Cloud Corporation' + Assert-PeMachineX64 $proxifyreExe + Assert-PeMachineX64 $socksifyDll + Assert-FileVersion $proxifyreExe '2.4.0' '2.4.0' 'ProxiFyre' 'NT KERNEL' + + $singBoxExtract = Join-Path $verificationRoot 'sing-box' + Expand-Archive -LiteralPath $singBoxPath -DestinationPath $singBoxExtract + Assert-PeMachineX64 (Get-UniqueFile $singBoxExtract 'sing-box.exe') + } finally { + try { + Remove-SafeGeneratedDirectory $verificationRoot $stagingDir '^\.verification$' + } catch { + throw "Could not safely remove the package verification directory: $($_.Exception.Message)" + } + } + + Assert-AuthenticodePublisher $packetFilterPath 'The Anti-Cloud Corporation' + if ( + (Get-MsiProperty $packetFilterPath 'ProductVersion') -cne '3.6.2.1' -or + (Get-MsiProperty $packetFilterPath 'ProductName') -cne 'Windows Packet Filter x64' -or + (Get-MsiProperty $packetFilterPath 'Manufacturer') -cne 'NT KERNEL' + ) { + throw 'Windows Packet Filter MSI product identity mismatch.' + } + Assert-AuthenticodePublisher $vcPath 'Microsoft Corporation' + Assert-FileVersion $vcPath '14.51.36247.0' '14.51.36247.0' 'Microsoft Visual C++ v14 Redistributable (x64) - 14.51.36247' + Assert-Unsigned $winswPath + Assert-ManagedAnyCpu $winswPath + Assert-FileVersion $winswPath '2.12.0.0' '2.12.0+eef5bade59fca0254e387ac73ed7625ba6aa7147' + + Assert-LocalLicenseIdentity 'proxifyre' (Join-Path $stagingDir 'proxifyre\LICENSE') + Assert-LocalLicenseIdentity 'windows-packet-filter' (Join-Path $stagingDir 'windows-packet-filter\LICENSE') + Assert-LocalLicenseIdentity 'vc-runtime' (Join-Path $stagingDir 'vc-runtime\LICENSE.docx') + Assert-LocalLicenseIdentity 'sing-box' (Join-Path $stagingDir 'sing-box\LICENSE') + Assert-LocalLicenseIdentity 'winsw' (Join-Path $stagingDir 'winsw\LICENSE.txt') + + $catalog = [ordered]@{ + schemaVersion = 1 + targetArch = 'x64' + components = @( + [ordered]@{ + id = 'proxifyre'; version = '2.4.0'; fileVersion = '2.4.0'; productVersion = '2.4.0' + assetPath = "proxifyre/$proxifyreName"; assetArch = 'x64'; effectiveTarget = 'x64' + sha256 = $proxifyreAsset.hash; size = $proxifyreAsset.size; sourceUrl = $proxifyreUrl + license = [ordered]@{ id = 'AGPL-3.0-only'; path = 'proxifyre/LICENSE' } + installRole = 'proxifyre-runtime' + updateTrustPolicy = [ordered]@{ + type = 'githubReleaseDigest'; repository = 'wiresock/proxifyre'; tagPattern = 'v*' + assetPattern = 'ProxiFyre-v*-x64-signed.zip'; requireStable = $true + authenticodePublishers = @('The Anti-Cloud Corporation') + } + } + [ordered]@{ + id = 'windows-packet-filter'; version = '3.6.2'; fileVersion = '3.6.2.1'; productVersion = '3.6.2.1' + assetPath = "windows-packet-filter/$packetFilterName"; assetArch = 'x64'; effectiveTarget = 'x64' + sha256 = $packetFilterAsset.hash; size = $packetFilterAsset.size; sourceUrl = $packetFilterUrl + license = [ordered]@{ id = 'MIT'; path = 'windows-packet-filter/LICENSE' } + installRole = 'packet-filter-driver' + updateTrustPolicy = [ordered]@{ + type = 'githubReleaseDigest'; repository = 'wiresock/ndisapi'; tagPattern = 'v*' + assetPattern = 'Windows.Packet.Filter.*.x64.msi'; requireStable = $true + authenticodePublishers = @('The Anti-Cloud Corporation') + } + } + [ordered]@{ + id = 'vc-runtime'; version = '14.51.36247.0'; fileVersion = '14.51.36247.0'; productVersion = '14.51.36247.0' + assetPath = "vc-runtime/$vcName"; assetArch = 'x64'; effectiveTarget = 'x64' + sha256 = $vcAsset.hash; size = $vcAsset.size; sourceUrl = $vcUrl + license = [ordered]@{ id = 'LicenseRef-Microsoft-Visual-Cpp-v14-Redistributable-2026'; path = 'vc-runtime/LICENSE.docx' } + installRole = 'vc-runtime-prerequisite' + updateTrustPolicy = [ordered]@{ + type = 'buildTimeOnlyAuthenticode'; allowedSourceHosts = @('aka.ms') + assetPattern = 'VC_redist.x64.exe'; publishers = @('Microsoft Corporation') + } + } + [ordered]@{ + id = 'sing-box'; version = '1.13.19' + assetPath = "sing-box/$singBoxName"; assetArch = 'x64'; effectiveTarget = 'x64' + sha256 = $singBoxAsset.hash; size = $singBoxAsset.size; sourceUrl = $singBoxUrl + license = [ordered]@{ id = 'LicenseRef-Sing-Box-Project'; path = 'sing-box/LICENSE' } + installRole = 'sing-box-runtime' + updateTrustPolicy = [ordered]@{ + type = 'githubReleaseDigest'; repository = 'SagerNet/sing-box'; tagPattern = 'v*' + assetPattern = 'sing-box-*-windows-amd64.zip'; requireStable = $true + } + } + [ordered]@{ + id = 'winsw'; version = '2.12.0'; fileVersion = '2.12.0.0' + productVersion = '2.12.0+eef5bade59fca0254e387ac73ed7625ba6aa7147' + assetPath = "winsw/$winswName"; assetArch = 'anycpu'; effectiveTarget = 'x64' + sha256 = $winswAsset.hash; size = $winswAsset.size; sourceUrl = $winswUrl + license = [ordered]@{ id = 'MIT'; path = 'winsw/LICENSE.txt' } + installRole = 'sing-box-service-wrapper' + updateTrustPolicy = [ordered]@{ + type = 'bundledOnlyNoIndependentProof' + reason = 'The official v2.12.0 asset is unsigned and has no independent release digest; runtime network update is disabled.' + } + } + ) + } + Write-DeterministicJson $catalog (Join-Path $stagingDir 'catalog.json') + + if ($SimulateFailure -eq 'Validation') { + $corrupt = [IO.File]::Open($proxifyrePath, [IO.FileMode]::Open, [IO.FileAccess]::ReadWrite, [IO.FileShare]::None) + try { + $firstByte = $corrupt.ReadByte() + $corrupt.Position = 0 + $corrupt.WriteByte([byte]($firstByte -bxor 0xff)) + } finally { + $corrupt.Dispose() + } + } + [void](Test-ComponentBundle $stagingDir) + + if ($SimulateFailure -ne 'Promotion' -and (Test-DirectoryContentEqual $resolvedOutputDir $stagingDir)) { + if ($backupWasEmptyPlaceholder -and (Test-Path -LiteralPath $backupDir)) { + Remove-SafeEmptyDirectory $backupDir $backupDir + $backupWasEmptyPlaceholder = $false + } + ConvertTo-ResultJson ([ordered]@{ + mode = 'update'; changed = $false; schemaVersion = 1; targetArch = 'x64' + outputDir = $resolvedOutputDir; componentCount = 5; backupDir = $null + releaseEvidence = $releaseEvidenceLabel + }) + return + } + + if (Test-Path -LiteralPath $backupDir) { + if (-not (Test-Path -LiteralPath $backupDir -PathType Container)) { + throw 'The previous bundle backup is not a directory.' + } + if ($backupWasEmptyPlaceholder) { + if (-not (Test-SafeEmptyDirectory $backupDir)) { + throw 'The empty previous bundle placeholder changed during the update.' + } + } else { + [void](Test-ComponentBundle $backupDir) + } + [IO.Directory]::Move($backupDir, $retiredBackupDir) + $oldBackupMoved = $true + } + if (Test-Path -LiteralPath $resolvedOutputDir) { + if ($activeWasEmptyPlaceholder) { + if (-not (Test-SafeEmptyDirectory $resolvedOutputDir)) { + throw 'The empty active bundle placeholder changed during the update.' + } + } else { + [void](Test-ComponentBundle $resolvedOutputDir) + } + [IO.Directory]::Move($resolvedOutputDir, $backupDir) + $activeMoved = $true + } + [IO.Directory]::Move($stagingDir, $resolvedOutputDir) + $stagingMoved = $true + if ($SimulateFailure -eq 'Promotion') { + throw 'Simulated bundle promotion failure after activating the candidate.' + } + [void](Test-ComponentBundle $resolvedOutputDir) + if ($activeMoved -and $activeWasEmptyPlaceholder) { + Remove-SafeEmptyDirectory $backupDir $backupDir + $activeMoved = $false + $activeWasEmptyPlaceholder = $false + } + $promoted = $true + if ($oldBackupMoved -and (Test-Path -LiteralPath $retiredBackupDir)) { + try { + Remove-SafeGeneratedDirectory $retiredBackupDir $outputParent '^\.proxywarden-components-previous-[0-9a-f]{32}$' + } catch { + # The new active bundle and its immediate backup are already valid. Preserve an older + # recovery directory if safe cleanup cannot be proven. + } + if (-not (Test-Path -LiteralPath $retiredBackupDir)) { + $oldBackupMoved = $false + } + } + + ConvertTo-ResultJson ([ordered]@{ + mode = 'update'; changed = $true; schemaVersion = 1; targetArch = 'x64' + outputDir = $resolvedOutputDir; componentCount = 5 + backupDir = $(if ($activeMoved) { $backupDir } else { $null }) + releaseEvidence = $releaseEvidenceLabel + }) +} catch { + $updateError = $_ + try { + if (-not $promoted) { + if ($stagingMoved) { + if (-not (Test-Path -LiteralPath $resolvedOutputDir) -or (Test-Path -LiteralPath $stagingDir)) { + throw 'Cannot preserve the failed candidate before rollback.' + } + [IO.Directory]::Move($resolvedOutputDir, $stagingDir) + $stagingMoved = $false + } + if ($activeMoved) { + if ((Test-Path -LiteralPath $resolvedOutputDir) -or -not (Test-Path -LiteralPath $backupDir)) { + throw 'Cannot restore the previous active bundle.' + } + [IO.Directory]::Move($backupDir, $resolvedOutputDir) + $activeMoved = $false + } + if ($oldBackupMoved) { + if ((Test-Path -LiteralPath $backupDir) -or -not (Test-Path -LiteralPath $retiredBackupDir)) { + throw 'Cannot restore the older recovery bundle.' + } + [IO.Directory]::Move($retiredBackupDir, $backupDir) + $oldBackupMoved = $false + } + } + } catch { + $preserveRecoveryArtifacts = $true + throw [InvalidOperationException]::new( + "Component bundle update failed and rollback could not be completed. Recovery artifacts were preserved. $($_.Exception.Message)", + $_.Exception + ) + } + throw $updateError +} finally { + if ($lockOwned) { + if ($null -ne $lock) { + $lock.Dispose() + } + if (-not $preserveRecoveryArtifacts -and (Test-Path -LiteralPath $lockPath)) { + $lockItem = Get-Item -LiteralPath $lockPath -Force -ErrorAction SilentlyContinue + if ($null -ne $lockItem -and ($lockItem.Attributes -band [IO.FileAttributes]::ReparsePoint) -eq 0) { + Remove-Item -LiteralPath $lockPath -Force -ErrorAction SilentlyContinue + } + } + $lockOwned = $false + } + if (-not $preserveRecoveryArtifacts -and (Test-Path -LiteralPath $stagingDir)) { + try { + Remove-SafeGeneratedDirectory $stagingDir $outputParent '^\.proxywarden-components-staging-[0-9a-f]{32}$' + } catch { + # Refuse unsafe recursive cleanup and leave the generated directory for inspection. + } + } +} diff --git a/scripts/update-proxifyre-bundle.ps1 b/scripts/update-proxifyre-bundle.ps1 deleted file mode 100644 index c050d71..0000000 --- a/scripts/update-proxifyre-bundle.ps1 +++ /dev/null @@ -1,143 +0,0 @@ -param( - [string]$OutputDir = (Join-Path $PSScriptRoot '..\src-tauri\bundled\proxifyre'), - [ValidateSet('x64', 'x86', 'ARM64')] - [string[]]$Architectures = @('x64'), - [switch]$SkipVcRuntime -) - -$ErrorActionPreference = 'Stop' -Set-StrictMode -Version Latest -$ProgressPreference = 'SilentlyContinue' - -function Invoke-JsonApi([string]$Uri) { - Invoke-RestMethod -Uri $Uri -Headers @{ - 'User-Agent' = 'proxywarden-bundle-updater' - 'Accept' = 'application/vnd.github+json' - } -TimeoutSec 60 -MaximumRedirection 10 -} - -function Invoke-FileDownload([string]$Uri, [string]$Path) { - $partialPath = "$Path.part" - Remove-Item -LiteralPath $partialPath -Force -ErrorAction SilentlyContinue - - try { - Invoke-WebRequest -UseBasicParsing -Uri $Uri -OutFile $partialPath -Headers @{ - 'User-Agent' = 'proxywarden-bundle-updater' - 'Accept' = 'application/octet-stream,*/*' - } -TimeoutSec 240 -MaximumRedirection 10 - } catch { - Remove-Item -LiteralPath $partialPath -Force -ErrorAction SilentlyContinue - throw - } - - $item = Get-Item -LiteralPath $partialPath - if ($item.Length -le 0) { - Remove-Item -LiteralPath $partialPath -Force -ErrorAction SilentlyContinue - throw "Downloaded file is empty: $Uri" - } - - Move-Item -LiteralPath $partialPath -Destination $Path -Force -} - -function Select-ReleaseAsset($Release, [string]$Pattern, [string]$Label) { - $asset = $Release.assets | Where-Object { $_.name -match $Pattern } | Select-Object -First 1 - if ($null -eq $asset) { - throw "No asset found for $Label using pattern $Pattern" - } - - $asset -} - -function Save-Asset([string]$Id, [string]$Name, [string]$Url, [string]$ExpectedDigest = '') { - $path = Join-Path $OutputDir $Name - if (Test-Path -LiteralPath $path) { - $existing = Get-Item -LiteralPath $path - if ($existing.Length -gt 0) { - $existingHash = (Get-FileHash -LiteralPath $path -Algorithm SHA256).Hash.ToLowerInvariant() - $expectedHash = '' - if (-not [string]::IsNullOrWhiteSpace($ExpectedDigest) -and $ExpectedDigest -match '^sha256:(.+)$') { - $expectedHash = $Matches[1].ToLowerInvariant() - } - - if ([string]::IsNullOrWhiteSpace($expectedHash) -or $existingHash -eq $expectedHash) { - Write-Host "Using existing $Name" - return [PSCustomObject]@{ - id = $Id - name = $Name - sha256 = $existingHash - size = $existing.Length - sourceUrl = $Url - } - } - } - } - - Write-Host "Downloading $Name" - Invoke-FileDownload $Url $path - - $hash = (Get-FileHash -LiteralPath $path -Algorithm SHA256).Hash.ToLowerInvariant() - if (-not [string]::IsNullOrWhiteSpace($ExpectedDigest) -and $ExpectedDigest -match '^sha256:(.+)$') { - $expected = $Matches[1].ToLowerInvariant() - if ($hash -ne $expected) { - throw "SHA256 mismatch for $Name. Expected $expected, got $hash." - } - } - - [PSCustomObject]@{ - id = $Id - name = $Name - sha256 = $hash - size = (Get-Item -LiteralPath $path).Length - sourceUrl = $Url - } -} - -$resolvedOutputDir = [System.IO.Path]::GetFullPath($OutputDir) -New-Item -ItemType Directory -Force -Path $resolvedOutputDir | Out-Null -$OutputDir = $resolvedOutputDir - -$selectedArchitectures = $Architectures | - ForEach-Object { - if ($_ -eq 'ARM64') { 'ARM64' } elseif ($_ -eq 'x86') { 'x86' } else { 'x64' } - } | - Select-Object -Unique - -$proxifyreRelease = Invoke-JsonApi 'https://api.github.com/repos/wiresock/proxifyre/releases/latest' -$ndisapiRelease = Invoke-JsonApi 'https://api.github.com/repos/wiresock/ndisapi/releases/latest' - -$files = New-Object System.Collections.Generic.List[object] - -foreach ($arch in $selectedArchitectures) { - $proxifyreAsset = Select-ReleaseAsset $proxifyreRelease "ProxiFyre-.*-$arch-signed\.zip$" "ProxiFyre $arch" - $files.Add((Save-Asset "proxifyre-$($arch.ToLowerInvariant())" $proxifyreAsset.name $proxifyreAsset.browser_download_url $proxifyreAsset.digest)) - - $ndisAsset = Select-ReleaseAsset $ndisapiRelease "Windows\.Packet\.Filter\..*\.$arch\.msi$" "Windows Packet Filter $arch" - $files.Add((Save-Asset "packet-filter-$($arch.ToLowerInvariant())" $ndisAsset.name $ndisAsset.browser_download_url $ndisAsset.digest)) -} - -if (-not $SkipVcRuntime) { - if ($selectedArchitectures | Where-Object { $_ -ne 'x86' }) { - $files.Add((Save-Asset 'vc-runtime-x64' 'vc_redist.x64.exe' 'https://aka.ms/vc14/vc_redist.x64.exe')) - } - if ($selectedArchitectures -contains 'x86') { - $files.Add((Save-Asset 'vc-runtime-x86' 'vc_redist.x86.exe' 'https://aka.ms/vc14/vc_redist.x86.exe')) - } -} - -$manifest = [PSCustomObject]@{ - generatedAt = (Get-Date).ToUniversalTime().ToString('o') - architectures = @($selectedArchitectures) - proxifyreRelease = $proxifyreRelease.tag_name - windowsPacketFilterRelease = $ndisapiRelease.tag_name - files = $files -} - -$manifestPath = Join-Path $OutputDir 'manifest.json' -$manifest | ConvertTo-Json -Depth 5 | Set-Content -LiteralPath $manifestPath -Encoding UTF8 - -$keepNames = @($files | ForEach-Object { $_.name }) + 'manifest.json' -Get-ChildItem -LiteralPath $OutputDir -File | - Where-Object { $keepNames -notcontains $_.Name } | - ForEach-Object { Remove-Item -LiteralPath $_.FullName -Force } - -Write-Host "Bundle updated: $OutputDir" diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 9f65409..9b79576 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -47,6 +47,15 @@ version = "1.0.103" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3" +[[package]] +name = "arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" +dependencies = [ + "derive_arbitrary", +] + [[package]] name = "atk" version = "0.18.2" @@ -543,6 +552,17 @@ dependencies = [ "serde_core", ] +[[package]] +name = "derive_arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + [[package]] name = "derive_more" version = "2.1.1" @@ -783,6 +803,7 @@ checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" dependencies = [ "crc32fast", "miniz_oxide", + "zlib-rs", ] [[package]] @@ -2314,20 +2335,24 @@ dependencies = [ [[package]] name = "proxywarden" -version = "1.1.0" +version = "2.0.0" dependencies = [ "base64 0.22.1", "percent-encoding", + "quick-xml", "reqwest 0.12.28", "serde", "serde_json", + "sha2", "tauri", "tauri-build", "tauri-plugin-dialog", "thiserror 2.0.18", "url", "uuid", + "windows-sys 0.61.2", "winreg", + "zip", ] [[package]] @@ -4847,6 +4872,25 @@ dependencies = [ "syn 2.0.118", ] +[[package]] +name = "zip" +version = "4.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caa8cd6af31c3b31c6631b8f483848b91589021b28fffe50adada48d4f4d2ed1" +dependencies = [ + "arbitrary", + "crc32fast", + "flate2", + "indexmap 2.14.0", + "memchr", +] + +[[package]] +name = "zlib-rs" +version = "0.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34b31d188d9d685a4f9c7b46d6e36631b07058d2cfe190267adce54dc230bf12" + [[package]] name = "zmij" version = "1.0.21" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 91872d5..d3b9c32 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "proxywarden" -version = "1.1.0" +version = "2.0.0" description = "Standalone Windows desktop proxy management app for ProxyWarden." authors = ["ProxyWarden"] edition = "2021" @@ -23,6 +23,26 @@ percent-encoding = "2" url = "2" uuid = { version = "1", features = ["v4"] } thiserror = "2" +sha2 = "0.10" +quick-xml = "0.39" +zip = { version = "4", default-features = false, features = ["deflate-flate2-zlib-rs"] } [target.'cfg(windows)'.dependencies] winreg = "0.55" +windows-sys = { version = "0.61.2", features = [ + "Win32_Foundation", + "Win32_Security", + "Win32_Security_Authorization", + "Win32_Security_Cryptography", + "Win32_Security_Cryptography_Catalog", + "Win32_Security_Cryptography_Sip", + "Win32_Security_WinTrust", + "Win32_Storage_FileSystem", + "Win32_System_Diagnostics_ToolHelp", + "Win32_System_Registry", + "Win32_System_Services", + "Win32_System_SystemInformation", + "Win32_System_Threading", + "Win32_UI_Shell", + "Win32_UI_WindowsAndMessaging", +] } diff --git a/src-tauri/bundled/cleanup/uninstall-managed-components.ps1 b/src-tauri/bundled/cleanup/uninstall-managed-components.ps1 deleted file mode 100644 index d277c01..0000000 --- a/src-tauri/bundled/cleanup/uninstall-managed-components.ps1 +++ /dev/null @@ -1,250 +0,0 @@ -param( - [string]$InstallRoot = "", - [switch]$ForceRemoveWindowsPacketFilter -) - -Set-StrictMode -Version Latest -$ErrorActionPreference = "Stop" - -function New-Result { - param( - [bool]$Success, - [string]$Message, - [hashtable]$Details = @{} - ) - - [ordered]@{ - success = $Success - message = $Message - details = $Details - } | ConvertTo-Json -Depth 8 -Compress -} - -function Get-FullPath([string]$Path) { - return [System.IO.Path]::GetFullPath($Path).TrimEnd("\") -} - -function Test-PathInside([string]$Path, [string]$Root) { - if ([string]::IsNullOrWhiteSpace($Path)) { return $false } - try { - $fullPath = Get-FullPath $Path - $fullRoot = Get-FullPath $Root - return $fullPath.StartsWith($fullRoot + "\", [StringComparison]::OrdinalIgnoreCase) - } catch { - return $false - } -} - -function Assert-SafeInstallRoot([string]$Root) { - if ([string]::IsNullOrWhiteSpace($Root)) { - throw "InstallRoot is empty." - } - - $full = Get-FullPath $Root - if ($full -match "^[A-Za-z]:\\?$") { - throw "Refusing to use drive root as InstallRoot: $full" - } - if ($full -match "\\Windows($|\\)" -or $full -match "\\ProgramData$" -or $full -match "\\Users$") { - throw "Refusing unsafe InstallRoot: $full" - } - - $knownAppFiles = @( - (Join-Path $full "proxywarden.exe"), - (Join-Path $full "uninstall.exe"), - (Join-Path $full "bundled\cleanup\uninstall-managed-components.ps1") - ) - foreach ($candidate in $knownAppFiles) { - if (Test-Path -LiteralPath $candidate) { return $full } - } - - throw "InstallRoot does not look like a ProxyWarden install directory: $full" -} - -function Resolve-SafeComponentDir([string]$Root, [string]$Leaf) { - $componentRoot = Join-Path $Root "components" - $path = Join-Path $componentRoot $Leaf - $full = Get-FullPath $path - $expectedParent = Get-FullPath $componentRoot - $actualLeaf = Split-Path -Leaf $full - - if ($actualLeaf -ne $Leaf) { - throw "Unexpected component directory leaf: $full" - } - if (-not $full.StartsWith($expectedParent + "\", [StringComparison]::OrdinalIgnoreCase)) { - throw "Component directory is outside ProxyWarden components root: $full" - } - - return $full -} - -function Read-ComponentMarker([string]$Dir) { - $markerPath = Join-Path $Dir "proxywarden-component.json" - if (-not (Test-Path -LiteralPath $markerPath)) { return $null } - try { - return Get-Content -LiteralPath $markerPath -Raw -Encoding UTF8 | ConvertFrom-Json - } catch { - return $null - } -} - -function Get-MarkerBool($Marker, [string]$Name) { - if ($null -eq $Marker) { return $false } - $property = $Marker.PSObject.Properties[$Name] - if ($null -eq $property) { return $false } - return [bool]$property.Value -} - -function Get-ServiceRecord([string]$Name) { - $escaped = $Name.Replace("'", "''") - return Get-CimInstance Win32_Service -Filter "Name='$escaped'" -ErrorAction SilentlyContinue -} - -function Get-ServiceImagePath($Record) { - if ($null -eq $Record -or [string]::IsNullOrWhiteSpace([string]$Record.PathName)) { - return $null - } - - $pathName = ([string]$Record.PathName).Trim() - if ($pathName -match '^"([^"]+)"') { return $Matches[1] } - if ($pathName -match '^(.+?\.exe)\b') { return $Matches[1].Trim() } - return $pathName -} - -function Stop-ServiceRecord($Record) { - if ($null -eq $Record) { return } - - $service = Get-Service -Name $Record.Name -ErrorAction SilentlyContinue - if ($null -ne $service -and $service.Status -ne "Stopped") { - Stop-Service -Name $service.Name -Force -ErrorAction SilentlyContinue - $service = Get-Service -Name $Record.Name -ErrorAction SilentlyContinue - if ($null -ne $service) { - try { $service.WaitForStatus("Stopped", [TimeSpan]::FromSeconds(12)) } catch {} - } - } - - $recordAfterStop = Get-ServiceRecord $Record.Name - if ($null -ne $recordAfterStop -and [int]$recordAfterStop.ProcessId -gt 0) { - taskkill.exe /PID ([int]$recordAfterStop.ProcessId) /F | Out-Null - Start-Sleep -Milliseconds 500 - } -} - -function Remove-ManagedService { - param( - [string[]]$Names, - [string]$InstallRoot, - [string]$UninstallExe = "" - ) - - $removed = @() - foreach ($name in $Names) { - $record = Get-ServiceRecord $name - if ($null -eq $record) { continue } - - $imagePath = Get-ServiceImagePath $record - if (-not [string]::IsNullOrWhiteSpace($imagePath) -and -not (Test-PathInside $imagePath $InstallRoot)) { - continue - } - - Stop-ServiceRecord $record - - if (-not [string]::IsNullOrWhiteSpace($UninstallExe) -and (Test-Path -LiteralPath $UninstallExe)) { - Push-Location (Split-Path -Parent $UninstallExe) - try { & $UninstallExe uninstall | Out-Null } finally { Pop-Location } - } - - $record = Get-ServiceRecord $name - if ($null -ne $record) { - sc.exe delete $name | Out-Null - } - $removed += $name - } - - return $removed -} - -function Remove-SafeDirectory([string]$Path, [string]$Root) { - if (-not (Test-Path -LiteralPath $Path)) { return $false } - if (-not (Test-PathInside $Path $Root)) { - throw "Refusing to remove directory outside InstallRoot: $Path" - } - Remove-Item -LiteralPath $Path -Recurse -Force - return $true -} - -function Remove-ManagedFirewallRules { - $removed = @() - foreach ($name in @("ProxyWarden.ProxiFyre.Inbound", "ProxyWarden.ProxiFyre.Outbound")) { - $rule = Get-NetFirewallRule -Name $name -ErrorAction SilentlyContinue - if ($null -eq $rule) { continue } - $rule | Remove-NetFirewallRule -ErrorAction Stop - $removed += $name - } - return $removed -} - -function Get-InstalledProgram([string]$Pattern) { - $paths = @( - "HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*", - "HKLM:\Software\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*" - ) - return Get-ItemProperty -Path $paths -ErrorAction SilentlyContinue | - Where-Object { $_.DisplayName -match $Pattern } | - Select-Object -First 1 DisplayName, DisplayVersion, PSChildName, UninstallString, QuietUninstallString -} - -function Resolve-MsiProductCode($Program, [string]$Label) { - if ($null -eq $Program) { return $null } - if ($Program.PSChildName -match "^\{[0-9A-Fa-f-]{36}\}$") { - return $Program.PSChildName - } - foreach ($candidate in @($Program.QuietUninstallString, $Program.UninstallString)) { - if ($candidate -match "\{[0-9A-Fa-f-]{36}\}") { - return $Matches[0] - } - } - throw "Could not resolve MSI product code for $Label." -} - -function Uninstall-MsiProgram($Program, [string]$Label) { - $productCode = Resolve-MsiProductCode $Program $Label - if ([string]::IsNullOrWhiteSpace($productCode)) { return $false } - - $logPath = Join-Path ([System.IO.Path]::GetTempPath()) "proxywarden-$Label-uninstall.log" - $process = Start-Process -FilePath "msiexec.exe" -ArgumentList @("/x", $productCode, "/qn", "/norestart", "/L*v", $logPath) -Wait -PassThru -WindowStyle Hidden - if ($process.ExitCode -ne 0 -and $process.ExitCode -ne 3010 -and $process.ExitCode -ne 1605) { - throw "$Label uninstall exited with code $($process.ExitCode). MSI log: $logPath" - } - - return $true -} - -try { - $details = @{} - $root = Assert-SafeInstallRoot $InstallRoot - $details.installRoot = $root - - $proxifyreDir = Resolve-SafeComponentDir $root "ProxiFyre" - $singboxDir = Resolve-SafeComponentDir $root "sing-box" - $proxifyreMarker = Read-ComponentMarker $proxifyreDir - $removePacketFilter = [bool]$ForceRemoveWindowsPacketFilter -or (Get-MarkerBool $proxifyreMarker "packetFilterInstalledByProxyWarden") - - $details.removedProxiFyreServices = Remove-ManagedService -Names @("ProxiFyreService", "ProxiFyre") -InstallRoot $root -UninstallExe (Join-Path $proxifyreDir "ProxiFyre.exe") - $details.removedSingBoxServices = Remove-ManagedService -Names @("ProxyWardenSingBox") -InstallRoot $root -UninstallExe (Join-Path $singboxDir "ProxyWardenSingBox.exe") - $details.removedProxiFyreFirewallRules = Remove-ManagedFirewallRules - $details.removedProxiFyreDir = Remove-SafeDirectory $proxifyreDir $root - $details.removedSingBoxDir = Remove-SafeDirectory $singboxDir $root - - if ($removePacketFilter) { - $packetFilter = Get-InstalledProgram "Windows Packet Filter|WinpkFilter|NDISAPI" - $details.removedWindowsPacketFilter = Uninstall-MsiProgram $packetFilter "windows-packet-filter" - } else { - $details.removedWindowsPacketFilter = $false - } - - New-Result -Success $true -Message "ProxyWarden managed components cleanup completed." -Details $details - exit 0 -} catch { - New-Result -Success $false -Message $_.Exception.Message -Details @{} - exit 1 -} diff --git a/src-tauri/bundled/components/catalog.json b/src-tauri/bundled/components/catalog.json new file mode 100644 index 0000000..82c160f --- /dev/null +++ b/src-tauri/bundled/components/catalog.json @@ -0,0 +1,130 @@ +{ + "schemaVersion": 1, + "targetArch": "x64", + "components": [ + { + "id": "proxifyre", + "version": "2.4.0", + "fileVersion": "2.4.0", + "productVersion": "2.4.0", + "assetPath": "proxifyre/ProxiFyre-v2.4.0-x64-signed.zip", + "assetArch": "x64", + "effectiveTarget": "x64", + "sha256": "eab65fd7d8eeb716abedb5614618c641de3f9eb8326b99cee1da787141e30cac", + "size": 1519694, + "sourceUrl": "https://github.com/wiresock/proxifyre/releases/download/v2.4.0/ProxiFyre-v2.4.0-x64-signed.zip", + "license": { + "id": "AGPL-3.0-only", + "path": "proxifyre/LICENSE" + }, + "installRole": "proxifyre-runtime", + "updateTrustPolicy": { + "type": "githubReleaseDigest", + "repository": "wiresock/proxifyre", + "tagPattern": "v*", + "assetPattern": "ProxiFyre-v*-x64-signed.zip", + "requireStable": true, + "authenticodePublishers": [ + "The Anti-Cloud Corporation" + ] + } + }, + { + "id": "windows-packet-filter", + "version": "3.6.2", + "fileVersion": "3.6.2.1", + "productVersion": "3.6.2.1", + "assetPath": "windows-packet-filter/Windows.Packet.Filter.3.6.2.1.x64.msi", + "assetArch": "x64", + "effectiveTarget": "x64", + "sha256": "9c388c0b7f189f7fa98720bae2caecf7d64f30910838b80b438ecf8956b8502c", + "size": 819200, + "sourceUrl": "https://github.com/wiresock/ndisapi/releases/download/v3.6.2/Windows.Packet.Filter.3.6.2.1.x64.msi", + "license": { + "id": "MIT", + "path": "windows-packet-filter/LICENSE" + }, + "installRole": "packet-filter-driver", + "updateTrustPolicy": { + "type": "githubReleaseDigest", + "repository": "wiresock/ndisapi", + "tagPattern": "v*", + "assetPattern": "Windows.Packet.Filter.*.x64.msi", + "requireStable": true, + "authenticodePublishers": [ + "The Anti-Cloud Corporation" + ] + } + }, + { + "id": "vc-runtime", + "version": "14.51.36247.0", + "fileVersion": "14.51.36247.0", + "productVersion": "14.51.36247.0", + "assetPath": "vc-runtime/VC_redist.x64.exe", + "assetArch": "x64", + "effectiveTarget": "x64", + "sha256": "843068991daaa1f73ad9f6239bce4d0f6a07a51f18c37ea2a867e9beca71295c", + "size": 18731856, + "sourceUrl": "https://aka.ms/vs/18/release/14.51.36247/VC_redist.x64.exe", + "license": { + "id": "LicenseRef-Microsoft-Visual-Cpp-v14-Redistributable-2026", + "path": "vc-runtime/LICENSE.docx" + }, + "installRole": "vc-runtime-prerequisite", + "updateTrustPolicy": { + "type": "buildTimeOnlyAuthenticode", + "allowedSourceHosts": [ + "aka.ms" + ], + "assetPattern": "VC_redist.x64.exe", + "publishers": [ + "Microsoft Corporation" + ] + } + }, + { + "id": "sing-box", + "version": "1.13.19", + "assetPath": "sing-box/sing-box-1.13.19-windows-amd64.zip", + "assetArch": "x64", + "effectiveTarget": "x64", + "sha256": "e011a4def2f5e2b143ed54adb2b1a20a6be407806ab4442f3667f1dd817a2c8d", + "size": 21046252, + "sourceUrl": "https://github.com/SagerNet/sing-box/releases/download/v1.13.19/sing-box-1.13.19-windows-amd64.zip", + "license": { + "id": "LicenseRef-Sing-Box-Project", + "path": "sing-box/LICENSE" + }, + "installRole": "sing-box-runtime", + "updateTrustPolicy": { + "type": "githubReleaseDigest", + "repository": "SagerNet/sing-box", + "tagPattern": "v*", + "assetPattern": "sing-box-*-windows-amd64.zip", + "requireStable": true + } + }, + { + "id": "winsw", + "version": "2.12.0", + "fileVersion": "2.12.0.0", + "productVersion": "2.12.0+eef5bade59fca0254e387ac73ed7625ba6aa7147", + "assetPath": "winsw/WinSW.NET461.exe", + "assetArch": "anycpu", + "effectiveTarget": "x64", + "sha256": "b5066b7bbdfba1293e5d15cda3caaea88fbeab35bd5b38c41c913d492aadfc4f", + "size": 655872, + "sourceUrl": "https://github.com/winsw/winsw/releases/download/v2.12.0/WinSW.NET461.exe", + "license": { + "id": "MIT", + "path": "winsw/LICENSE.txt" + }, + "installRole": "sing-box-service-wrapper", + "updateTrustPolicy": { + "type": "bundledOnlyNoIndependentProof", + "reason": "The official v2.12.0 asset is unsigned and has no independent release digest; runtime network update is disabled." + } + } + ] +} diff --git a/src-tauri/bundled/components/proxifyre/LICENSE b/src-tauri/bundled/components/proxifyre/LICENSE new file mode 100644 index 0000000..0ad25db --- /dev/null +++ b/src-tauri/bundled/components/proxifyre/LICENSE @@ -0,0 +1,661 @@ + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU Affero General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 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 Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU AGPL, see +. diff --git a/src-tauri/bundled/proxifyre/ProxiFyre-v2.2.1-x64-signed.zip b/src-tauri/bundled/components/proxifyre/ProxiFyre-v2.4.0-x64-signed.zip similarity index 62% rename from src-tauri/bundled/proxifyre/ProxiFyre-v2.2.1-x64-signed.zip rename to src-tauri/bundled/components/proxifyre/ProxiFyre-v2.4.0-x64-signed.zip index 5abd3f4e4be2085348ac4e3a0e8d4ae93c758496..3ff732a47a173e5e1386b879ba28062408f210fe 100644 GIT binary patch delta 615814 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