diff --git a/README.md b/README.md index 51e85a5..167f451 100644 --- a/README.md +++ b/README.md @@ -53,55 +53,17 @@ docker compose -f docker-compose.client.yml logs -f docker compose -f docker-compose.client.yml restart ``` -## Windows: standalone desktop client direction +## Windows Desktop Client -Windows app routing lives in a separate Tauri 2 desktop utility, not as -`APP_MODE=windows` inside the current Node gateway/client server. The active -workspace slice is `apps/windows-client`. - -Active design documents: - -- Product/tech brief: `docs/windows-client-product-tech-brief.md` -- Execution plan: `docs/goals/windows-modular-client/PLAN.md` -- Windows client README: `apps/windows-client/README.md` - -Target shape: - -- Control App: compact Windows UI for status, profiles, targets, components, - logs, and diagnostics. -- Proxyfier Layer: adapter boundary with ProxiFyre as the first engine for - per-app TCP/UDP routing. -- Local sing-box: optional local runtime; external SOCKS5/HTTP targets must - work without it. - -Development checks: +The Windows desktop client has been split out of this repository. Continue +Windows-specific work in the sibling repository: ```powershell -cd apps/windows-client -npm install -npm run build -npm run tauri -- info - -cd src-tauri -cargo test +cd D:\repos\ProxyWarden ``` -Native Tauri build requires WebView2, Rust/rustup, and Visual Studio Build -Tools with MSVC and Windows SDK components. In the current checkpoint, frontend -builds pass, while native Rust/Tauri tests require that Windows toolchain. - -The three Windows pieces are installed and operated separately: - -```powershell -cd apps/windows-client -& .\scripts\install-control-app.ps1 -PlanOnly -& .\scripts\install-proxyfier.ps1 -PlanOnly -& .\scripts\install-singbox.ps1 -PlanOnly -``` - -`-PlanOnly` returns structured JSON without install side effects. Real install -or service operations must be explicit; profile apply must not silently install -Proxyfier or Local sing-box. +This repository keeps the gateway and Docker client runtime; it no longer owns +the Tauri Windows app, ProxiFyre adapter, or Local sing-box installer flow. Windows source configuration is owned by JSON under `C:\ProgramData\VpnProxy\config`. Generated ProxiFyre and sing-box files under diff --git a/apps/windows-client/.gitignore b/apps/windows-client/.gitignore deleted file mode 100644 index cfcc268..0000000 --- a/apps/windows-client/.gitignore +++ /dev/null @@ -1,4 +0,0 @@ -node_modules/ -dist/ -src-tauri/target/ - diff --git a/apps/windows-client/README.md b/apps/windows-client/README.md deleted file mode 100644 index 043a3bd..0000000 --- a/apps/windows-client/README.md +++ /dev/null @@ -1,138 +0,0 @@ -# VPN Proxy Windows Client - -Standalone Windows desktop utility for app-level proxy routing. This app is -separate from the current Docker gateway/client runtime and must not be wired -through `APP_MODE=windows`. - -## Components - -- Control App: Tauri 2 + React/TypeScript UI and Rust command layer. -- Proxyfier Layer: ProxiFyre adapter for per-application routing. -- Local sing-box: optional local runtime, used only by targets that explicitly - require `singbox`. - -External SOCKS5 targets are the MVP path and do not require Local sing-box. - -## Source And Generated Files - -Source configuration is owned by Rust domain models and JSON files under: - -```text -C:\ProgramData\VpnProxy\config\profiles.json -C:\ProgramData\VpnProxy\config\targets.json -C:\ProgramData\VpnProxy\config\components.json -C:\ProgramData\VpnProxy\config\local-singbox.json -C:\ProgramData\VpnProxy\state\singbox-subscription-cache.json -C:\ProgramData\VpnProxy\state\activity.json -``` - -Generated artifacts are derived and can be recreated: - -```text -C:\ProgramData\VpnProxy\generated\proxifyre-app-config.json -C:\ProgramData\VpnProxy\generated\sing-box-config.json -``` - -## Development - -```powershell -cd apps/windows-client -npm install -npm run build -``` - -Run the browser preview shell: - -```powershell -npm run dev -- --host 127.0.0.1 -``` - -Run Tauri checks when the native Windows toolchain is installed: - -```powershell -npm run tauri -- info -npm run tauri -- dev -npm run tauri -- build -``` - -Run Rust tests when Rust/Cargo are installed: - -```powershell -cd apps/windows-client/src-tauri -cargo test -``` - -Native Tauri build requires WebView2, Rust via rustup, and Visual Studio Build -Tools with MSVC and Windows SDK components. - -## Explicit Installer Boundaries - -Installer scripts are explicit per component and return structured JSON in -`-PlanOnly` mode: - -```powershell -& .\scripts\install-control-app.ps1 -PlanOnly -& .\scripts\install-proxyfier.ps1 -PlanOnly -& .\scripts\install-singbox.ps1 -PlanOnly -``` - -Installers must be launched intentionally by the user or by a future narrow -helper permission. Profile apply must not silently install Control App, -Proxyfier, or Local sing-box. - -Local sing-box install creates this optional runtime: - -```text -C:\Program Files\VpnProxy\sing-box\sing-box.exe -C:\Program Files\VpnProxy\sing-box\VpnProxySingBox.exe -Windows service: VpnProxySingBox -``` - -The install flow downloads `sing-box` from `SagerNet/sing-box` releases and the -WinSW service wrapper from `winsw/winsw` releases, then writes a service config -that points at `C:\ProgramData\VpnProxy\generated\sing-box-config.json`. It -requires UAC confirmation. Uninstall is scoped to the configured -`VpnProxy\sing-box` install root. - -The external proxy route remains independent from Local sing-box. Choosing -Local sing-box in the UI generates `sing-box-config.json`, ensures the -`local-singbox` SOCKS5 target at `127.0.0.1:1080`, and then applies ProxiFyre -to that local target. - -## Existing Proxyfier Detection - -The app detects an already installed Proxyfier layer before showing component -status or applying profiles. Detection checks: - -- uninstall registry entries for `ProxiFyre` and `Proxifier`; -- common install folders such as `C:\Tools\ProxiFyre`, - `%ProgramFiles%\ProxiFyre`, and `%ProgramFiles%\Proxifier`; -- running `ProxiFyre` / `Proxifier` processes and the `ProxiFyreService` - service. - -For portable installs, set an override before launching the app: - -```powershell -$env:VPN_PROXY_PROXIFYRE_ROOT = 'D:\Tools\ProxiFyre' -npm run tauri -- dev -``` - -`ProxiFyre` installs are compatible with the current generated -`app-config.json` apply path. Plain `Proxifier` installs are detected and shown, -but automatic profile apply is not enabled for them yet because they use a -different profile format. - -## MVP Verification Flow - -1. Start the Control App or browser preview. -2. Confirm Components shows Control App, Proxyfier Layer, and optional Local - sing-box separately. -3. Add or keep an external SOCKS5 target. -4. Add a process/folder/exe profile such as Discord. -5. Apply profiles and verify generated ProxiFyre config plus activity entry. -6. Install Proxyfier separately before applying to a real service. -7. Install and start Local sing-box only when using a local target. - -Task evidence is recorded in -`docs/goals/windows-modular-client/EVIDENCE.md` and -`docs/goals/windows-local-singbox/EVIDENCE.md`. diff --git a/apps/windows-client/index.html b/apps/windows-client/index.html deleted file mode 100644 index be02298..0000000 --- a/apps/windows-client/index.html +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - VPN Proxy для Windows - - -
- - - diff --git a/apps/windows-client/package-lock.json b/apps/windows-client/package-lock.json deleted file mode 100644 index 73cf8ca..0000000 --- a/apps/windows-client/package-lock.json +++ /dev/null @@ -1,2126 +0,0 @@ -{ - "name": "vpn-proxy-windows-client", - "version": "0.1.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "vpn-proxy-windows-client", - "version": "0.1.0", - "dependencies": { - "@tauri-apps/api": "^2.0.0", - "@tauri-apps/plugin-dialog": "^2.7.1", - "lucide-react": "^1.23.0", - "react": "^19.0.0", - "react-dom": "^19.0.0" - }, - "devDependencies": { - "@tauri-apps/cli": "^2.0.0", - "@types/react": "^19.0.0", - "@types/react-dom": "^19.0.0", - "@vitejs/plugin-react": "^5.0.0", - "typescript": "^5.8.0", - "vite": "^7.0.0" - } - }, - "node_modules/@babel/code-frame": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", - "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-validator-identifier": "^7.29.7", - "js-tokens": "^4.0.0", - "picocolors": "^1.1.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/compat-data": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", - "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/core": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", - "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.29.7", - "@babel/generator": "^7.29.7", - "@babel/helper-compilation-targets": "^7.29.7", - "@babel/helper-module-transforms": "^7.29.7", - "@babel/helpers": "^7.29.7", - "@babel/parser": "^7.29.7", - "@babel/template": "^7.29.7", - "@babel/traverse": "^7.29.7", - "@babel/types": "^7.29.7", - "@jridgewell/remapping": "^2.3.5", - "convert-source-map": "^2.0.0", - "debug": "^4.1.0", - "gensync": "^1.0.0-beta.2", - "json5": "^2.2.3", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/babel" - } - }, - "node_modules/@babel/generator": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz", - "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.29.7", - "@babel/types": "^7.29.7", - "@jridgewell/gen-mapping": "^0.3.12", - "@jridgewell/trace-mapping": "^0.3.28", - "jsesc": "^3.0.2" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-compilation-targets": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", - "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/compat-data": "^7.29.7", - "@babel/helper-validator-option": "^7.29.7", - "browserslist": "^4.24.0", - "lru-cache": "^5.1.1", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-globals": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", - "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-imports": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", - "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/traverse": "^7.29.7", - "@babel/types": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-transforms": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", - "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-module-imports": "^7.29.7", - "@babel/helper-validator-identifier": "^7.29.7", - "@babel/traverse": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-plugin-utils": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz", - "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-string-parser": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", - "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", - "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-option": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", - "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helpers": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", - "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/template": "^7.29.7", - "@babel/types": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/parser": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", - "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.29.7" - }, - "bin": { - "parser": "bin/babel-parser.js" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@babel/plugin-transform-react-jsx-self": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.29.7.tgz", - "integrity": "sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-react-jsx-source": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.29.7.tgz", - "integrity": "sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/template": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", - "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.29.7", - "@babel/parser": "^7.29.7", - "@babel/types": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/traverse": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz", - "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.29.7", - "@babel/generator": "^7.29.7", - "@babel/helper-globals": "^7.29.7", - "@babel/parser": "^7.29.7", - "@babel/template": "^7.29.7", - "@babel/types": "^7.29.7", - "debug": "^4.3.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/types": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", - "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-string-parser": "^7.29.7", - "@babel/helper-validator-identifier": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz", - "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz", - "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz", - "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz", - "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz", - "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz", - "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz", - "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz", - "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz", - "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz", - "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz", - "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz", - "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz", - "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==", - "cpu": [ - "mips64el" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz", - "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz", - "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz", - "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz", - "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-arm64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz", - "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz", - "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-arm64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz", - "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz", - "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openharmony-arm64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz", - "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz", - "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz", - "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz", - "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz", - "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.13", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", - "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0", - "@jridgewell/trace-mapping": "^0.3.24" - } - }, - "node_modules/@jridgewell/remapping": { - "version": "2.3.5", - "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", - "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" - } - }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", - "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", - "dev": true, - "license": "MIT" - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.31", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", - "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, - "node_modules/@rolldown/pluginutils": { - "version": "1.0.0-rc.3", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.3.tgz", - "integrity": "sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.2.tgz", - "integrity": "sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-android-arm64": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.2.tgz", - "integrity": "sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.2.tgz", - "integrity": "sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.2.tgz", - "integrity": "sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.2.tgz", - "integrity": "sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.2.tgz", - "integrity": "sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.2.tgz", - "integrity": "sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg==", - "cpu": [ - "arm" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.2.tgz", - "integrity": "sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA==", - "cpu": [ - "arm" - ], - "dev": true, - "libc": [ - "musl" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.2.tgz", - "integrity": "sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA==", - "cpu": [ - "arm64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.2.tgz", - "integrity": "sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "libc": [ - "musl" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.2.tgz", - "integrity": "sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg==", - "cpu": [ - "loong64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-loong64-musl": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.2.tgz", - "integrity": "sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ==", - "cpu": [ - "loong64" - ], - "dev": true, - "libc": [ - "musl" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.2.tgz", - "integrity": "sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A==", - "cpu": [ - "ppc64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-ppc64-musl": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.2.tgz", - "integrity": "sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w==", - "cpu": [ - "ppc64" - ], - "dev": true, - "libc": [ - "musl" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.2.tgz", - "integrity": "sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg==", - "cpu": [ - "riscv64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.2.tgz", - "integrity": "sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q==", - "cpu": [ - "riscv64" - ], - "dev": true, - "libc": [ - "musl" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.2.tgz", - "integrity": "sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg==", - "cpu": [ - "s390x" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.2.tgz", - "integrity": "sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A==", - "cpu": [ - "x64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.2.tgz", - "integrity": "sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg==", - "cpu": [ - "x64" - ], - "dev": true, - "libc": [ - "musl" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-openbsd-x64": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.2.tgz", - "integrity": "sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ] - }, - "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.2.tgz", - "integrity": "sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ] - }, - "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.2.tgz", - "integrity": "sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.2.tgz", - "integrity": "sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.2.tgz", - "integrity": "sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.2.tgz", - "integrity": "sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@tauri-apps/api": { - "version": "2.11.1", - "resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-2.11.1.tgz", - "integrity": "sha512-M2FPuYND2m+wh5hfW9ZpSdxMPdEJovPBWwoHJmwUpysTYNHaOkVFN419m/K0LIgjb/7KU2vBgsUepJWugQCvAA==", - "license": "Apache-2.0 OR MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/tauri" - } - }, - "node_modules/@tauri-apps/cli": { - "version": "2.11.4", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli/-/cli-2.11.4.tgz", - "integrity": "sha512-R8xGtMpwyetawSqm9kYOuMmEqkhUbvcUy8n0aNXIxollKBLESUu5f4Fx+64hgASYm1H+jSWq6jCW6zqTnH6hqQ==", - "dev": true, - "license": "Apache-2.0 OR MIT", - "bin": { - "tauri": "tauri.js" - }, - "engines": { - "node": ">= 10" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/tauri" - }, - "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" - } - }, - "node_modules/@tauri-apps/cli-darwin-arm64": { - "version": "2.11.4", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-2.11.4.tgz", - "integrity": "sha512-1ryOF3ZhpZ/nemHV5zVwBQBz9jDGKmKPvWPADOhc83ig0P4bMc2iER4NbC6r9sjeIZ6RVQ4g3RZIYvezhcl4TQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "Apache-2.0 OR MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tauri-apps/cli-darwin-x64": { - "version": "2.11.4", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-x64/-/cli-darwin-x64-2.11.4.tgz", - "integrity": "sha512-uFsGQAAfuyz1k/yGLmkWfkBlgKAqZfxqlHmLWx81QU27RJWfmbNHCIq8T8w1e+VClleIuZUjpHWfoE4E3DLo3A==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "Apache-2.0 OR MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tauri-apps/cli-linux-arm-gnueabihf": { - "version": "2.11.4", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm-gnueabihf/-/cli-linux-arm-gnueabihf-2.11.4.tgz", - "integrity": "sha512-IaHZn5CdBL21oUmjiVOS1ctw6Ip1O0pjp70FwOWmYz1myWe0SY96ZIj2FYf7pT0m8bI2h/hrs5ZbEXXh44/MkQ==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "Apache-2.0 OR MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tauri-apps/cli-linux-arm64-gnu": { - "version": "2.11.4", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-gnu/-/cli-linux-arm64-gnu-2.11.4.tgz", - "integrity": "sha512-N41/ukTRVe6XSuUTESuFdGeOW2i7k62tK+6gHK5Kd5/q5RPvvi19GaWAVPPb9u95HSGmTChSolBfzynUsssFaA==", - "cpu": [ - "arm64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "Apache-2.0 OR MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tauri-apps/cli-linux-arm64-musl": { - "version": "2.11.4", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.11.4.tgz", - "integrity": "sha512-v277UnT/fB64xAfSroL5N3Km3tLmvATWqJJw/wRI+g6o+HkeD0slyE7gOhNs1MbjE41R7bQOTxMVoL3aomUJmw==", - "cpu": [ - "arm64" - ], - "dev": true, - "libc": [ - "musl" - ], - "license": "Apache-2.0 OR MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tauri-apps/cli-linux-riscv64-gnu": { - "version": "2.11.4", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-riscv64-gnu/-/cli-linux-riscv64-gnu-2.11.4.tgz", - "integrity": "sha512-qqgNkQ2u1yZHxjhxsZaxUtRDW8dIqIYm33rx/mzwQv0SfY9x1B+iraj8vWeFiXjjSVVhEMepXSOts1TqPzvXNQ==", - "cpu": [ - "riscv64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "Apache-2.0 OR MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tauri-apps/cli-linux-x64-gnu": { - "version": "2.11.4", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-2.11.4.tgz", - "integrity": "sha512-2VRNWl84FOH0m2giiDkO2h0QXlcMJeX+zJDpI5kDIQAx6s+geF3v48F4DXfJez4GS/FdoDGnPnw1C2iYGbQ7bQ==", - "cpu": [ - "x64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "Apache-2.0 OR MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tauri-apps/cli-linux-x64-musl": { - "version": "2.11.4", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-2.11.4.tgz", - "integrity": "sha512-o9GyhYor/nc7xarmwDE3ka2szuW3uuZzXjHWh64Q8YX5AtSgxdQkFWzrY4O8KiGtVNvFBI14H3Q49Qj5TOIP/A==", - "cpu": [ - "x64" - ], - "dev": true, - "libc": [ - "musl" - ], - "license": "Apache-2.0 OR MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tauri-apps/cli-win32-arm64-msvc": { - "version": "2.11.4", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-arm64-msvc/-/cli-win32-arm64-msvc-2.11.4.tgz", - "integrity": "sha512-ld5Ehb598m0VkYyylRPNeCFsBe/km0jxis6KgMpl3IGY6I/i1RwQXO05I1AsXUXO2WC6AvB/Lw4qTf/asiuEiQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "Apache-2.0 OR MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tauri-apps/cli-win32-ia32-msvc": { - "version": "2.11.4", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-ia32-msvc/-/cli-win32-ia32-msvc-2.11.4.tgz", - "integrity": "sha512-12Hxi0XX/H5VFxO/bGgHkFWhml9VMgEOu9CidjeCeTNQ1l6fpUlbiGgSP7CLI3PFtW9/FfbeHieZ+kyWK5H7CA==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "Apache-2.0 OR MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tauri-apps/cli-win32-x64-msvc": { - "version": "2.11.4", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-2.11.4.tgz", - "integrity": "sha512-+vDiqBIU5dMISg/wNvX3sF+ZHfgJGJ5T0AcO+EHNXV9GGAG+P5fzodlDXD3QdKCRgZxMoCm5PPvj3BqLNjBthw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "Apache-2.0 OR MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tauri-apps/plugin-dialog": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-dialog/-/plugin-dialog-2.7.1.tgz", - "integrity": "sha512-OK1UBXYt+ojcmxMktzzuyonYIFta8CmAASpX+CA+DTGK24KlHjhYI6x2iOJ/TjZF4N7/ACK1oFmEOjIY9IhzOQ==", - "license": "MIT OR Apache-2.0", - "dependencies": { - "@tauri-apps/api": "^2.11.0" - } - }, - "node_modules/@types/babel__core": { - "version": "7.20.5", - "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", - "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.20.7", - "@babel/types": "^7.20.7", - "@types/babel__generator": "*", - "@types/babel__template": "*", - "@types/babel__traverse": "*" - } - }, - "node_modules/@types/babel__generator": { - "version": "7.27.0", - "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", - "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.0.0" - } - }, - "node_modules/@types/babel__template": { - "version": "7.4.4", - "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", - "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.1.0", - "@babel/types": "^7.0.0" - } - }, - "node_modules/@types/babel__traverse": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", - "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.28.2" - } - }, - "node_modules/@types/estree": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", - "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/react": { - "version": "19.2.17", - "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.17.tgz", - "integrity": "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==", - "dev": true, - "license": "MIT", - "dependencies": { - "csstype": "^3.2.2" - } - }, - "node_modules/@types/react-dom": { - "version": "19.2.3", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", - "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "@types/react": "^19.2.0" - } - }, - "node_modules/@vitejs/plugin-react": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.2.0.tgz", - "integrity": "sha512-YmKkfhOAi3wsB1PhJq5Scj3GXMn3WvtQ/JC0xoopuHoXSdmtdStOpFrYaT1kie2YgFBcIe64ROzMYRjCrYOdYw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/core": "^7.29.0", - "@babel/plugin-transform-react-jsx-self": "^7.27.1", - "@babel/plugin-transform-react-jsx-source": "^7.27.1", - "@rolldown/pluginutils": "1.0.0-rc.3", - "@types/babel__core": "^7.20.5", - "react-refresh": "^0.18.0" - }, - "engines": { - "node": "^20.19.0 || >=22.12.0" - }, - "peerDependencies": { - "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" - } - }, - "node_modules/baseline-browser-mapping": { - "version": "2.10.41", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.41.tgz", - "integrity": "sha512-WwS7MHhqGHHlaVsqRZnhvCEMS0owDX+SxRlve7JkuH7My1Ara3ZriTmCQupPfYjxMZ8I/tgxtJYr2t7taHaH4A==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "baseline-browser-mapping": "dist/cli.cjs" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/browserslist": { - "version": "4.28.4", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.4.tgz", - "integrity": "sha512-MTc8i/x9jBQd1iMw2CFGS+rwMa07eYjLR0CCTLDACl9xhxy+nIs3KeML/biicXtk9JrZ6dnnTatmc7ErPXIxqw==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "baseline-browser-mapping": "^2.10.38", - "caniuse-lite": "^1.0.30001799", - "electron-to-chromium": "^1.5.376", - "node-releases": "^2.0.48", - "update-browserslist-db": "^1.2.3" - }, - "bin": { - "browserslist": "cli.js" - }, - "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" - } - }, - "node_modules/caniuse-lite": { - "version": "1.0.30001800", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001800.tgz", - "integrity": "sha512-MMHtuAz9Ys840zAY5F4k6fV5GaivZ9sPk+nz0mY+GYVzRBnYkN0mpqkSR92oWRQ19yQWo4HvBV/FnC16AJX8MA==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/caniuse-lite" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "CC-BY-4.0" - }, - "node_modules/convert-source-map": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", - "dev": true, - "license": "MIT" - }, - "node_modules/csstype": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", - "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/electron-to-chromium": { - "version": "1.5.385", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.385.tgz", - "integrity": "sha512-78sa/M08MNAYHQfjoWMvOlKQqZ0ElhSm/L5HNUc96VZ3b+KvDVnngFm8sYQy0XrhTRgAhggHr5abA7yTvRdo4Q==", - "dev": true, - "license": "ISC" - }, - "node_modules/esbuild": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz", - "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=18" - }, - "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" - } - }, - "node_modules/escalade": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", - "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/fdir": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/gensync": { - "version": "1.0.0-beta.2", - "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", - "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/jsesc": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", - "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", - "dev": true, - "license": "MIT", - "bin": { - "jsesc": "bin/jsesc" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/json5": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", - "dev": true, - "license": "MIT", - "bin": { - "json5": "lib/cli.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/lru-cache": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", - "dev": true, - "license": "ISC", - "dependencies": { - "yallist": "^3.0.2" - } - }, - "node_modules/lucide-react": { - "version": "1.23.0", - "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.23.0.tgz", - "integrity": "sha512-38BpJcD0JhFosxHApP/BYsBetLpQFRoTRzEzstM/XCc3jsAG7wqaY1lgVwxiUe3xqYE+lNxo2PkCmYwXWrwwIw==", - "license": "ISC", - "peerDependencies": { - "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" - } - }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, - "license": "MIT" - }, - "node_modules/nanoid": { - "version": "3.3.15", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.15.tgz", - "integrity": "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, - "node_modules/node-releases": { - "version": "2.0.50", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.50.tgz", - "integrity": "sha512-J6l92tKHX6w8Jy5nO1Vuc01NoIiRGi/d6qBKVxh+IQ8Cr3b6HbVNfKiF8ZpFKufTwpwxMmce2W3iQZ861ZRyTg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true, - "license": "ISC" - }, - "node_modules/picomatch": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", - "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/postcss": { - "version": "8.5.16", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.16.tgz", - "integrity": "sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "nanoid": "^3.3.12", - "picocolors": "^1.1.1", - "source-map-js": "^1.2.1" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, - "node_modules/react": { - "version": "19.2.7", - "resolved": "https://registry.npmjs.org/react/-/react-19.2.7.tgz", - "integrity": "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/react-dom": { - "version": "19.2.7", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.7.tgz", - "integrity": "sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==", - "license": "MIT", - "dependencies": { - "scheduler": "^0.27.0" - }, - "peerDependencies": { - "react": "^19.2.7" - } - }, - "node_modules/react-refresh": { - "version": "0.18.0", - "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz", - "integrity": "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/rollup": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.2.tgz", - "integrity": "sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/estree": "1.0.9" - }, - "bin": { - "rollup": "dist/bin/rollup" - }, - "engines": { - "node": ">=18.0.0", - "npm": ">=8.0.0" - }, - "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.62.2", - "@rollup/rollup-android-arm64": "4.62.2", - "@rollup/rollup-darwin-arm64": "4.62.2", - "@rollup/rollup-darwin-x64": "4.62.2", - "@rollup/rollup-freebsd-arm64": "4.62.2", - "@rollup/rollup-freebsd-x64": "4.62.2", - "@rollup/rollup-linux-arm-gnueabihf": "4.62.2", - "@rollup/rollup-linux-arm-musleabihf": "4.62.2", - "@rollup/rollup-linux-arm64-gnu": "4.62.2", - "@rollup/rollup-linux-arm64-musl": "4.62.2", - "@rollup/rollup-linux-loong64-gnu": "4.62.2", - "@rollup/rollup-linux-loong64-musl": "4.62.2", - "@rollup/rollup-linux-ppc64-gnu": "4.62.2", - "@rollup/rollup-linux-ppc64-musl": "4.62.2", - "@rollup/rollup-linux-riscv64-gnu": "4.62.2", - "@rollup/rollup-linux-riscv64-musl": "4.62.2", - "@rollup/rollup-linux-s390x-gnu": "4.62.2", - "@rollup/rollup-linux-x64-gnu": "4.62.2", - "@rollup/rollup-linux-x64-musl": "4.62.2", - "@rollup/rollup-openbsd-x64": "4.62.2", - "@rollup/rollup-openharmony-arm64": "4.62.2", - "@rollup/rollup-win32-arm64-msvc": "4.62.2", - "@rollup/rollup-win32-ia32-msvc": "4.62.2", - "@rollup/rollup-win32-x64-gnu": "4.62.2", - "@rollup/rollup-win32-x64-msvc": "4.62.2", - "fsevents": "~2.3.2" - } - }, - "node_modules/scheduler": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", - "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", - "license": "MIT" - }, - "node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/source-map-js": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/tinyglobby": { - "version": "0.2.17", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", - "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", - "dev": true, - "license": "MIT", - "dependencies": { - "fdir": "^6.5.0", - "picomatch": "^4.0.4" - }, - "engines": { - "node": ">=12.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/SuperchupuDev" - } - }, - "node_modules/typescript": { - "version": "5.9.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", - "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/update-browserslist-db": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", - "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "escalade": "^3.2.0", - "picocolors": "^1.1.1" - }, - "bin": { - "update-browserslist-db": "cli.js" - }, - "peerDependencies": { - "browserslist": ">= 4.21.0" - } - }, - "node_modules/vite": { - "version": "7.3.6", - "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.6.tgz", - "integrity": "sha512-4XP60spRGjSZFf1qYH+dJIkK2znL3zQfl9KkOV9MkkRR/3Dls0dxaBsQPTloEc5BLXWPL9vsOxopxyKoMmDueg==", - "dev": true, - "license": "MIT", - "dependencies": { - "esbuild": "^0.27.0 || ^0.28.0", - "fdir": "^6.5.0", - "picomatch": "^4.0.3", - "postcss": "^8.5.6", - "rollup": "^4.43.0", - "tinyglobby": "^0.2.15" - }, - "bin": { - "vite": "bin/vite.js" - }, - "engines": { - "node": "^20.19.0 || >=22.12.0" - }, - "funding": { - "url": "https://github.com/vitejs/vite?sponsor=1" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" - }, - "peerDependencies": { - "@types/node": "^20.19.0 || >=22.12.0", - "jiti": ">=1.21.0", - "less": "^4.0.0", - "lightningcss": "^1.21.0", - "sass": "^1.70.0", - "sass-embedded": "^1.70.0", - "stylus": ">=0.54.8", - "sugarss": "^5.0.0", - "terser": "^5.16.0", - "tsx": "^4.8.1", - "yaml": "^2.4.2" - }, - "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 - } - } - }, - "node_modules/yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", - "dev": true, - "license": "ISC" - } - } -} diff --git a/apps/windows-client/package.json b/apps/windows-client/package.json deleted file mode 100644 index 62c35bc..0000000 --- a/apps/windows-client/package.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "name": "vpn-proxy-windows-client", - "version": "0.1.0", - "private": true, - "type": "module", - "description": "Standalone Windows desktop proxy management app for VPN Proxy.", - "scripts": { - "dev": "vite", - "build": "tsc && vite build", - "preview": "vite preview", - "tauri": "tauri" - }, - "dependencies": { - "@tauri-apps/api": "^2.0.0", - "@tauri-apps/plugin-dialog": "^2.7.1", - "lucide-react": "^1.23.0", - "react": "^19.0.0", - "react-dom": "^19.0.0" - }, - "devDependencies": { - "@tauri-apps/cli": "^2.0.0", - "@types/react": "^19.0.0", - "@types/react-dom": "^19.0.0", - "@vitejs/plugin-react": "^5.0.0", - "typescript": "^5.8.0", - "vite": "^7.0.0" - } -} diff --git a/apps/windows-client/scripts/install-control-app.ps1 b/apps/windows-client/scripts/install-control-app.ps1 deleted file mode 100644 index bef4573..0000000 --- a/apps/windows-client/scripts/install-control-app.ps1 +++ /dev/null @@ -1,79 +0,0 @@ -param( - [string]$InstallRoot = "C:\Program Files\VpnProxy\ControlApp", - [string]$DataRoot = "C:\ProgramData\VpnProxy", - [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/apps/windows-client/scripts/install-proxyfier.ps1 b/apps/windows-client/scripts/install-proxyfier.ps1 deleted file mode 100644 index 521c438..0000000 --- a/apps/windows-client/scripts/install-proxyfier.ps1 +++ /dev/null @@ -1,96 +0,0 @@ -param( - [string]$InstallRoot = "C:\Tools\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/apps/windows-client/scripts/install-singbox.ps1 b/apps/windows-client/scripts/install-singbox.ps1 deleted file mode 100644 index f69eefe..0000000 --- a/apps/windows-client/scripts/install-singbox.ps1 +++ /dev/null @@ -1,270 +0,0 @@ -param( - [string]$InstallRoot = "C:\Program Files\VpnProxy\sing-box", - [string]$ServiceName = "VpnProxySingBox", - [string]$ConfigSource = "C:\ProgramData\VpnProxy\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" = "vpn-proxy-windows-client" } -} - -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 "\\VpnProxy$|\\vpn-proxy$" -} - -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 - VPN Proxy Local sing-box - Local sing-box runtime managed by VPN Proxy Windows client. - %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()) ("vpn-proxy-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" = "vpn-proxy-windows-client" } - $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" = "vpn-proxy-windows-client" } - $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/apps/windows-client/src-tauri/Cargo.lock b/apps/windows-client/src-tauri/Cargo.lock deleted file mode 100644 index e13342c..0000000 --- a/apps/windows-client/src-tauri/Cargo.lock +++ /dev/null @@ -1,4850 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 4 - -[[package]] -name = "adler2" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" - -[[package]] -name = "aho-corasick" -version = "1.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" -dependencies = [ - "memchr", -] - -[[package]] -name = "alloc-no-stdlib" -version = "2.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" - -[[package]] -name = "alloc-stdlib" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e76a019e91224d279006ff972f1e984179a6e9feb050adba6ce8274aef23195" -dependencies = [ - "alloc-no-stdlib", -] - -[[package]] -name = "android_system_properties" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" -dependencies = [ - "libc", -] - -[[package]] -name = "anyhow" -version = "1.0.103" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3" - -[[package]] -name = "atk" -version = "0.18.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "241b621213072e993be4f6f3a9e4b45f65b7e6faad43001be957184b7bb1824b" -dependencies = [ - "atk-sys", - "glib", - "libc", -] - -[[package]] -name = "atk-sys" -version = "0.18.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5e48b684b0ca77d2bbadeef17424c2ea3c897d44d566a1617e7e8f30614d086" -dependencies = [ - "glib-sys", - "gobject-sys", - "libc", - "system-deps", -] - -[[package]] -name = "atomic-waker" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" - -[[package]] -name = "autocfg" -version = "1.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" - -[[package]] -name = "base64" -version = "0.21.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" - -[[package]] -name = "base64" -version = "0.22.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" - -[[package]] -name = "bit-set" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" -dependencies = [ - "bit-vec", -] - -[[package]] -name = "bit-vec" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" - -[[package]] -name = "bitflags" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" - -[[package]] -name = "bitflags" -version = "2.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" -dependencies = [ - "serde_core", -] - -[[package]] -name = "block-buffer" -version = "0.10.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" -dependencies = [ - "generic-array", -] - -[[package]] -name = "block2" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" -dependencies = [ - "objc2", -] - -[[package]] -name = "brotli" -version = "8.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cc91aac060a7a1e25823bdccbfb6af1875b88f17c6daac97894eed8207166b3" -dependencies = [ - "alloc-no-stdlib", - "alloc-stdlib", - "brotli-decompressor", -] - -[[package]] -name = "brotli-decompressor" -version = "5.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a32acac15fe1967bc3986b2a6347dffc965602354ea6f450ad07e8bfd253583" -dependencies = [ - "alloc-no-stdlib", - "alloc-stdlib", -] - -[[package]] -name = "bs58" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4" -dependencies = [ - "tinyvec", -] - -[[package]] -name = "bumpalo" -version = "3.20.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" - -[[package]] -name = "bytemuck" -version = "1.25.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" - -[[package]] -name = "byteorder" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" - -[[package]] -name = "bytes" -version = "1.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ae3f5d315924270530207e2a68396c3cc547f6dca3fbdca317cfb1a51edb593" -dependencies = [ - "serde", -] - -[[package]] -name = "cairo-rs" -version = "0.18.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ca26ef0159422fb77631dc9d17b102f253b876fe1586b03b803e63a309b4ee2" -dependencies = [ - "bitflags 2.13.0", - "cairo-sys-rs", - "glib", - "libc", - "once_cell", - "thiserror 1.0.69", -] - -[[package]] -name = "cairo-sys-rs" -version = "0.18.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "685c9fa8e590b8b3d678873528d83411db17242a73fccaed827770ea0fedda51" -dependencies = [ - "glib-sys", - "libc", - "system-deps", -] - -[[package]] -name = "camino" -version = "1.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f2d30e4173c4026932d51d31d6b0613b1fd3014bf3f9f8943d4ba139c437ba0" -dependencies = [ - "serde_core", -] - -[[package]] -name = "cargo-platform" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e35af189006b9c0f00a064685c727031e3ed2d8020f7ba284d78cc2671bd36ea" -dependencies = [ - "serde", -] - -[[package]] -name = "cargo_metadata" -version = "0.19.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd5eb614ed4c27c5d706420e4320fbe3216ab31fa1c33cd8246ac36dae4479ba" -dependencies = [ - "camino", - "cargo-platform", - "semver", - "serde", - "serde_json", - "thiserror 2.0.18", -] - -[[package]] -name = "cargo_toml" -version = "0.22.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "374b7c592d9c00c1f4972ea58390ac6b18cbb6ab79011f3bdc90a0b82ca06b77" -dependencies = [ - "serde", - "toml 0.9.12+spec-1.1.0", -] - -[[package]] -name = "cc" -version = "1.2.65" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e228eec9be7c17ccb640b59b36a5cd805ea2a564a4c5e162c2f659fea30d3b96" -dependencies = [ - "find-msvc-tools", - "shlex", -] - -[[package]] -name = "cesu8" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" - -[[package]] -name = "cfb" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d38f2da7a0a2c4ccf0065be06397cc26a81f4e528be095826eee9d4adbb8c60f" -dependencies = [ - "byteorder", - "fnv", - "uuid", -] - -[[package]] -name = "cfg-expr" -version = "0.15.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02" -dependencies = [ - "smallvec", - "target-lexicon", -] - -[[package]] -name = "cfg-if" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" - -[[package]] -name = "cfg_aliases" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" - -[[package]] -name = "chacha20" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81" -dependencies = [ - "cfg-if", - "cpufeatures 0.3.0", - "rand_core", -] - -[[package]] -name = "chrono" -version = "0.4.45" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" -dependencies = [ - "iana-time-zone", - "num-traits", - "serde", - "windows-link 0.2.1", -] - -[[package]] -name = "combine" -version = "4.6.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" -dependencies = [ - "bytes", - "memchr", -] - -[[package]] -name = "cookie" -version = "0.18.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747" -dependencies = [ - "time", - "version_check", -] - -[[package]] -name = "core-foundation" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" -dependencies = [ - "core-foundation-sys", - "libc", -] - -[[package]] -name = "core-foundation-sys" -version = "0.8.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" - -[[package]] -name = "core-graphics" -version = "0.25.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "064badf302c3194842cf2c5d61f56cc88e54a759313879cdf03abdd27d0c3b97" -dependencies = [ - "bitflags 2.13.0", - "core-foundation", - "core-graphics-types", - "foreign-types", - "libc", -] - -[[package]] -name = "core-graphics-types" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" -dependencies = [ - "bitflags 2.13.0", - "core-foundation", - "libc", -] - -[[package]] -name = "cpufeatures" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" -dependencies = [ - "libc", -] - -[[package]] -name = "cpufeatures" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" -dependencies = [ - "libc", -] - -[[package]] -name = "crc32fast" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "crossbeam-channel" -version = "0.5.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" -dependencies = [ - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-utils" -version = "0.8.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" - -[[package]] -name = "crypto-common" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" -dependencies = [ - "generic-array", - "typenum", -] - -[[package]] -name = "cssparser" -version = "0.36.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dae61cf9c0abb83bd659dab65b7e4e38d8236824c85f0f804f173567bda257d2" -dependencies = [ - "cssparser-macros", - "dtoa-short", - "itoa", - "phf", - "smallvec", -] - -[[package]] -name = "cssparser-macros" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" -dependencies = [ - "quote", - "syn 2.0.118", -] - -[[package]] -name = "ctor" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "352d39c2f7bef1d6ad73db6f5160efcaed66d94ef8c6c573a8410c00bf909a98" -dependencies = [ - "ctor-proc-macro", - "dtor", -] - -[[package]] -name = "ctor-proc-macro" -version = "0.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52560adf09603e58c9a7ee1fe1dcb95a16927b17c127f0ac02d6e768a0e25bc1" - -[[package]] -name = "darling" -version = "0.23.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" -dependencies = [ - "darling_core", - "darling_macro", -] - -[[package]] -name = "darling_core" -version = "0.23.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" -dependencies = [ - "ident_case", - "proc-macro2", - "quote", - "strsim", - "syn 2.0.118", -] - -[[package]] -name = "darling_macro" -version = "0.23.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" -dependencies = [ - "darling_core", - "quote", - "syn 2.0.118", -] - -[[package]] -name = "dbus" -version = "0.9.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ab69f03cc8c4340c9c8e315114e1658e6775a9b16a04357973aa21cec22b32e" -dependencies = [ - "libc", - "libdbus-sys", - "windows-sys 0.61.2", -] - -[[package]] -name = "deranged" -version = "0.5.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" -dependencies = [ - "serde_core", -] - -[[package]] -name = "derive_more" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" -dependencies = [ - "derive_more-impl", -] - -[[package]] -name = "derive_more-impl" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" -dependencies = [ - "proc-macro2", - "quote", - "rustc_version", - "syn 2.0.118", -] - -[[package]] -name = "digest" -version = "0.10.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" -dependencies = [ - "block-buffer", - "crypto-common", -] - -[[package]] -name = "dirs" -version = "6.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" -dependencies = [ - "dirs-sys", -] - -[[package]] -name = "dirs-sys" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" -dependencies = [ - "libc", - "option-ext", - "redox_users", - "windows-sys 0.61.2", -] - -[[package]] -name = "dispatch2" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" -dependencies = [ - "bitflags 2.13.0", - "block2", - "libc", - "objc2", -] - -[[package]] -name = "displaydoc" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.118", -] - -[[package]] -name = "dlopen2" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e2c5bd4158e66d1e215c49b837e11d62f3267b30c92f1d171c4d3105e3dc4d4" -dependencies = [ - "dlopen2_derive", - "libc", - "once_cell", - "winapi", -] - -[[package]] -name = "dlopen2_derive" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fbbb781877580993a8707ec48672673ec7b81eeba04cfd2310bd28c08e47c8f" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.118", -] - -[[package]] -name = "dom_query" -version = "0.27.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "521e380c0c8afb8d9a1e83a1822ee03556fc3e3e7dbc1fd30be14e37f9cb3f89" -dependencies = [ - "bit-set", - "cssparser", - "foldhash", - "html5ever", - "precomputed-hash", - "selectors", - "tendril", -] - -[[package]] -name = "dpi" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76" -dependencies = [ - "serde", -] - -[[package]] -name = "dtoa" -version = "1.0.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c3cf4824e2d5f025c7b531afcb2325364084a16806f6d47fbc1f5fbd9960590" - -[[package]] -name = "dtoa-short" -version = "0.3.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd1511a7b6a56299bd043a9c167a6d2bfb37bf84a6dfceaba651168adfb43c87" -dependencies = [ - "dtoa", -] - -[[package]] -name = "dtor" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1057d6c64987086ff8ed0fd3fbf377a6b7d205cc7715868cd401705f715cbe4" -dependencies = [ - "dtor-proc-macro", -] - -[[package]] -name = "dtor-proc-macro" -version = "0.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f678cf4a922c215c63e0de95eb1ff08a958a81d47e485cf9da1e27bf6305cfa5" - -[[package]] -name = "dunce" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" - -[[package]] -name = "dyn-clone" -version = "1.0.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" - -[[package]] -name = "embed-resource" -version = "3.0.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbfdaacccebec3b28e4866b8973543c7647797db5ada1bdab552e48fe665fbbd" -dependencies = [ - "cc", - "memchr", - "rustc_version", - "toml 1.1.2+spec-1.1.0", - "vswhom", - "winreg", -] - -[[package]] -name = "embed_plist" -version = "1.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7" - -[[package]] -name = "equivalent" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" - -[[package]] -name = "erased-serde" -version = "0.4.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2add8a07dd6a8d93ff627029c51de145e12686fbc36ecb298ac22e74cf02dec" -dependencies = [ - "serde", - "serde_core", - "typeid", -] - -[[package]] -name = "fastrand" -version = "2.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" - -[[package]] -name = "fdeflate" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" -dependencies = [ - "simd-adler32", -] - -[[package]] -name = "field-offset" -version = "0.3.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38e2275cc4e4fc009b0669731a1e5ab7ebf11f469eaede2bab9309a5b4d6057f" -dependencies = [ - "memoffset", - "rustc_version", -] - -[[package]] -name = "find-msvc-tools" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" - -[[package]] -name = "flate2" -version = "1.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" -dependencies = [ - "crc32fast", - "miniz_oxide", -] - -[[package]] -name = "fnv" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" - -[[package]] -name = "foldhash" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" - -[[package]] -name = "foreign-types" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" -dependencies = [ - "foreign-types-macros", - "foreign-types-shared", -] - -[[package]] -name = "foreign-types-macros" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.118", -] - -[[package]] -name = "foreign-types-shared" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" - -[[package]] -name = "form_urlencoded" -version = "1.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" -dependencies = [ - "percent-encoding", -] - -[[package]] -name = "futures-channel" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" -dependencies = [ - "futures-core", - "futures-sink", -] - -[[package]] -name = "futures-core" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" - -[[package]] -name = "futures-executor" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" -dependencies = [ - "futures-core", - "futures-task", - "futures-util", -] - -[[package]] -name = "futures-io" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" - -[[package]] -name = "futures-macro" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.118", -] - -[[package]] -name = "futures-sink" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" - -[[package]] -name = "futures-task" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" - -[[package]] -name = "futures-util" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" -dependencies = [ - "futures-core", - "futures-io", - "futures-macro", - "futures-sink", - "futures-task", - "memchr", - "pin-project-lite", - "slab", -] - -[[package]] -name = "gdk" -version = "0.18.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9f245958c627ac99d8e529166f9823fb3b838d1d41fd2b297af3075093c2691" -dependencies = [ - "cairo-rs", - "gdk-pixbuf", - "gdk-sys", - "gio", - "glib", - "libc", - "pango", -] - -[[package]] -name = "gdk-pixbuf" -version = "0.18.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50e1f5f1b0bfb830d6ccc8066d18db35c487b1b2b1e8589b5dfe9f07e8defaec" -dependencies = [ - "gdk-pixbuf-sys", - "gio", - "glib", - "libc", - "once_cell", -] - -[[package]] -name = "gdk-pixbuf-sys" -version = "0.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f9839ea644ed9c97a34d129ad56d38a25e6756f99f3a88e15cd39c20629caf7" -dependencies = [ - "gio-sys", - "glib-sys", - "gobject-sys", - "libc", - "system-deps", -] - -[[package]] -name = "gdk-sys" -version = "0.18.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c2d13f38594ac1e66619e188c6d5a1adb98d11b2fcf7894fc416ad76aa2f3f7" -dependencies = [ - "cairo-sys-rs", - "gdk-pixbuf-sys", - "gio-sys", - "glib-sys", - "gobject-sys", - "libc", - "pango-sys", - "pkg-config", - "system-deps", -] - -[[package]] -name = "gdkwayland-sys" -version = "0.18.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "140071d506d223f7572b9f09b5e155afbd77428cd5cc7af8f2694c41d98dfe69" -dependencies = [ - "gdk-sys", - "glib-sys", - "gobject-sys", - "libc", - "pkg-config", - "system-deps", -] - -[[package]] -name = "gdkx11" -version = "0.18.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3caa00e14351bebbc8183b3c36690327eb77c49abc2268dd4bd36b856db3fbfe" -dependencies = [ - "gdk", - "gdkx11-sys", - "gio", - "glib", - "libc", - "x11", -] - -[[package]] -name = "gdkx11-sys" -version = "0.18.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e2e7445fe01ac26f11601db260dd8608fe172514eb63b3b5e261ea6b0f4428d" -dependencies = [ - "gdk-sys", - "glib-sys", - "libc", - "system-deps", - "x11", -] - -[[package]] -name = "generic-array" -version = "0.14.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" -dependencies = [ - "typenum", - "version_check", -] - -[[package]] -name = "getrandom" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" -dependencies = [ - "cfg-if", - "js-sys", - "libc", - "wasi", - "wasm-bindgen", -] - -[[package]] -name = "getrandom" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" -dependencies = [ - "cfg-if", - "libc", - "r-efi 5.3.0", - "wasip2", -] - -[[package]] -name = "getrandom" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" -dependencies = [ - "cfg-if", - "js-sys", - "libc", - "r-efi 6.0.0", - "rand_core", - "wasm-bindgen", -] - -[[package]] -name = "gio" -version = "0.18.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4fc8f532f87b79cbc51a79748f16a6828fb784be93145a322fa14d06d354c73" -dependencies = [ - "futures-channel", - "futures-core", - "futures-io", - "futures-util", - "gio-sys", - "glib", - "libc", - "once_cell", - "pin-project-lite", - "smallvec", - "thiserror 1.0.69", -] - -[[package]] -name = "gio-sys" -version = "0.18.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37566df850baf5e4cb0dfb78af2e4b9898d817ed9263d1090a2df958c64737d2" -dependencies = [ - "glib-sys", - "gobject-sys", - "libc", - "system-deps", - "winapi", -] - -[[package]] -name = "glib" -version = "0.18.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "233daaf6e83ae6a12a52055f568f9d7cf4671dabb78ff9560ab6da230ce00ee5" -dependencies = [ - "bitflags 2.13.0", - "futures-channel", - "futures-core", - "futures-executor", - "futures-task", - "futures-util", - "gio-sys", - "glib-macros", - "glib-sys", - "gobject-sys", - "libc", - "memchr", - "once_cell", - "smallvec", - "thiserror 1.0.69", -] - -[[package]] -name = "glib-macros" -version = "0.18.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bb0228f477c0900c880fd78c8759b95c7636dbd7842707f49e132378aa2acdc" -dependencies = [ - "heck 0.4.1", - "proc-macro-crate 2.0.2", - "proc-macro-error", - "proc-macro2", - "quote", - "syn 2.0.118", -] - -[[package]] -name = "glib-sys" -version = "0.18.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "063ce2eb6a8d0ea93d2bf8ba1957e78dbab6be1c2220dd3daca57d5a9d869898" -dependencies = [ - "libc", - "system-deps", -] - -[[package]] -name = "glob" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" - -[[package]] -name = "gobject-sys" -version = "0.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0850127b514d1c4a4654ead6dedadb18198999985908e6ffe4436f53c785ce44" -dependencies = [ - "glib-sys", - "libc", - "system-deps", -] - -[[package]] -name = "gtk" -version = "0.18.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd56fb197bfc42bd5d2751f4f017d44ff59fbb58140c6b49f9b3b2bdab08506a" -dependencies = [ - "atk", - "cairo-rs", - "field-offset", - "futures-channel", - "gdk", - "gdk-pixbuf", - "gio", - "glib", - "gtk-sys", - "gtk3-macros", - "libc", - "pango", - "pkg-config", -] - -[[package]] -name = "gtk-sys" -version = "0.18.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f29a1c21c59553eb7dd40e918be54dccd60c52b049b75119d5d96ce6b624414" -dependencies = [ - "atk-sys", - "cairo-sys-rs", - "gdk-pixbuf-sys", - "gdk-sys", - "gio-sys", - "glib-sys", - "gobject-sys", - "libc", - "pango-sys", - "system-deps", -] - -[[package]] -name = "gtk3-macros" -version = "0.18.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52ff3c5b21f14f0736fed6dcfc0bfb4225ebf5725f3c0209edeec181e4d73e9d" -dependencies = [ - "proc-macro-crate 1.3.1", - "proc-macro-error", - "proc-macro2", - "quote", - "syn 2.0.118", -] - -[[package]] -name = "hashbrown" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" - -[[package]] -name = "hashbrown" -version = "0.17.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" - -[[package]] -name = "heck" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" - -[[package]] -name = "heck" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" - -[[package]] -name = "hex" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" - -[[package]] -name = "html5ever" -version = "0.38.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1054432bae2f14e0061e33d23402fbaa67a921d319d56adc6bcf887ddad1cbc2" -dependencies = [ - "log", - "markup5ever", -] - -[[package]] -name = "http" -version = "1.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6970f50e31d6fc17d3fa27329444bfa74e196cf62e95052a3f6fee181dba6425" -dependencies = [ - "bytes", - "itoa", -] - -[[package]] -name = "http-body" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" -dependencies = [ - "bytes", - "http", -] - -[[package]] -name = "http-body-util" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" -dependencies = [ - "bytes", - "futures-core", - "http", - "http-body", - "pin-project-lite", -] - -[[package]] -name = "httparse" -version = "1.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" - -[[package]] -name = "hyper" -version = "1.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55281c53a1894c864990125767da440a4e630446785086f52523b20033b74498" -dependencies = [ - "atomic-waker", - "bytes", - "futures-channel", - "futures-core", - "http", - "http-body", - "httparse", - "itoa", - "pin-project-lite", - "smallvec", - "tokio", - "want", -] - -[[package]] -name = "hyper-rustls" -version = "0.27.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" -dependencies = [ - "http", - "hyper", - "hyper-util", - "rustls", - "tokio", - "tokio-rustls", - "tower-service", - "webpki-roots", -] - -[[package]] -name = "hyper-util" -version = "0.1.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" -dependencies = [ - "base64 0.22.1", - "bytes", - "futures-channel", - "futures-util", - "http", - "http-body", - "hyper", - "ipnet", - "libc", - "percent-encoding", - "pin-project-lite", - "socket2", - "tokio", - "tower-service", - "tracing", -] - -[[package]] -name = "iana-time-zone" -version = "0.1.65" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" -dependencies = [ - "android_system_properties", - "core-foundation-sys", - "iana-time-zone-haiku", - "js-sys", - "log", - "wasm-bindgen", - "windows-core 0.62.2", -] - -[[package]] -name = "iana-time-zone-haiku" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" -dependencies = [ - "cc", -] - -[[package]] -name = "ico" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e795dff5605e0f04bff85ca41b51a96b83e80b281e96231bcaaf1ac35103371" -dependencies = [ - "byteorder", - "png 0.17.16", -] - -[[package]] -name = "icu_collections" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" -dependencies = [ - "displaydoc", - "potential_utf", - "utf8_iter", - "yoke", - "zerofrom", - "zerovec", -] - -[[package]] -name = "icu_locale_core" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" -dependencies = [ - "displaydoc", - "litemap", - "tinystr", - "writeable", - "zerovec", -] - -[[package]] -name = "icu_normalizer" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" -dependencies = [ - "icu_collections", - "icu_normalizer_data", - "icu_properties", - "icu_provider", - "smallvec", - "zerovec", -] - -[[package]] -name = "icu_normalizer_data" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" - -[[package]] -name = "icu_properties" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" -dependencies = [ - "icu_collections", - "icu_locale_core", - "icu_properties_data", - "icu_provider", - "zerotrie", - "zerovec", -] - -[[package]] -name = "icu_properties_data" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" - -[[package]] -name = "icu_provider" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" -dependencies = [ - "displaydoc", - "icu_locale_core", - "writeable", - "yoke", - "zerofrom", - "zerotrie", - "zerovec", -] - -[[package]] -name = "ident_case" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" - -[[package]] -name = "idna" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" -dependencies = [ - "idna_adapter", - "smallvec", - "utf8_iter", -] - -[[package]] -name = "idna_adapter" -version = "1.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" -dependencies = [ - "icu_normalizer", - "icu_properties", -] - -[[package]] -name = "indexmap" -version = "1.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" -dependencies = [ - "autocfg", - "hashbrown 0.12.3", - "serde", -] - -[[package]] -name = "indexmap" -version = "2.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" -dependencies = [ - "equivalent", - "hashbrown 0.17.1", - "serde", - "serde_core", -] - -[[package]] -name = "infer" -version = "0.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a588916bfdfd92e71cacef98a63d9b1f0d74d6599980d11894290e7ddefffcf7" -dependencies = [ - "cfb", -] - -[[package]] -name = "ipnet" -version = "2.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" - -[[package]] -name = "itoa" -version = "1.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" - -[[package]] -name = "javascriptcore-rs" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca5671e9ffce8ffba57afc24070e906da7fc4b1ba66f2cabebf61bf2ea257fcc" -dependencies = [ - "bitflags 1.3.2", - "glib", - "javascriptcore-rs-sys", -] - -[[package]] -name = "javascriptcore-rs-sys" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af1be78d14ffa4b75b66df31840478fef72b51f8c2465d4ca7c194da9f7a5124" -dependencies = [ - "glib-sys", - "gobject-sys", - "libc", - "system-deps", -] - -[[package]] -name = "jni" -version = "0.21.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" -dependencies = [ - "cesu8", - "cfg-if", - "combine", - "jni-sys 0.3.1", - "log", - "thiserror 1.0.69", - "walkdir", - "windows-sys 0.45.0", -] - -[[package]] -name = "jni-sys" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41a652e1f9b6e0275df1f15b32661cf0d4b78d4d87ddec5e0c3c20f097433258" -dependencies = [ - "jni-sys 0.4.1", -] - -[[package]] -name = "jni-sys" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" -dependencies = [ - "jni-sys-macros", -] - -[[package]] -name = "jni-sys-macros" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" -dependencies = [ - "quote", - "syn 2.0.118", -] - -[[package]] -name = "js-sys" -version = "0.3.103" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102" -dependencies = [ - "cfg-if", - "futures-util", - "wasm-bindgen", -] - -[[package]] -name = "json-patch" -version = "3.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "863726d7afb6bc2590eeff7135d923545e5e964f004c2ccf8716c25e70a86f08" -dependencies = [ - "jsonptr", - "serde", - "serde_json", - "thiserror 1.0.69", -] - -[[package]] -name = "jsonptr" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dea2b27dd239b2556ed7a25ba842fe47fd602e7fc7433c2a8d6106d4d9edd70" -dependencies = [ - "serde", - "serde_json", -] - -[[package]] -name = "keyboard-types" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b750dcadc39a09dbadd74e118f6dd6598df77fa01df0cfcdc52c28dece74528a" -dependencies = [ - "bitflags 2.13.0", - "serde", - "unicode-segmentation", -] - -[[package]] -name = "libappindicator" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03589b9607c868cc7ae54c0b2a22c8dc03dd41692d48f2d7df73615c6a95dc0a" -dependencies = [ - "glib", - "gtk", - "gtk-sys", - "libappindicator-sys", - "log", -] - -[[package]] -name = "libappindicator-sys" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e9ec52138abedcc58dc17a7c6c0c00a2bdb4f3427c7f63fa97fd0d859155caf" -dependencies = [ - "gtk-sys", - "libloading", - "once_cell", -] - -[[package]] -name = "libc" -version = "0.2.186" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" - -[[package]] -name = "libdbus-sys" -version = "0.2.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "328c4789d42200f1eeec05bd86c9c13c7f091d2ba9a6ea35acdf51f31bc0f043" -dependencies = [ - "pkg-config", -] - -[[package]] -name = "libloading" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f" -dependencies = [ - "cfg-if", - "winapi", -] - -[[package]] -name = "libredox" -version = "0.1.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c943259e342f1e06ff2da7a83eabdfe7f92ce10262688dbf1895ff0b3e6e4652" -dependencies = [ - "libc", -] - -[[package]] -name = "litemap" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" - -[[package]] -name = "lock_api" -version = "0.4.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" -dependencies = [ - "scopeguard", -] - -[[package]] -name = "log" -version = "0.4.33" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" - -[[package]] -name = "lru-slab" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" - -[[package]] -name = "markup5ever" -version = "0.38.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8983d30f2915feeaaab2d6babdd6bc7e9ed1a00b66b5e6d74df19aa9c0e91862" -dependencies = [ - "log", - "tendril", - "web_atoms", -] - -[[package]] -name = "memchr" -version = "2.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4" - -[[package]] -name = "memoffset" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" -dependencies = [ - "autocfg", -] - -[[package]] -name = "mime" -version = "0.3.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" - -[[package]] -name = "miniz_oxide" -version = "0.8.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" -dependencies = [ - "adler2", - "simd-adler32", -] - -[[package]] -name = "mio" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02bd0af71c67b473010cbbc60715ee815645a4dc942899111f494b4b737d6fda" -dependencies = [ - "libc", - "wasi", - "windows-sys 0.61.2", -] - -[[package]] -name = "muda" -version = "0.19.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1dd04e60bc0b07438a6771710ee1698f98f6ebbc7f89b61264af1563b8aeb878" -dependencies = [ - "crossbeam-channel", - "dpi", - "gtk", - "keyboard-types", - "objc2", - "objc2-app-kit", - "objc2-core-foundation", - "objc2-foundation", - "once_cell", - "png 0.18.1", - "serde", - "thiserror 2.0.18", - "windows-sys 0.61.2", -] - -[[package]] -name = "ndk" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" -dependencies = [ - "bitflags 2.13.0", - "jni-sys 0.3.1", - "log", - "ndk-sys", - "num_enum", - "raw-window-handle", - "thiserror 1.0.69", -] - -[[package]] -name = "ndk-sys" -version = "0.6.0+11769913" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873" -dependencies = [ - "jni-sys 0.3.1", -] - -[[package]] -name = "new_debug_unreachable" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" - -[[package]] -name = "num-conv" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" - -[[package]] -name = "num-traits" -version = "0.2.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" -dependencies = [ - "autocfg", -] - -[[package]] -name = "num_enum" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26" -dependencies = [ - "num_enum_derive", - "rustversion", -] - -[[package]] -name = "num_enum_derive" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8" -dependencies = [ - "proc-macro-crate 3.5.0", - "proc-macro2", - "quote", - "syn 2.0.118", -] - -[[package]] -name = "objc2" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" -dependencies = [ - "objc2-encode", - "objc2-exception-helper", -] - -[[package]] -name = "objc2-app-kit" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" -dependencies = [ - "bitflags 2.13.0", - "block2", - "objc2", - "objc2-core-foundation", - "objc2-foundation", -] - -[[package]] -name = "objc2-cloud-kit" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73ad74d880bb43877038da939b7427bba67e9dd42004a18b809ba7d87cee241c" -dependencies = [ - "bitflags 2.13.0", - "objc2", - "objc2-foundation", -] - -[[package]] -name = "objc2-core-data" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b402a653efbb5e82ce4df10683b6b28027616a2715e90009947d50b8dd298fa" -dependencies = [ - "objc2", - "objc2-foundation", -] - -[[package]] -name = "objc2-core-foundation" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" -dependencies = [ - "bitflags 2.13.0", - "dispatch2", - "objc2", -] - -[[package]] -name = "objc2-core-graphics" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" -dependencies = [ - "bitflags 2.13.0", - "dispatch2", - "objc2", - "objc2-core-foundation", - "objc2-io-surface", -] - -[[package]] -name = "objc2-core-image" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5d563b38d2b97209f8e861173de434bd0214cf020e3423a52624cd1d989f006" -dependencies = [ - "objc2", - "objc2-foundation", -] - -[[package]] -name = "objc2-core-location" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca347214e24bc973fc025fd0d36ebb179ff30536ed1f80252706db19ee452009" -dependencies = [ - "objc2", - "objc2-foundation", -] - -[[package]] -name = "objc2-core-text" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cde0dfb48d25d2b4862161a4d5fcc0e3c24367869ad306b0c9ec0073bfed92d" -dependencies = [ - "bitflags 2.13.0", - "objc2", - "objc2-core-foundation", - "objc2-core-graphics", -] - -[[package]] -name = "objc2-encode" -version = "4.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" - -[[package]] -name = "objc2-exception-helper" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7a1c5fbb72d7735b076bb47b578523aedc40f3c439bea6dfd595c089d79d98a" -dependencies = [ - "cc", -] - -[[package]] -name = "objc2-foundation" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" -dependencies = [ - "bitflags 2.13.0", - "block2", - "libc", - "objc2", - "objc2-core-foundation", -] - -[[package]] -name = "objc2-io-surface" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" -dependencies = [ - "bitflags 2.13.0", - "objc2", - "objc2-core-foundation", -] - -[[package]] -name = "objc2-quartz-core" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f" -dependencies = [ - "bitflags 2.13.0", - "objc2", - "objc2-core-foundation", - "objc2-foundation", -] - -[[package]] -name = "objc2-ui-kit" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d87d638e33c06f577498cbcc50491496a3ed4246998a7fbba7ccb98b1e7eab22" -dependencies = [ - "bitflags 2.13.0", - "block2", - "objc2", - "objc2-cloud-kit", - "objc2-core-data", - "objc2-core-foundation", - "objc2-core-graphics", - "objc2-core-image", - "objc2-core-location", - "objc2-core-text", - "objc2-foundation", - "objc2-quartz-core", - "objc2-user-notifications", -] - -[[package]] -name = "objc2-user-notifications" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9df9128cbbfef73cda168416ccf7f837b62737d748333bfe9ab71c245d76613e" -dependencies = [ - "objc2", - "objc2-foundation", -] - -[[package]] -name = "objc2-web-kit" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2e5aaab980c433cf470df9d7af96a7b46a9d892d521a2cbbb2f8a4c16751e7f" -dependencies = [ - "bitflags 2.13.0", - "block2", - "objc2", - "objc2-app-kit", - "objc2-core-foundation", - "objc2-foundation", -] - -[[package]] -name = "once_cell" -version = "1.21.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" - -[[package]] -name = "option-ext" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" - -[[package]] -name = "pango" -version = "0.18.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ca27ec1eb0457ab26f3036ea52229edbdb74dee1edd29063f5b9b010e7ebee4" -dependencies = [ - "gio", - "glib", - "libc", - "once_cell", - "pango-sys", -] - -[[package]] -name = "pango-sys" -version = "0.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "436737e391a843e5933d6d9aa102cb126d501e815b83601365a948a518555dc5" -dependencies = [ - "glib-sys", - "gobject-sys", - "libc", - "system-deps", -] - -[[package]] -name = "parking_lot" -version = "0.12.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" -dependencies = [ - "lock_api", - "parking_lot_core", -] - -[[package]] -name = "parking_lot_core" -version = "0.9.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" -dependencies = [ - "cfg-if", - "libc", - "redox_syscall", - "smallvec", - "windows-link 0.2.1", -] - -[[package]] -name = "percent-encoding" -version = "2.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" - -[[package]] -name = "phf" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf" -dependencies = [ - "phf_macros", - "phf_shared", - "serde", -] - -[[package]] -name = "phf_codegen" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49aa7f9d80421bca176ca8dbfebe668cc7a2684708594ec9f3c0db0805d5d6e1" -dependencies = [ - "phf_generator", - "phf_shared", -] - -[[package]] -name = "phf_generator" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737" -dependencies = [ - "fastrand", - "phf_shared", -] - -[[package]] -name = "phf_macros" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "812f032b54b1e759ccd5f8b6677695d5268c588701effba24601f6932f8269ef" -dependencies = [ - "phf_generator", - "phf_shared", - "proc-macro2", - "quote", - "syn 2.0.118", -] - -[[package]] -name = "phf_shared" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266" -dependencies = [ - "siphasher", -] - -[[package]] -name = "pin-project-lite" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" - -[[package]] -name = "pkg-config" -version = "0.3.33" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" - -[[package]] -name = "plist" -version = "1.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "092791278e026273c1b65bbdcfbba3a300f2994c896bd01ab01da613c29c46f1" -dependencies = [ - "base64 0.22.1", - "indexmap 2.14.0", - "quick-xml", - "serde", - "time", -] - -[[package]] -name = "png" -version = "0.17.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526" -dependencies = [ - "bitflags 1.3.2", - "crc32fast", - "fdeflate", - "flate2", - "miniz_oxide", -] - -[[package]] -name = "png" -version = "0.18.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" -dependencies = [ - "bitflags 2.13.0", - "crc32fast", - "fdeflate", - "flate2", - "miniz_oxide", -] - -[[package]] -name = "potential_utf" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" -dependencies = [ - "zerovec", -] - -[[package]] -name = "powerfmt" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" - -[[package]] -name = "precomputed-hash" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" - -[[package]] -name = "proc-macro-crate" -version = "1.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" -dependencies = [ - "once_cell", - "toml_edit 0.19.15", -] - -[[package]] -name = "proc-macro-crate" -version = "2.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b00f26d3400549137f92511a46ac1cd8ce37cb5598a96d382381458b992a5d24" -dependencies = [ - "toml_datetime 0.6.3", - "toml_edit 0.20.2", -] - -[[package]] -name = "proc-macro-crate" -version = "3.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" -dependencies = [ - "toml_edit 0.25.12+spec-1.1.0", -] - -[[package]] -name = "proc-macro-error" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" -dependencies = [ - "proc-macro-error-attr", - "proc-macro2", - "quote", - "syn 1.0.109", - "version_check", -] - -[[package]] -name = "proc-macro-error-attr" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" -dependencies = [ - "proc-macro2", - "quote", - "version_check", -] - -[[package]] -name = "proc-macro2" -version = "1.0.106" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "quick-xml" -version = "0.39.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdcc8dd4e2f670d309a5f0e83fe36dfdc05af317008fea29144da1a2ac858e5e" -dependencies = [ - "memchr", -] - -[[package]] -name = "quinn" -version = "0.11.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c1a41e437b6bbd489372cd4971de128e85c855f56c57f283d20ff016cf7c0a8" -dependencies = [ - "bytes", - "cfg_aliases", - "pin-project-lite", - "quinn-proto", - "quinn-udp", - "rustc-hash", - "rustls", - "socket2", - "thiserror 2.0.18", - "tokio", - "tracing", - "web-time", -] - -[[package]] -name = "quinn-proto" -version = "0.11.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f4bfc015262b9df63c8845072ce59068853ff5872180c2ce2f13038b970e560" -dependencies = [ - "bytes", - "getrandom 0.4.3", - "lru-slab", - "rand", - "rand_pcg", - "ring", - "rustc-hash", - "rustls", - "rustls-pki-types", - "slab", - "thiserror 2.0.18", - "tinyvec", - "tracing", - "web-time", -] - -[[package]] -name = "quinn-udp" -version = "0.5.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35a133f956daabe89a61a685c2649f13d82d5aa4bd5d12d1277e1072a21c0694" -dependencies = [ - "cfg_aliases", - "libc", - "once_cell", - "socket2", - "tracing", - "windows-sys 0.61.2", -] - -[[package]] -name = "quote" -version = "1.0.46" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "r-efi" -version = "5.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" - -[[package]] -name = "r-efi" -version = "6.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" - -[[package]] -name = "rand" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80" -dependencies = [ - "chacha20", - "getrandom 0.4.3", - "rand_core", -] - -[[package]] -name = "rand_core" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" - -[[package]] -name = "rand_pcg" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "caa0f4137e1c0a72f4c651489402276c8e8e1cf081f3b0ba156d2cbeef09e86a" -dependencies = [ - "rand_core", -] - -[[package]] -name = "raw-window-handle" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" - -[[package]] -name = "redox_syscall" -version = "0.5.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" -dependencies = [ - "bitflags 2.13.0", -] - -[[package]] -name = "redox_users" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" -dependencies = [ - "getrandom 0.2.17", - "libredox", - "thiserror 2.0.18", -] - -[[package]] -name = "ref-cast" -version = "1.0.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" -dependencies = [ - "ref-cast-impl", -] - -[[package]] -name = "ref-cast-impl" -version = "1.0.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.118", -] - -[[package]] -name = "regex" -version = "1.12.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1292b7759ae1cb9ec195452d1390a074f0cd8541ab7a5a8c31cd6db45d4a6ba" -dependencies = [ - "aho-corasick", - "memchr", - "regex-automata", - "regex-syntax", -] - -[[package]] -name = "regex-automata" -version = "0.4.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" -dependencies = [ - "aho-corasick", - "memchr", - "regex-syntax", -] - -[[package]] -name = "regex-syntax" -version = "0.8.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" - -[[package]] -name = "reqwest" -version = "0.12.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" -dependencies = [ - "base64 0.22.1", - "bytes", - "futures-channel", - "futures-core", - "futures-util", - "http", - "http-body", - "http-body-util", - "hyper", - "hyper-rustls", - "hyper-util", - "js-sys", - "log", - "percent-encoding", - "pin-project-lite", - "quinn", - "rustls", - "rustls-pki-types", - "serde", - "serde_json", - "serde_urlencoded", - "sync_wrapper", - "tokio", - "tokio-rustls", - "tower", - "tower-http", - "tower-service", - "url", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", - "webpki-roots", -] - -[[package]] -name = "reqwest" -version = "0.13.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "219c5811de6525e5416c7d5d53bb656d3afdbc6c5af816e0802bcfa42dbdc1c3" -dependencies = [ - "base64 0.22.1", - "bytes", - "futures-core", - "futures-util", - "http", - "http-body", - "http-body-util", - "hyper", - "hyper-util", - "js-sys", - "log", - "percent-encoding", - "pin-project-lite", - "serde", - "serde_json", - "sync_wrapper", - "tokio", - "tokio-util", - "tower", - "tower-http", - "tower-service", - "url", - "wasm-bindgen", - "wasm-bindgen-futures", - "wasm-streams", - "web-sys", -] - -[[package]] -name = "rfd" -version = "0.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a15ad77d9e70a92437d8f74c35d99b4e4691128df018833e99f90bcd36152672" -dependencies = [ - "block2", - "dispatch2", - "glib-sys", - "gobject-sys", - "gtk-sys", - "js-sys", - "log", - "objc2", - "objc2-app-kit", - "objc2-core-foundation", - "objc2-foundation", - "raw-window-handle", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", - "windows-sys 0.60.2", -] - -[[package]] -name = "ring" -version = "0.17.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" -dependencies = [ - "cc", - "cfg-if", - "getrandom 0.2.17", - "libc", - "untrusted", - "windows-sys 0.52.0", -] - -[[package]] -name = "rustc-hash" -version = "2.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" - -[[package]] -name = "rustc_version" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" -dependencies = [ - "semver", -] - -[[package]] -name = "rustls" -version = "0.23.41" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b92b125634d9b795e7beca796cc790df15a7fb38323bf3196fda83292d06b1f" -dependencies = [ - "once_cell", - "ring", - "rustls-pki-types", - "rustls-webpki", - "subtle", - "zeroize", -] - -[[package]] -name = "rustls-pki-types" -version = "1.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "764899a24af3980067ee14bc143654f297b22eaebfe3c7b6b211920a5a59b046" -dependencies = [ - "web-time", - "zeroize", -] - -[[package]] -name = "rustls-webpki" -version = "0.103.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" -dependencies = [ - "ring", - "rustls-pki-types", - "untrusted", -] - -[[package]] -name = "rustversion" -version = "1.0.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" - -[[package]] -name = "ryu" -version = "1.0.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" - -[[package]] -name = "same-file" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" -dependencies = [ - "winapi-util", -] - -[[package]] -name = "schemars" -version = "0.8.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fbf2ae1b8bc8e02df939598064d22402220cd5bbcca1c76f7d6a310974d5615" -dependencies = [ - "dyn-clone", - "indexmap 1.9.3", - "schemars_derive", - "serde", - "serde_json", - "url", - "uuid", -] - -[[package]] -name = "schemars" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f" -dependencies = [ - "dyn-clone", - "ref-cast", - "serde", - "serde_json", -] - -[[package]] -name = "schemars" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2b42f36aa1cd011945615b92222f6bf73c599a102a300334cd7f8dbeec726cc" -dependencies = [ - "dyn-clone", - "ref-cast", - "serde", - "serde_json", -] - -[[package]] -name = "schemars_derive" -version = "0.8.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32e265784ad618884abaea0600a9adf15393368d840e0222d101a072f3f7534d" -dependencies = [ - "proc-macro2", - "quote", - "serde_derive_internals", - "syn 2.0.118", -] - -[[package]] -name = "scopeguard" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" - -[[package]] -name = "selectors" -version = "0.36.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5d9c0c92a92d33f08817311cf3f2c29a3538a8240e94a6a3c622ce652d7e00c" -dependencies = [ - "bitflags 2.13.0", - "cssparser", - "derive_more", - "log", - "new_debug_unreachable", - "phf", - "phf_codegen", - "precomputed-hash", - "rustc-hash", - "servo_arc", - "smallvec", -] - -[[package]] -name = "semver" -version = "1.0.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" -dependencies = [ - "serde", - "serde_core", -] - -[[package]] -name = "serde" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" -dependencies = [ - "serde_core", - "serde_derive", -] - -[[package]] -name = "serde-untagged" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9faf48a4a2d2693be24c6289dbe26552776eb7737074e6722891fadbe6c5058" -dependencies = [ - "erased-serde", - "serde", - "serde_core", - "typeid", -] - -[[package]] -name = "serde_core" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde_derive" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.118", -] - -[[package]] -name = "serde_derive_internals" -version = "0.29.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.118", -] - -[[package]] -name = "serde_json" -version = "1.0.150" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" -dependencies = [ - "itoa", - "memchr", - "serde", - "serde_core", - "zmij", -] - -[[package]] -name = "serde_repr" -version = "0.1.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.118", -] - -[[package]] -name = "serde_spanned" -version = "0.6.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" -dependencies = [ - "serde", -] - -[[package]] -name = "serde_spanned" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" -dependencies = [ - "serde_core", -] - -[[package]] -name = "serde_urlencoded" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" -dependencies = [ - "form_urlencoded", - "itoa", - "ryu", - "serde", -] - -[[package]] -name = "serde_with" -version = "3.21.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76a5c54c7310e7b8b9577c286d7e399ddd876c3e12b3ed917a8aabc4b96e9e8c" -dependencies = [ - "base64 0.22.1", - "bs58", - "chrono", - "hex", - "indexmap 1.9.3", - "indexmap 2.14.0", - "schemars 0.9.0", - "schemars 1.2.1", - "serde_core", - "serde_json", - "serde_with_macros", - "time", -] - -[[package]] -name = "serde_with_macros" -version = "3.21.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84d57bc0c8b9a17920c178daa6bb924850d54a9c97ab45194bb8c17ad66bb660" -dependencies = [ - "darling", - "proc-macro2", - "quote", - "syn 2.0.118", -] - -[[package]] -name = "serialize-to-javascript" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04f3666a07a197cdb77cdf306c32be9b7f598d7060d50cfd4d5aa04bfd92f6c5" -dependencies = [ - "serde", - "serde_json", - "serialize-to-javascript-impl", -] - -[[package]] -name = "serialize-to-javascript-impl" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "772ee033c0916d670af7860b6e1ef7d658a4629a6d0b4c8c3e67f09b3765b75d" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.118", -] - -[[package]] -name = "servo_arc" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "170fb83ab34de17dc69aa7c67482b22218ddb85da56546f9bd6b929e32a05930" -dependencies = [ - "stable_deref_trait", -] - -[[package]] -name = "sha2" -version = "0.10.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" -dependencies = [ - "cfg-if", - "cpufeatures 0.2.17", - "digest", -] - -[[package]] -name = "shlex" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" - -[[package]] -name = "simd-adler32" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" - -[[package]] -name = "siphasher" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649" - -[[package]] -name = "slab" -version = "0.4.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" - -[[package]] -name = "smallvec" -version = "1.15.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" - -[[package]] -name = "socket2" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51" -dependencies = [ - "libc", - "windows-sys 0.61.2", -] - -[[package]] -name = "softbuffer" -version = "0.4.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aac18da81ebbf05109ab275b157c22a653bb3c12cf884450179942f81bcbf6c3" -dependencies = [ - "bytemuck", - "js-sys", - "ndk", - "objc2", - "objc2-core-foundation", - "objc2-core-graphics", - "objc2-foundation", - "objc2-quartz-core", - "raw-window-handle", - "redox_syscall", - "tracing", - "wasm-bindgen", - "web-sys", - "windows-sys 0.61.2", -] - -[[package]] -name = "soup3" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "471f924a40f31251afc77450e781cb26d55c0b650842efafc9c6cbd2f7cc4f9f" -dependencies = [ - "futures-channel", - "gio", - "glib", - "libc", - "soup3-sys", -] - -[[package]] -name = "soup3-sys" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ebe8950a680a12f24f15ebe1bf70db7af98ad242d9db43596ad3108aab86c27" -dependencies = [ - "gio-sys", - "glib-sys", - "gobject-sys", - "libc", - "system-deps", -] - -[[package]] -name = "stable_deref_trait" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" - -[[package]] -name = "string_cache" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a18596f8c785a729f2819c0f6a7eae6ebeebdfffbfe4214ae6b087f690e31901" -dependencies = [ - "new_debug_unreachable", - "parking_lot", - "phf_shared", - "precomputed-hash", -] - -[[package]] -name = "string_cache_codegen" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "585635e46db231059f76c5849798146164652513eb9e8ab2685939dd90f29b69" -dependencies = [ - "phf_generator", - "phf_shared", - "proc-macro2", - "quote", -] - -[[package]] -name = "strsim" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" - -[[package]] -name = "subtle" -version = "2.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" - -[[package]] -name = "swift-rs" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4057c98e2e852d51fdcfca832aac7b571f6b351ad159f9eda5db1655f8d0c4d7" -dependencies = [ - "base64 0.21.7", - "serde", - "serde_json", -] - -[[package]] -name = "syn" -version = "1.0.109" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" -dependencies = [ - "proc-macro2", - "unicode-ident", -] - -[[package]] -name = "syn" -version = "2.0.118" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "sync_wrapper" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" -dependencies = [ - "futures-core", -] - -[[package]] -name = "synstructure" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.118", -] - -[[package]] -name = "system-deps" -version = "6.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349" -dependencies = [ - "cfg-expr", - "heck 0.5.0", - "pkg-config", - "toml 0.8.2", - "version-compare", -] - -[[package]] -name = "tao" -version = "0.35.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1c93047acf68669466a34690ac58cca7010bd1b201e1ec86f1fd0a75d3dd4a9" -dependencies = [ - "bitflags 2.13.0", - "block2", - "core-foundation", - "core-graphics", - "crossbeam-channel", - "dbus", - "dispatch2", - "dlopen2", - "dpi", - "gdkwayland-sys", - "gdkx11-sys", - "gtk", - "jni", - "libc", - "log", - "ndk", - "ndk-sys", - "objc2", - "objc2-app-kit", - "objc2-foundation", - "objc2-ui-kit", - "once_cell", - "parking_lot", - "percent-encoding", - "raw-window-handle", - "tao-macros", - "unicode-segmentation", - "url", - "windows", - "windows-core 0.61.2", - "windows-version", - "x11-dl", -] - -[[package]] -name = "tao-macros" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4e16beb8b2ac17db28eab8bca40e62dbfbb34c0fcdc6d9826b11b7b5d047dfd" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.118", -] - -[[package]] -name = "target-lexicon" -version = "0.12.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" - -[[package]] -name = "tauri" -version = "2.11.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "667b20e2726d572dea2de7370da16e188eb06008faf9a92fab7cdc46791190b5" -dependencies = [ - "anyhow", - "bytes", - "cookie", - "dirs", - "dunce", - "embed_plist", - "getrandom 0.3.4", - "glob", - "gtk", - "heck 0.5.0", - "http", - "jni", - "libc", - "log", - "mime", - "muda", - "objc2", - "objc2-app-kit", - "objc2-foundation", - "objc2-ui-kit", - "objc2-web-kit", - "percent-encoding", - "plist", - "raw-window-handle", - "reqwest 0.13.4", - "serde", - "serde_json", - "serde_repr", - "serialize-to-javascript", - "swift-rs", - "tauri-build", - "tauri-macros", - "tauri-runtime", - "tauri-runtime-wry", - "tauri-utils", - "thiserror 2.0.18", - "tokio", - "tray-icon", - "url", - "webkit2gtk", - "webview2-com", - "window-vibrancy", - "windows", -] - -[[package]] -name = "tauri-build" -version = "2.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc9ce40b16101cb6ea63d3e221567affd1c3a9205f95d7bc574941a10636b632" -dependencies = [ - "anyhow", - "cargo_toml", - "dirs", - "glob", - "heck 0.5.0", - "json-patch", - "schemars 0.8.22", - "semver", - "serde", - "serde_json", - "tauri-utils", - "tauri-winres", - "walkdir", -] - -[[package]] -name = "tauri-codegen" -version = "2.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08279169ff42f8fc45a1dbc9dcae888893ba95288142e5880c59b93a26d2cfc5" -dependencies = [ - "base64 0.22.1", - "brotli", - "ico", - "json-patch", - "plist", - "png 0.17.16", - "proc-macro2", - "quote", - "semver", - "serde", - "serde_json", - "sha2", - "syn 2.0.118", - "tauri-utils", - "thiserror 2.0.18", - "time", - "url", - "uuid", - "walkdir", -] - -[[package]] -name = "tauri-macros" -version = "2.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8b394794f399a421811d06966343e7933fcae92d59f5180b9388d1174497a45" -dependencies = [ - "heck 0.5.0", - "proc-macro2", - "quote", - "syn 2.0.118", - "tauri-codegen", - "tauri-utils", -] - -[[package]] -name = "tauri-plugin" -version = "2.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74be5dd4bed9afbd145e5716b5fa2ec28cbc29c34ffa61c258c9273d896c8020" -dependencies = [ - "anyhow", - "glob", - "plist", - "schemars 0.8.22", - "serde", - "serde_json", - "tauri-utils", - "walkdir", -] - -[[package]] -name = "tauri-plugin-dialog" -version = "2.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65981abb771e74e571a38196c3baa11c459379164791eba0e67abc1a5fac9884" -dependencies = [ - "log", - "raw-window-handle", - "rfd", - "serde", - "serde_json", - "tauri", - "tauri-plugin", - "tauri-plugin-fs", - "thiserror 2.0.18", - "url", -] - -[[package]] -name = "tauri-plugin-fs" -version = "2.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7ecc274121aca0c036a2b42d1cbe83d368d348f54e0bb8a735c2b1548e8f371" -dependencies = [ - "anyhow", - "dunce", - "glob", - "log", - "objc2-foundation", - "percent-encoding", - "schemars 0.8.22", - "serde", - "serde_json", - "serde_repr", - "tauri", - "tauri-plugin", - "tauri-utils", - "thiserror 2.0.18", - "toml 1.1.2+spec-1.1.0", - "url", -] - -[[package]] -name = "tauri-runtime" -version = "2.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0b4bc95aed361b0019067d189a1174a603d460d0f6c72606512d59fc9c12ec8" -dependencies = [ - "cookie", - "dpi", - "gtk", - "http", - "jni", - "objc2", - "objc2-ui-kit", - "objc2-web-kit", - "raw-window-handle", - "serde", - "serde_json", - "tauri-utils", - "thiserror 2.0.18", - "url", - "webkit2gtk", - "webview2-com", - "windows", -] - -[[package]] -name = "tauri-runtime-wry" -version = "2.11.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e6fac707727b7a2f48e4ded90976324267371073edbb415ffb73bb0458d203f" -dependencies = [ - "gtk", - "http", - "jni", - "log", - "objc2", - "objc2-app-kit", - "once_cell", - "percent-encoding", - "raw-window-handle", - "softbuffer", - "tao", - "tauri-runtime", - "tauri-utils", - "url", - "webkit2gtk", - "webview2-com", - "windows", - "wry", -] - -[[package]] -name = "tauri-utils" -version = "2.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e176a18e67764923c4f1ce66f25ae4abe5f688384d5eb1a0fa6c77f3d90f887" -dependencies = [ - "anyhow", - "brotli", - "cargo_metadata", - "ctor", - "dom_query", - "dunce", - "glob", - "http", - "infer", - "json-patch", - "log", - "memchr", - "phf", - "plist", - "proc-macro2", - "quote", - "regex", - "schemars 0.8.22", - "semver", - "serde", - "serde-untagged", - "serde_json", - "serde_with", - "swift-rs", - "thiserror 2.0.18", - "toml 1.1.2+spec-1.1.0", - "url", - "urlpattern", - "uuid", - "walkdir", -] - -[[package]] -name = "tauri-winres" -version = "0.3.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc65d45c68858bfe420dd29e834b5d15dbecf8a07a8a16cf4d532c7b1f69d4b6" -dependencies = [ - "dunce", - "embed-resource", - "toml 1.1.2+spec-1.1.0", -] - -[[package]] -name = "tendril" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4790fc369d5a530f4b544b094e31388b9b3a37c0f4652ade4505945f5660d24" -dependencies = [ - "new_debug_unreachable", - "utf-8", -] - -[[package]] -name = "thiserror" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" -dependencies = [ - "thiserror-impl 1.0.69", -] - -[[package]] -name = "thiserror" -version = "2.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" -dependencies = [ - "thiserror-impl 2.0.18", -] - -[[package]] -name = "thiserror-impl" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.118", -] - -[[package]] -name = "thiserror-impl" -version = "2.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.118", -] - -[[package]] -name = "time" -version = "0.3.53" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18dfaaeddcb932337b5e7866ee7d0ce9b76d2fd092997146f187ec09b4558a50" -dependencies = [ - "deranged", - "num-conv", - "powerfmt", - "serde_core", - "time-core", - "time-macros", -] - -[[package]] -name = "time-core" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" - -[[package]] -name = "time-macros" -version = "0.2.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c431b87111666e491a90baa837f914fb45cd5dc3c268591b0220ff5057f2085f" -dependencies = [ - "num-conv", - "time-core", -] - -[[package]] -name = "tinystr" -version = "0.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" -dependencies = [ - "displaydoc", - "zerovec", -] - -[[package]] -name = "tinyvec" -version = "1.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" -dependencies = [ - "tinyvec_macros", -] - -[[package]] -name = "tinyvec_macros" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" - -[[package]] -name = "tokio" -version = "1.52.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" -dependencies = [ - "bytes", - "libc", - "mio", - "pin-project-lite", - "socket2", - "windows-sys 0.61.2", -] - -[[package]] -name = "tokio-rustls" -version = "0.26.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" -dependencies = [ - "rustls", - "tokio", -] - -[[package]] -name = "tokio-util" -version = "0.7.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" -dependencies = [ - "bytes", - "futures-core", - "futures-sink", - "pin-project-lite", - "tokio", -] - -[[package]] -name = "toml" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "185d8ab0dfbb35cf1399a6344d8484209c088f75f8f68230da55d48d95d43e3d" -dependencies = [ - "serde", - "serde_spanned 0.6.9", - "toml_datetime 0.6.3", - "toml_edit 0.20.2", -] - -[[package]] -name = "toml" -version = "0.9.12+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863" -dependencies = [ - "indexmap 2.14.0", - "serde_core", - "serde_spanned 1.1.1", - "toml_datetime 0.7.5+spec-1.1.0", - "toml_parser", - "toml_writer", - "winnow 0.7.15", -] - -[[package]] -name = "toml" -version = "1.1.2+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81f3d15e84cbcd896376e6730314d59fb5a87f31e4b038454184435cd57defee" -dependencies = [ - "indexmap 2.14.0", - "serde_core", - "serde_spanned 1.1.1", - "toml_datetime 1.1.1+spec-1.1.0", - "toml_parser", - "toml_writer", - "winnow 1.0.3", -] - -[[package]] -name = "toml_datetime" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" -dependencies = [ - "serde", -] - -[[package]] -name = "toml_datetime" -version = "0.7.5+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" -dependencies = [ - "serde_core", -] - -[[package]] -name = "toml_datetime" -version = "1.1.1+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" -dependencies = [ - "serde_core", -] - -[[package]] -name = "toml_edit" -version = "0.19.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" -dependencies = [ - "indexmap 2.14.0", - "toml_datetime 0.6.3", - "winnow 0.5.40", -] - -[[package]] -name = "toml_edit" -version = "0.20.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "396e4d48bbb2b7554c944bde63101b5ae446cff6ec4a24227428f15eb72ef338" -dependencies = [ - "indexmap 2.14.0", - "serde", - "serde_spanned 0.6.9", - "toml_datetime 0.6.3", - "winnow 0.5.40", -] - -[[package]] -name = "toml_edit" -version = "0.25.12+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2153edc6955a6c354fad8f5efd38b6a8769bdccf9fe50f8e1329f81b0baa5d7" -dependencies = [ - "indexmap 2.14.0", - "toml_datetime 1.1.1+spec-1.1.0", - "toml_parser", - "winnow 1.0.3", -] - -[[package]] -name = "toml_parser" -version = "1.1.2+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" -dependencies = [ - "winnow 1.0.3", -] - -[[package]] -name = "toml_writer" -version = "1.1.1+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db" - -[[package]] -name = "tower" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" -dependencies = [ - "futures-core", - "futures-util", - "pin-project-lite", - "sync_wrapper", - "tokio", - "tower-layer", - "tower-service", -] - -[[package]] -name = "tower-http" -version = "0.6.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" -dependencies = [ - "bitflags 2.13.0", - "bytes", - "futures-util", - "http", - "http-body", - "pin-project-lite", - "tower", - "tower-layer", - "tower-service", - "url", -] - -[[package]] -name = "tower-layer" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" - -[[package]] -name = "tower-service" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" - -[[package]] -name = "tracing" -version = "0.1.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" -dependencies = [ - "pin-project-lite", - "tracing-core", -] - -[[package]] -name = "tracing-core" -version = "0.1.36" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" -dependencies = [ - "once_cell", -] - -[[package]] -name = "tray-icon" -version = "0.24.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65ba1e5f6b9ef9fd87e21b9c6f351554dbd717960089168fcfdef854686961dc" -dependencies = [ - "crossbeam-channel", - "dirs", - "libappindicator", - "muda", - "objc2", - "objc2-app-kit", - "objc2-core-foundation", - "objc2-core-graphics", - "objc2-foundation", - "once_cell", - "png 0.18.1", - "serde", - "thiserror 2.0.18", - "windows-sys 0.61.2", -] - -[[package]] -name = "try-lock" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" - -[[package]] -name = "typeid" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" - -[[package]] -name = "typenum" -version = "1.20.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" - -[[package]] -name = "unic-char-property" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8c57a407d9b6fa02b4795eb81c5b6652060a15a7903ea981f3d723e6c0be221" -dependencies = [ - "unic-char-range", -] - -[[package]] -name = "unic-char-range" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0398022d5f700414f6b899e10b8348231abf9173fa93144cbc1a43b9793c1fbc" - -[[package]] -name = "unic-common" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80d7ff825a6a654ee85a63e80f92f054f904f21e7d12da4e22f9834a4aaa35bc" - -[[package]] -name = "unic-ucd-ident" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e230a37c0381caa9219d67cf063aa3a375ffed5bf541a452db16e744bdab6987" -dependencies = [ - "unic-char-property", - "unic-char-range", - "unic-ucd-version", -] - -[[package]] -name = "unic-ucd-version" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96bd2f2237fe450fcd0a1d2f5f4e91711124f7857ba2e964247776ebeeb7b0c4" -dependencies = [ - "unic-common", -] - -[[package]] -name = "unicode-ident" -version = "1.0.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" - -[[package]] -name = "unicode-segmentation" -version = "1.13.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" - -[[package]] -name = "untrusted" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" - -[[package]] -name = "url" -version = "2.5.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" -dependencies = [ - "form_urlencoded", - "idna", - "percent-encoding", - "serde", - "serde_derive", -] - -[[package]] -name = "urlpattern" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70acd30e3aa1450bc2eece896ce2ad0d178e9c079493819301573dae3c37ba6d" -dependencies = [ - "regex", - "serde", - "unic-ucd-ident", - "url", -] - -[[package]] -name = "utf-8" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" - -[[package]] -name = "utf8_iter" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" - -[[package]] -name = "uuid" -version = "1.23.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf80a72845275afea99e7f2b434723d3bc7e38470fcd1c7ed39a599c73319a53" -dependencies = [ - "getrandom 0.4.3", - "js-sys", - "serde_core", - "wasm-bindgen", -] - -[[package]] -name = "version-compare" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03c2856837ef78f57382f06b2b8563a2f512f7185d732608fd9176cb3b8edf0e" - -[[package]] -name = "version_check" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" - -[[package]] -name = "vpn-proxy-windows-client" -version = "0.1.0" -dependencies = [ - "base64 0.22.1", - "reqwest 0.12.28", - "serde", - "serde_json", - "tauri", - "tauri-build", - "tauri-plugin-dialog", - "url", -] - -[[package]] -name = "vswhom" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be979b7f07507105799e854203b470ff7c78a1639e330a58f183b5fea574608b" -dependencies = [ - "libc", - "vswhom-sys", -] - -[[package]] -name = "vswhom-sys" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb067e4cbd1ff067d1df46c9194b5de0e98efd2810bbc95c5d5e5f25a3231150" -dependencies = [ - "cc", - "libc", -] - -[[package]] -name = "walkdir" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" -dependencies = [ - "same-file", - "winapi-util", -] - -[[package]] -name = "want" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" -dependencies = [ - "try-lock", -] - -[[package]] -name = "wasi" -version = "0.11.1+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" - -[[package]] -name = "wasip2" -version = "1.0.4+wasi-0.2.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" -dependencies = [ - "wit-bindgen", -] - -[[package]] -name = "wasm-bindgen" -version = "0.2.126" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4" -dependencies = [ - "cfg-if", - "once_cell", - "rustversion", - "wasm-bindgen-macro", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-futures" -version = "0.4.76" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c62df1340f32221cb9c54d6a27b030e3dba64361d4a95bed55f9aacb44da291d" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "wasm-bindgen-macro" -version = "0.2.126" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1" -dependencies = [ - "quote", - "wasm-bindgen-macro-support", -] - -[[package]] -name = "wasm-bindgen-macro-support" -version = "0.2.126" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e" -dependencies = [ - "bumpalo", - "proc-macro2", - "quote", - "syn 2.0.118", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-shared" -version = "0.2.126" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "wasm-streams" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d1ec4f6517c9e11ae630e200b2b65d193279042e28edd4a2cda233e46670bbb" -dependencies = [ - "futures-util", - "js-sys", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", -] - -[[package]] -name = "web-sys" -version = "0.3.103" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8622dcb61c0bcc9fffa6938bed81210af2da9a7e4a1a834b2e37a59b6dfb6141" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "web-time" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "web_atoms" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "075474b12bcb3d2e3d4546580e9de478eeeead668a1761e2a8860c836b7ef297" -dependencies = [ - "phf", - "phf_codegen", - "string_cache", - "string_cache_codegen", -] - -[[package]] -name = "webkit2gtk" -version = "2.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1027150013530fb2eaf806408df88461ae4815a45c541c8975e61d6f2fc4793" -dependencies = [ - "bitflags 1.3.2", - "cairo-rs", - "gdk", - "gdk-sys", - "gio", - "gio-sys", - "glib", - "glib-sys", - "gobject-sys", - "gtk", - "gtk-sys", - "javascriptcore-rs", - "libc", - "once_cell", - "soup3", - "webkit2gtk-sys", -] - -[[package]] -name = "webkit2gtk-sys" -version = "2.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "916a5f65c2ef0dfe12fff695960a2ec3d4565359fdbb2e9943c974e06c734ea5" -dependencies = [ - "bitflags 1.3.2", - "cairo-sys-rs", - "gdk-sys", - "gio-sys", - "glib-sys", - "gobject-sys", - "gtk-sys", - "javascriptcore-rs-sys", - "libc", - "pkg-config", - "soup3-sys", - "system-deps", -] - -[[package]] -name = "webpki-roots" -version = "1.0.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf85cb06032201fa7c6f829d7db5a7e5aa45bcc0655327713065f6f0576731bf" -dependencies = [ - "rustls-pki-types", -] - -[[package]] -name = "webview2-com" -version = "0.38.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7130243a7a5b33c54a444e54842e6a9e133de08b5ad7b5861cd8ed9a6a5bc96a" -dependencies = [ - "webview2-com-macros", - "webview2-com-sys", - "windows", - "windows-core 0.61.2", - "windows-implement", - "windows-interface", -] - -[[package]] -name = "webview2-com-macros" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67a921c1b6914c367b2b823cd4cde6f96beec77d30a939c8199bb377cf9b9b54" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.118", -] - -[[package]] -name = "webview2-com-sys" -version = "0.38.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "381336cfffd772377d291702245447a5251a2ffa5bad679c99e61bc48bacbf9c" -dependencies = [ - "thiserror 2.0.18", - "windows", - "windows-core 0.61.2", -] - -[[package]] -name = "winapi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" -dependencies = [ - "winapi-i686-pc-windows-gnu", - "winapi-x86_64-pc-windows-gnu", -] - -[[package]] -name = "winapi-i686-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" - -[[package]] -name = "winapi-util" -version = "0.1.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" -dependencies = [ - "windows-sys 0.61.2", -] - -[[package]] -name = "winapi-x86_64-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" - -[[package]] -name = "window-vibrancy" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9bec5a31f3f9362f2258fd0e9c9dd61a9ca432e7306cc78c444258f0dce9a9c" -dependencies = [ - "objc2", - "objc2-app-kit", - "objc2-core-foundation", - "objc2-foundation", - "raw-window-handle", - "windows-sys 0.59.0", - "windows-version", -] - -[[package]] -name = "windows" -version = "0.61.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" -dependencies = [ - "windows-collections", - "windows-core 0.61.2", - "windows-future", - "windows-link 0.1.3", - "windows-numerics", -] - -[[package]] -name = "windows-collections" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" -dependencies = [ - "windows-core 0.61.2", -] - -[[package]] -name = "windows-core" -version = "0.61.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" -dependencies = [ - "windows-implement", - "windows-interface", - "windows-link 0.1.3", - "windows-result 0.3.4", - "windows-strings 0.4.2", -] - -[[package]] -name = "windows-core" -version = "0.62.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" -dependencies = [ - "windows-implement", - "windows-interface", - "windows-link 0.2.1", - "windows-result 0.4.1", - "windows-strings 0.5.1", -] - -[[package]] -name = "windows-future" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" -dependencies = [ - "windows-core 0.61.2", - "windows-link 0.1.3", - "windows-threading", -] - -[[package]] -name = "windows-implement" -version = "0.60.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.118", -] - -[[package]] -name = "windows-interface" -version = "0.59.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.118", -] - -[[package]] -name = "windows-link" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" - -[[package]] -name = "windows-link" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" - -[[package]] -name = "windows-numerics" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" -dependencies = [ - "windows-core 0.61.2", - "windows-link 0.1.3", -] - -[[package]] -name = "windows-result" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" -dependencies = [ - "windows-link 0.1.3", -] - -[[package]] -name = "windows-result" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" -dependencies = [ - "windows-link 0.2.1", -] - -[[package]] -name = "windows-strings" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" -dependencies = [ - "windows-link 0.1.3", -] - -[[package]] -name = "windows-strings" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" -dependencies = [ - "windows-link 0.2.1", -] - -[[package]] -name = "windows-sys" -version = "0.45.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" -dependencies = [ - "windows-targets 0.42.2", -] - -[[package]] -name = "windows-sys" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" -dependencies = [ - "windows-targets 0.52.6", -] - -[[package]] -name = "windows-sys" -version = "0.59.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" -dependencies = [ - "windows-targets 0.52.6", -] - -[[package]] -name = "windows-sys" -version = "0.60.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" -dependencies = [ - "windows-targets 0.53.5", -] - -[[package]] -name = "windows-sys" -version = "0.61.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" -dependencies = [ - "windows-link 0.2.1", -] - -[[package]] -name = "windows-targets" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" -dependencies = [ - "windows_aarch64_gnullvm 0.42.2", - "windows_aarch64_msvc 0.42.2", - "windows_i686_gnu 0.42.2", - "windows_i686_msvc 0.42.2", - "windows_x86_64_gnu 0.42.2", - "windows_x86_64_gnullvm 0.42.2", - "windows_x86_64_msvc 0.42.2", -] - -[[package]] -name = "windows-targets" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" -dependencies = [ - "windows_aarch64_gnullvm 0.52.6", - "windows_aarch64_msvc 0.52.6", - "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm 0.52.6", - "windows_i686_msvc 0.52.6", - "windows_x86_64_gnu 0.52.6", - "windows_x86_64_gnullvm 0.52.6", - "windows_x86_64_msvc 0.52.6", -] - -[[package]] -name = "windows-targets" -version = "0.53.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" -dependencies = [ - "windows-link 0.2.1", - "windows_aarch64_gnullvm 0.53.1", - "windows_aarch64_msvc 0.53.1", - "windows_i686_gnu 0.53.1", - "windows_i686_gnullvm 0.53.1", - "windows_i686_msvc 0.53.1", - "windows_x86_64_gnu 0.53.1", - "windows_x86_64_gnullvm 0.53.1", - "windows_x86_64_msvc 0.53.1", -] - -[[package]] -name = "windows-threading" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6" -dependencies = [ - "windows-link 0.1.3", -] - -[[package]] -name = "windows-version" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4060a1da109b9d0326b7262c8e12c84df67cc0dbc9e33cf49e01ccc2eb63631" -dependencies = [ - "windows-link 0.2.1", -] - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" - -[[package]] -name = "windows_i686_gnu" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" - -[[package]] -name = "windows_i686_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" - -[[package]] -name = "windows_i686_gnu" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" - -[[package]] -name = "windows_i686_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" - -[[package]] -name = "windows_i686_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" - -[[package]] -name = "windows_i686_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" - -[[package]] -name = "windows_i686_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" - -[[package]] -name = "windows_i686_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" - -[[package]] -name = "winnow" -version = "0.5.40" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" -dependencies = [ - "memchr", -] - -[[package]] -name = "winnow" -version = "0.7.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" - -[[package]] -name = "winnow" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1" -dependencies = [ - "memchr", -] - -[[package]] -name = "winreg" -version = "0.55.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb5a765337c50e9ec252c2069be9bf91c7df47afb103b642ba3a53bf8101be97" -dependencies = [ - "cfg-if", - "windows-sys 0.59.0", -] - -[[package]] -name = "wit-bindgen" -version = "0.57.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" - -[[package]] -name = "writeable" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" - -[[package]] -name = "wry" -version = "0.55.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "186f9871daa55fd9c016578b810d149de58367113db7fb72b462d2323ce19514" -dependencies = [ - "base64 0.22.1", - "block2", - "cookie", - "crossbeam-channel", - "dirs", - "dom_query", - "dpi", - "dunce", - "gdkx11", - "gtk", - "http", - "javascriptcore-rs", - "jni", - "libc", - "ndk", - "objc2", - "objc2-app-kit", - "objc2-core-foundation", - "objc2-foundation", - "objc2-ui-kit", - "objc2-web-kit", - "once_cell", - "percent-encoding", - "raw-window-handle", - "sha2", - "soup3", - "tao-macros", - "thiserror 2.0.18", - "url", - "webkit2gtk", - "webkit2gtk-sys", - "webview2-com", - "windows", - "windows-core 0.61.2", - "windows-version", - "x11-dl", -] - -[[package]] -name = "x11" -version = "2.21.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "502da5464ccd04011667b11c435cb992822c2c0dbde1770c988480d312a0db2e" -dependencies = [ - "libc", - "pkg-config", -] - -[[package]] -name = "x11-dl" -version = "2.21.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38735924fedd5314a6e548792904ed8c6de6636285cb9fec04d5b1db85c1516f" -dependencies = [ - "libc", - "once_cell", - "pkg-config", -] - -[[package]] -name = "yoke" -version = "0.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" -dependencies = [ - "stable_deref_trait", - "yoke-derive", - "zerofrom", -] - -[[package]] -name = "yoke-derive" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.118", - "synstructure", -] - -[[package]] -name = "zerofrom" -version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" -dependencies = [ - "zerofrom-derive", -] - -[[package]] -name = "zerofrom-derive" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.118", - "synstructure", -] - -[[package]] -name = "zeroize" -version = "1.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" - -[[package]] -name = "zerotrie" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" -dependencies = [ - "displaydoc", - "yoke", - "zerofrom", -] - -[[package]] -name = "zerovec" -version = "0.11.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" -dependencies = [ - "yoke", - "zerofrom", - "zerovec-derive", -] - -[[package]] -name = "zerovec-derive" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.118", -] - -[[package]] -name = "zmij" -version = "1.0.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/apps/windows-client/src-tauri/Cargo.toml b/apps/windows-client/src-tauri/Cargo.toml deleted file mode 100644 index 3eed604..0000000 --- a/apps/windows-client/src-tauri/Cargo.toml +++ /dev/null @@ -1,22 +0,0 @@ -[package] -name = "vpn-proxy-windows-client" -version = "0.1.0" -description = "Standalone Windows desktop proxy management app for VPN Proxy." -authors = ["VPN Proxy"] -edition = "2021" - -[lib] -name = "vpn_proxy_windows_client_lib" -crate-type = ["staticlib", "cdylib", "rlib"] - -[build-dependencies] -tauri-build = { version = "2", features = [] } - -[dependencies] -tauri = { version = "2", features = [] } -serde = { version = "1", features = ["derive"] } -serde_json = "1" -tauri-plugin-dialog = "2.7.1" -base64 = "0.22" -reqwest = { version = "0.12", default-features = false, features = ["blocking", "rustls-tls"] } -url = "2" diff --git a/apps/windows-client/src-tauri/build.rs b/apps/windows-client/src-tauri/build.rs deleted file mode 100644 index 261851f..0000000 --- a/apps/windows-client/src-tauri/build.rs +++ /dev/null @@ -1,3 +0,0 @@ -fn main() { - tauri_build::build(); -} diff --git a/apps/windows-client/src-tauri/capabilities/default.json b/apps/windows-client/src-tauri/capabilities/default.json deleted file mode 100644 index eb4bbd8..0000000 --- a/apps/windows-client/src-tauri/capabilities/default.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "$schema": "../gen/schemas/desktop-schema.json", - "identifier": "default", - "description": "Default capability for the main VPN Proxy Windows shell. Task 8 keeps helper/install launch explicit: no shell or sidecar permission is granted here until a packaged helper is declared.", - "windows": ["main"], - "permissions": ["core:default", "dialog:allow-open"] -} diff --git a/apps/windows-client/src-tauri/gen/schemas/acl-manifests.json b/apps/windows-client/src-tauri/gen/schemas/acl-manifests.json deleted file mode 100644 index f2f210a..0000000 --- a/apps/windows-client/src-tauri/gen/schemas/acl-manifests.json +++ /dev/null @@ -1 +0,0 @@ -{"core":{"default_permission":{"identifier":"default","description":"Default core plugins set.","permissions":["core:path:default","core:event:default","core:window:default","core:webview:default","core:app:default","core:image:default","core:resources:default","core:menu:default","core:tray:default"]},"permissions":{},"permission_sets":{},"global_scope_schema":null},"core:app":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-version","allow-name","allow-tauri-version","allow-identifier","allow-bundle-type","allow-register-listener","allow-remove-listener","allow-supports-multiple-windows"]},"permissions":{"allow-app-hide":{"identifier":"allow-app-hide","description":"Enables the app_hide command without any pre-configured scope.","commands":{"allow":["app_hide"],"deny":[]}},"allow-app-show":{"identifier":"allow-app-show","description":"Enables the app_show command without any pre-configured scope.","commands":{"allow":["app_show"],"deny":[]}},"allow-bundle-type":{"identifier":"allow-bundle-type","description":"Enables the bundle_type command without any pre-configured scope.","commands":{"allow":["bundle_type"],"deny":[]}},"allow-default-window-icon":{"identifier":"allow-default-window-icon","description":"Enables the default_window_icon command without any pre-configured scope.","commands":{"allow":["default_window_icon"],"deny":[]}},"allow-fetch-data-store-identifiers":{"identifier":"allow-fetch-data-store-identifiers","description":"Enables the fetch_data_store_identifiers command without any pre-configured scope.","commands":{"allow":["fetch_data_store_identifiers"],"deny":[]}},"allow-identifier":{"identifier":"allow-identifier","description":"Enables the identifier command without any pre-configured scope.","commands":{"allow":["identifier"],"deny":[]}},"allow-name":{"identifier":"allow-name","description":"Enables the name command without any pre-configured scope.","commands":{"allow":["name"],"deny":[]}},"allow-register-listener":{"identifier":"allow-register-listener","description":"Enables the register_listener command without any pre-configured scope.","commands":{"allow":["register_listener"],"deny":[]}},"allow-remove-data-store":{"identifier":"allow-remove-data-store","description":"Enables the remove_data_store command without any pre-configured scope.","commands":{"allow":["remove_data_store"],"deny":[]}},"allow-remove-listener":{"identifier":"allow-remove-listener","description":"Enables the remove_listener command without any pre-configured scope.","commands":{"allow":["remove_listener"],"deny":[]}},"allow-set-app-theme":{"identifier":"allow-set-app-theme","description":"Enables the set_app_theme command without any pre-configured scope.","commands":{"allow":["set_app_theme"],"deny":[]}},"allow-set-dock-visibility":{"identifier":"allow-set-dock-visibility","description":"Enables the set_dock_visibility command without any pre-configured scope.","commands":{"allow":["set_dock_visibility"],"deny":[]}},"allow-supports-multiple-windows":{"identifier":"allow-supports-multiple-windows","description":"Enables the supports_multiple_windows command without any pre-configured scope.","commands":{"allow":["supports_multiple_windows"],"deny":[]}},"allow-tauri-version":{"identifier":"allow-tauri-version","description":"Enables the tauri_version command without any pre-configured scope.","commands":{"allow":["tauri_version"],"deny":[]}},"allow-version":{"identifier":"allow-version","description":"Enables the version command without any pre-configured scope.","commands":{"allow":["version"],"deny":[]}},"deny-app-hide":{"identifier":"deny-app-hide","description":"Denies the app_hide command without any pre-configured scope.","commands":{"allow":[],"deny":["app_hide"]}},"deny-app-show":{"identifier":"deny-app-show","description":"Denies the app_show command without any pre-configured scope.","commands":{"allow":[],"deny":["app_show"]}},"deny-bundle-type":{"identifier":"deny-bundle-type","description":"Denies the bundle_type command without any pre-configured scope.","commands":{"allow":[],"deny":["bundle_type"]}},"deny-default-window-icon":{"identifier":"deny-default-window-icon","description":"Denies the default_window_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["default_window_icon"]}},"deny-fetch-data-store-identifiers":{"identifier":"deny-fetch-data-store-identifiers","description":"Denies the fetch_data_store_identifiers command without any pre-configured scope.","commands":{"allow":[],"deny":["fetch_data_store_identifiers"]}},"deny-identifier":{"identifier":"deny-identifier","description":"Denies the identifier command without any pre-configured scope.","commands":{"allow":[],"deny":["identifier"]}},"deny-name":{"identifier":"deny-name","description":"Denies the name command without any pre-configured scope.","commands":{"allow":[],"deny":["name"]}},"deny-register-listener":{"identifier":"deny-register-listener","description":"Denies the register_listener command without any pre-configured scope.","commands":{"allow":[],"deny":["register_listener"]}},"deny-remove-data-store":{"identifier":"deny-remove-data-store","description":"Denies the remove_data_store command without any pre-configured scope.","commands":{"allow":[],"deny":["remove_data_store"]}},"deny-remove-listener":{"identifier":"deny-remove-listener","description":"Denies the remove_listener command without any pre-configured scope.","commands":{"allow":[],"deny":["remove_listener"]}},"deny-set-app-theme":{"identifier":"deny-set-app-theme","description":"Denies the set_app_theme command without any pre-configured scope.","commands":{"allow":[],"deny":["set_app_theme"]}},"deny-set-dock-visibility":{"identifier":"deny-set-dock-visibility","description":"Denies the set_dock_visibility command without any pre-configured scope.","commands":{"allow":[],"deny":["set_dock_visibility"]}},"deny-supports-multiple-windows":{"identifier":"deny-supports-multiple-windows","description":"Denies the supports_multiple_windows command without any pre-configured scope.","commands":{"allow":[],"deny":["supports_multiple_windows"]}},"deny-tauri-version":{"identifier":"deny-tauri-version","description":"Denies the tauri_version command without any pre-configured scope.","commands":{"allow":[],"deny":["tauri_version"]}},"deny-version":{"identifier":"deny-version","description":"Denies the version command without any pre-configured scope.","commands":{"allow":[],"deny":["version"]}}},"permission_sets":{},"global_scope_schema":null},"core:event":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-listen","allow-unlisten","allow-emit","allow-emit-to"]},"permissions":{"allow-emit":{"identifier":"allow-emit","description":"Enables the emit command without any pre-configured scope.","commands":{"allow":["emit"],"deny":[]}},"allow-emit-to":{"identifier":"allow-emit-to","description":"Enables the emit_to command without any pre-configured scope.","commands":{"allow":["emit_to"],"deny":[]}},"allow-listen":{"identifier":"allow-listen","description":"Enables the listen command without any pre-configured scope.","commands":{"allow":["listen"],"deny":[]}},"allow-unlisten":{"identifier":"allow-unlisten","description":"Enables the unlisten command without any pre-configured scope.","commands":{"allow":["unlisten"],"deny":[]}},"deny-emit":{"identifier":"deny-emit","description":"Denies the emit command without any pre-configured scope.","commands":{"allow":[],"deny":["emit"]}},"deny-emit-to":{"identifier":"deny-emit-to","description":"Denies the emit_to command without any pre-configured scope.","commands":{"allow":[],"deny":["emit_to"]}},"deny-listen":{"identifier":"deny-listen","description":"Denies the listen command without any pre-configured scope.","commands":{"allow":[],"deny":["listen"]}},"deny-unlisten":{"identifier":"deny-unlisten","description":"Denies the unlisten command without any pre-configured scope.","commands":{"allow":[],"deny":["unlisten"]}}},"permission_sets":{},"global_scope_schema":null},"core:image":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-new","allow-from-bytes","allow-from-path","allow-rgba","allow-size"]},"permissions":{"allow-from-bytes":{"identifier":"allow-from-bytes","description":"Enables the from_bytes command without any pre-configured scope.","commands":{"allow":["from_bytes"],"deny":[]}},"allow-from-path":{"identifier":"allow-from-path","description":"Enables the from_path command without any pre-configured scope.","commands":{"allow":["from_path"],"deny":[]}},"allow-new":{"identifier":"allow-new","description":"Enables the new command without any pre-configured scope.","commands":{"allow":["new"],"deny":[]}},"allow-rgba":{"identifier":"allow-rgba","description":"Enables the rgba command without any pre-configured scope.","commands":{"allow":["rgba"],"deny":[]}},"allow-size":{"identifier":"allow-size","description":"Enables the size command without any pre-configured scope.","commands":{"allow":["size"],"deny":[]}},"deny-from-bytes":{"identifier":"deny-from-bytes","description":"Denies the from_bytes command without any pre-configured scope.","commands":{"allow":[],"deny":["from_bytes"]}},"deny-from-path":{"identifier":"deny-from-path","description":"Denies the from_path command without any pre-configured scope.","commands":{"allow":[],"deny":["from_path"]}},"deny-new":{"identifier":"deny-new","description":"Denies the new command without any pre-configured scope.","commands":{"allow":[],"deny":["new"]}},"deny-rgba":{"identifier":"deny-rgba","description":"Denies the rgba command without any pre-configured scope.","commands":{"allow":[],"deny":["rgba"]}},"deny-size":{"identifier":"deny-size","description":"Denies the size command without any pre-configured scope.","commands":{"allow":[],"deny":["size"]}}},"permission_sets":{},"global_scope_schema":null},"core:menu":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-new","allow-append","allow-prepend","allow-insert","allow-remove","allow-remove-at","allow-items","allow-get","allow-popup","allow-create-default","allow-set-as-app-menu","allow-set-as-window-menu","allow-text","allow-set-text","allow-is-enabled","allow-set-enabled","allow-set-accelerator","allow-set-as-windows-menu-for-nsapp","allow-set-as-help-menu-for-nsapp","allow-is-checked","allow-set-checked","allow-set-icon"]},"permissions":{"allow-append":{"identifier":"allow-append","description":"Enables the append command without any pre-configured scope.","commands":{"allow":["append"],"deny":[]}},"allow-create-default":{"identifier":"allow-create-default","description":"Enables the create_default command without any pre-configured scope.","commands":{"allow":["create_default"],"deny":[]}},"allow-get":{"identifier":"allow-get","description":"Enables the get command without any pre-configured scope.","commands":{"allow":["get"],"deny":[]}},"allow-insert":{"identifier":"allow-insert","description":"Enables the insert command without any pre-configured scope.","commands":{"allow":["insert"],"deny":[]}},"allow-is-checked":{"identifier":"allow-is-checked","description":"Enables the is_checked command without any pre-configured scope.","commands":{"allow":["is_checked"],"deny":[]}},"allow-is-enabled":{"identifier":"allow-is-enabled","description":"Enables the is_enabled command without any pre-configured scope.","commands":{"allow":["is_enabled"],"deny":[]}},"allow-items":{"identifier":"allow-items","description":"Enables the items command without any pre-configured scope.","commands":{"allow":["items"],"deny":[]}},"allow-new":{"identifier":"allow-new","description":"Enables the new command without any pre-configured scope.","commands":{"allow":["new"],"deny":[]}},"allow-popup":{"identifier":"allow-popup","description":"Enables the popup command without any pre-configured scope.","commands":{"allow":["popup"],"deny":[]}},"allow-prepend":{"identifier":"allow-prepend","description":"Enables the prepend command without any pre-configured scope.","commands":{"allow":["prepend"],"deny":[]}},"allow-remove":{"identifier":"allow-remove","description":"Enables the remove command without any pre-configured scope.","commands":{"allow":["remove"],"deny":[]}},"allow-remove-at":{"identifier":"allow-remove-at","description":"Enables the remove_at command without any pre-configured scope.","commands":{"allow":["remove_at"],"deny":[]}},"allow-set-accelerator":{"identifier":"allow-set-accelerator","description":"Enables the set_accelerator command without any pre-configured scope.","commands":{"allow":["set_accelerator"],"deny":[]}},"allow-set-as-app-menu":{"identifier":"allow-set-as-app-menu","description":"Enables the set_as_app_menu command without any pre-configured scope.","commands":{"allow":["set_as_app_menu"],"deny":[]}},"allow-set-as-help-menu-for-nsapp":{"identifier":"allow-set-as-help-menu-for-nsapp","description":"Enables the set_as_help_menu_for_nsapp command without any pre-configured scope.","commands":{"allow":["set_as_help_menu_for_nsapp"],"deny":[]}},"allow-set-as-window-menu":{"identifier":"allow-set-as-window-menu","description":"Enables the set_as_window_menu command without any pre-configured scope.","commands":{"allow":["set_as_window_menu"],"deny":[]}},"allow-set-as-windows-menu-for-nsapp":{"identifier":"allow-set-as-windows-menu-for-nsapp","description":"Enables the set_as_windows_menu_for_nsapp command without any pre-configured scope.","commands":{"allow":["set_as_windows_menu_for_nsapp"],"deny":[]}},"allow-set-checked":{"identifier":"allow-set-checked","description":"Enables the set_checked command without any pre-configured scope.","commands":{"allow":["set_checked"],"deny":[]}},"allow-set-enabled":{"identifier":"allow-set-enabled","description":"Enables the set_enabled command without any pre-configured scope.","commands":{"allow":["set_enabled"],"deny":[]}},"allow-set-icon":{"identifier":"allow-set-icon","description":"Enables the set_icon command without any pre-configured scope.","commands":{"allow":["set_icon"],"deny":[]}},"allow-set-text":{"identifier":"allow-set-text","description":"Enables the set_text command without any pre-configured scope.","commands":{"allow":["set_text"],"deny":[]}},"allow-text":{"identifier":"allow-text","description":"Enables the text command without any pre-configured scope.","commands":{"allow":["text"],"deny":[]}},"deny-append":{"identifier":"deny-append","description":"Denies the append command without any pre-configured scope.","commands":{"allow":[],"deny":["append"]}},"deny-create-default":{"identifier":"deny-create-default","description":"Denies the create_default command without any pre-configured scope.","commands":{"allow":[],"deny":["create_default"]}},"deny-get":{"identifier":"deny-get","description":"Denies the get command without any pre-configured scope.","commands":{"allow":[],"deny":["get"]}},"deny-insert":{"identifier":"deny-insert","description":"Denies the insert command without any pre-configured scope.","commands":{"allow":[],"deny":["insert"]}},"deny-is-checked":{"identifier":"deny-is-checked","description":"Denies the is_checked command without any pre-configured scope.","commands":{"allow":[],"deny":["is_checked"]}},"deny-is-enabled":{"identifier":"deny-is-enabled","description":"Denies the is_enabled command without any pre-configured scope.","commands":{"allow":[],"deny":["is_enabled"]}},"deny-items":{"identifier":"deny-items","description":"Denies the items command without any pre-configured scope.","commands":{"allow":[],"deny":["items"]}},"deny-new":{"identifier":"deny-new","description":"Denies the new command without any pre-configured scope.","commands":{"allow":[],"deny":["new"]}},"deny-popup":{"identifier":"deny-popup","description":"Denies the popup command without any pre-configured scope.","commands":{"allow":[],"deny":["popup"]}},"deny-prepend":{"identifier":"deny-prepend","description":"Denies the prepend command without any pre-configured scope.","commands":{"allow":[],"deny":["prepend"]}},"deny-remove":{"identifier":"deny-remove","description":"Denies the remove command without any pre-configured scope.","commands":{"allow":[],"deny":["remove"]}},"deny-remove-at":{"identifier":"deny-remove-at","description":"Denies the remove_at command without any pre-configured scope.","commands":{"allow":[],"deny":["remove_at"]}},"deny-set-accelerator":{"identifier":"deny-set-accelerator","description":"Denies the set_accelerator command without any pre-configured scope.","commands":{"allow":[],"deny":["set_accelerator"]}},"deny-set-as-app-menu":{"identifier":"deny-set-as-app-menu","description":"Denies the set_as_app_menu command without any pre-configured scope.","commands":{"allow":[],"deny":["set_as_app_menu"]}},"deny-set-as-help-menu-for-nsapp":{"identifier":"deny-set-as-help-menu-for-nsapp","description":"Denies the set_as_help_menu_for_nsapp command without any pre-configured scope.","commands":{"allow":[],"deny":["set_as_help_menu_for_nsapp"]}},"deny-set-as-window-menu":{"identifier":"deny-set-as-window-menu","description":"Denies the set_as_window_menu command without any pre-configured scope.","commands":{"allow":[],"deny":["set_as_window_menu"]}},"deny-set-as-windows-menu-for-nsapp":{"identifier":"deny-set-as-windows-menu-for-nsapp","description":"Denies the set_as_windows_menu_for_nsapp command without any pre-configured scope.","commands":{"allow":[],"deny":["set_as_windows_menu_for_nsapp"]}},"deny-set-checked":{"identifier":"deny-set-checked","description":"Denies the set_checked command without any pre-configured scope.","commands":{"allow":[],"deny":["set_checked"]}},"deny-set-enabled":{"identifier":"deny-set-enabled","description":"Denies the set_enabled command without any pre-configured scope.","commands":{"allow":[],"deny":["set_enabled"]}},"deny-set-icon":{"identifier":"deny-set-icon","description":"Denies the set_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon"]}},"deny-set-text":{"identifier":"deny-set-text","description":"Denies the set_text command without any pre-configured scope.","commands":{"allow":[],"deny":["set_text"]}},"deny-text":{"identifier":"deny-text","description":"Denies the text command without any pre-configured scope.","commands":{"allow":[],"deny":["text"]}}},"permission_sets":{},"global_scope_schema":null},"core:path":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-resolve-directory","allow-resolve","allow-normalize","allow-join","allow-dirname","allow-extname","allow-basename","allow-is-absolute"]},"permissions":{"allow-basename":{"identifier":"allow-basename","description":"Enables the basename command without any pre-configured scope.","commands":{"allow":["basename"],"deny":[]}},"allow-dirname":{"identifier":"allow-dirname","description":"Enables the dirname command without any pre-configured scope.","commands":{"allow":["dirname"],"deny":[]}},"allow-extname":{"identifier":"allow-extname","description":"Enables the extname command without any pre-configured scope.","commands":{"allow":["extname"],"deny":[]}},"allow-is-absolute":{"identifier":"allow-is-absolute","description":"Enables the is_absolute command without any pre-configured scope.","commands":{"allow":["is_absolute"],"deny":[]}},"allow-join":{"identifier":"allow-join","description":"Enables the join command without any pre-configured scope.","commands":{"allow":["join"],"deny":[]}},"allow-normalize":{"identifier":"allow-normalize","description":"Enables the normalize command without any pre-configured scope.","commands":{"allow":["normalize"],"deny":[]}},"allow-resolve":{"identifier":"allow-resolve","description":"Enables the resolve command without any pre-configured scope.","commands":{"allow":["resolve"],"deny":[]}},"allow-resolve-directory":{"identifier":"allow-resolve-directory","description":"Enables the resolve_directory command without any pre-configured scope.","commands":{"allow":["resolve_directory"],"deny":[]}},"deny-basename":{"identifier":"deny-basename","description":"Denies the basename command without any pre-configured scope.","commands":{"allow":[],"deny":["basename"]}},"deny-dirname":{"identifier":"deny-dirname","description":"Denies the dirname command without any pre-configured scope.","commands":{"allow":[],"deny":["dirname"]}},"deny-extname":{"identifier":"deny-extname","description":"Denies the extname command without any pre-configured scope.","commands":{"allow":[],"deny":["extname"]}},"deny-is-absolute":{"identifier":"deny-is-absolute","description":"Denies the is_absolute command without any pre-configured scope.","commands":{"allow":[],"deny":["is_absolute"]}},"deny-join":{"identifier":"deny-join","description":"Denies the join command without any pre-configured scope.","commands":{"allow":[],"deny":["join"]}},"deny-normalize":{"identifier":"deny-normalize","description":"Denies the normalize command without any pre-configured scope.","commands":{"allow":[],"deny":["normalize"]}},"deny-resolve":{"identifier":"deny-resolve","description":"Denies the resolve command without any pre-configured scope.","commands":{"allow":[],"deny":["resolve"]}},"deny-resolve-directory":{"identifier":"deny-resolve-directory","description":"Denies the resolve_directory command without any pre-configured scope.","commands":{"allow":[],"deny":["resolve_directory"]}}},"permission_sets":{},"global_scope_schema":null},"core:resources":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-close"]},"permissions":{"allow-close":{"identifier":"allow-close","description":"Enables the close command without any pre-configured scope.","commands":{"allow":["close"],"deny":[]}},"deny-close":{"identifier":"deny-close","description":"Denies the close command without any pre-configured scope.","commands":{"allow":[],"deny":["close"]}}},"permission_sets":{},"global_scope_schema":null},"core:tray":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-new","allow-get-by-id","allow-remove-by-id","allow-set-icon","allow-set-menu","allow-set-tooltip","allow-set-title","allow-set-visible","allow-set-temp-dir-path","allow-set-icon-as-template","allow-set-icon-with-as-template","allow-set-show-menu-on-left-click"]},"permissions":{"allow-get-by-id":{"identifier":"allow-get-by-id","description":"Enables the get_by_id command without any pre-configured scope.","commands":{"allow":["get_by_id"],"deny":[]}},"allow-new":{"identifier":"allow-new","description":"Enables the new command without any pre-configured scope.","commands":{"allow":["new"],"deny":[]}},"allow-remove-by-id":{"identifier":"allow-remove-by-id","description":"Enables the remove_by_id command without any pre-configured scope.","commands":{"allow":["remove_by_id"],"deny":[]}},"allow-set-icon":{"identifier":"allow-set-icon","description":"Enables the set_icon command without any pre-configured scope.","commands":{"allow":["set_icon"],"deny":[]}},"allow-set-icon-as-template":{"identifier":"allow-set-icon-as-template","description":"Enables the set_icon_as_template command without any pre-configured scope.","commands":{"allow":["set_icon_as_template"],"deny":[]}},"allow-set-icon-with-as-template":{"identifier":"allow-set-icon-with-as-template","description":"Enables the set_icon_with_as_template command without any pre-configured scope.","commands":{"allow":["set_icon_with_as_template"],"deny":[]}},"allow-set-menu":{"identifier":"allow-set-menu","description":"Enables the set_menu command without any pre-configured scope.","commands":{"allow":["set_menu"],"deny":[]}},"allow-set-show-menu-on-left-click":{"identifier":"allow-set-show-menu-on-left-click","description":"Enables the set_show_menu_on_left_click command without any pre-configured scope.","commands":{"allow":["set_show_menu_on_left_click"],"deny":[]}},"allow-set-temp-dir-path":{"identifier":"allow-set-temp-dir-path","description":"Enables the set_temp_dir_path command without any pre-configured scope.","commands":{"allow":["set_temp_dir_path"],"deny":[]}},"allow-set-title":{"identifier":"allow-set-title","description":"Enables the set_title command without any pre-configured scope.","commands":{"allow":["set_title"],"deny":[]}},"allow-set-tooltip":{"identifier":"allow-set-tooltip","description":"Enables the set_tooltip command without any pre-configured scope.","commands":{"allow":["set_tooltip"],"deny":[]}},"allow-set-visible":{"identifier":"allow-set-visible","description":"Enables the set_visible command without any pre-configured scope.","commands":{"allow":["set_visible"],"deny":[]}},"deny-get-by-id":{"identifier":"deny-get-by-id","description":"Denies the get_by_id command without any pre-configured scope.","commands":{"allow":[],"deny":["get_by_id"]}},"deny-new":{"identifier":"deny-new","description":"Denies the new command without any pre-configured scope.","commands":{"allow":[],"deny":["new"]}},"deny-remove-by-id":{"identifier":"deny-remove-by-id","description":"Denies the remove_by_id command without any pre-configured scope.","commands":{"allow":[],"deny":["remove_by_id"]}},"deny-set-icon":{"identifier":"deny-set-icon","description":"Denies the set_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon"]}},"deny-set-icon-as-template":{"identifier":"deny-set-icon-as-template","description":"Denies the set_icon_as_template command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon_as_template"]}},"deny-set-icon-with-as-template":{"identifier":"deny-set-icon-with-as-template","description":"Denies the set_icon_with_as_template command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon_with_as_template"]}},"deny-set-menu":{"identifier":"deny-set-menu","description":"Denies the set_menu command without any pre-configured scope.","commands":{"allow":[],"deny":["set_menu"]}},"deny-set-show-menu-on-left-click":{"identifier":"deny-set-show-menu-on-left-click","description":"Denies the set_show_menu_on_left_click command without any pre-configured scope.","commands":{"allow":[],"deny":["set_show_menu_on_left_click"]}},"deny-set-temp-dir-path":{"identifier":"deny-set-temp-dir-path","description":"Denies the set_temp_dir_path command without any pre-configured scope.","commands":{"allow":[],"deny":["set_temp_dir_path"]}},"deny-set-title":{"identifier":"deny-set-title","description":"Denies the set_title command without any pre-configured scope.","commands":{"allow":[],"deny":["set_title"]}},"deny-set-tooltip":{"identifier":"deny-set-tooltip","description":"Denies the set_tooltip command without any pre-configured scope.","commands":{"allow":[],"deny":["set_tooltip"]}},"deny-set-visible":{"identifier":"deny-set-visible","description":"Denies the set_visible command without any pre-configured scope.","commands":{"allow":[],"deny":["set_visible"]}}},"permission_sets":{},"global_scope_schema":null},"core:webview":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-get-all-webviews","allow-webview-position","allow-webview-size","allow-internal-toggle-devtools"]},"permissions":{"allow-clear-all-browsing-data":{"identifier":"allow-clear-all-browsing-data","description":"Enables the clear_all_browsing_data command without any pre-configured scope.","commands":{"allow":["clear_all_browsing_data"],"deny":[]}},"allow-create-webview":{"identifier":"allow-create-webview","description":"Enables the create_webview command without any pre-configured scope.","commands":{"allow":["create_webview"],"deny":[]}},"allow-create-webview-window":{"identifier":"allow-create-webview-window","description":"Enables the create_webview_window command without any pre-configured scope.","commands":{"allow":["create_webview_window"],"deny":[]}},"allow-get-all-webviews":{"identifier":"allow-get-all-webviews","description":"Enables the get_all_webviews command without any pre-configured scope.","commands":{"allow":["get_all_webviews"],"deny":[]}},"allow-internal-toggle-devtools":{"identifier":"allow-internal-toggle-devtools","description":"Enables the internal_toggle_devtools command without any pre-configured scope.","commands":{"allow":["internal_toggle_devtools"],"deny":[]}},"allow-print":{"identifier":"allow-print","description":"Enables the print command without any pre-configured scope.","commands":{"allow":["print"],"deny":[]}},"allow-reparent":{"identifier":"allow-reparent","description":"Enables the reparent command without any pre-configured scope.","commands":{"allow":["reparent"],"deny":[]}},"allow-set-webview-auto-resize":{"identifier":"allow-set-webview-auto-resize","description":"Enables the set_webview_auto_resize command without any pre-configured scope.","commands":{"allow":["set_webview_auto_resize"],"deny":[]}},"allow-set-webview-background-color":{"identifier":"allow-set-webview-background-color","description":"Enables the set_webview_background_color command without any pre-configured scope.","commands":{"allow":["set_webview_background_color"],"deny":[]}},"allow-set-webview-focus":{"identifier":"allow-set-webview-focus","description":"Enables the set_webview_focus command without any pre-configured scope.","commands":{"allow":["set_webview_focus"],"deny":[]}},"allow-set-webview-position":{"identifier":"allow-set-webview-position","description":"Enables the set_webview_position command without any pre-configured scope.","commands":{"allow":["set_webview_position"],"deny":[]}},"allow-set-webview-size":{"identifier":"allow-set-webview-size","description":"Enables the set_webview_size command without any pre-configured scope.","commands":{"allow":["set_webview_size"],"deny":[]}},"allow-set-webview-zoom":{"identifier":"allow-set-webview-zoom","description":"Enables the set_webview_zoom command without any pre-configured scope.","commands":{"allow":["set_webview_zoom"],"deny":[]}},"allow-webview-close":{"identifier":"allow-webview-close","description":"Enables the webview_close command without any pre-configured scope.","commands":{"allow":["webview_close"],"deny":[]}},"allow-webview-hide":{"identifier":"allow-webview-hide","description":"Enables the webview_hide command without any pre-configured scope.","commands":{"allow":["webview_hide"],"deny":[]}},"allow-webview-position":{"identifier":"allow-webview-position","description":"Enables the webview_position command without any pre-configured scope.","commands":{"allow":["webview_position"],"deny":[]}},"allow-webview-show":{"identifier":"allow-webview-show","description":"Enables the webview_show command without any pre-configured scope.","commands":{"allow":["webview_show"],"deny":[]}},"allow-webview-size":{"identifier":"allow-webview-size","description":"Enables the webview_size command without any pre-configured scope.","commands":{"allow":["webview_size"],"deny":[]}},"deny-clear-all-browsing-data":{"identifier":"deny-clear-all-browsing-data","description":"Denies the clear_all_browsing_data command without any pre-configured scope.","commands":{"allow":[],"deny":["clear_all_browsing_data"]}},"deny-create-webview":{"identifier":"deny-create-webview","description":"Denies the create_webview command without any pre-configured scope.","commands":{"allow":[],"deny":["create_webview"]}},"deny-create-webview-window":{"identifier":"deny-create-webview-window","description":"Denies the create_webview_window command without any pre-configured scope.","commands":{"allow":[],"deny":["create_webview_window"]}},"deny-get-all-webviews":{"identifier":"deny-get-all-webviews","description":"Denies the get_all_webviews command without any pre-configured scope.","commands":{"allow":[],"deny":["get_all_webviews"]}},"deny-internal-toggle-devtools":{"identifier":"deny-internal-toggle-devtools","description":"Denies the internal_toggle_devtools command without any pre-configured scope.","commands":{"allow":[],"deny":["internal_toggle_devtools"]}},"deny-print":{"identifier":"deny-print","description":"Denies the print command without any pre-configured scope.","commands":{"allow":[],"deny":["print"]}},"deny-reparent":{"identifier":"deny-reparent","description":"Denies the reparent command without any pre-configured scope.","commands":{"allow":[],"deny":["reparent"]}},"deny-set-webview-auto-resize":{"identifier":"deny-set-webview-auto-resize","description":"Denies the set_webview_auto_resize command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_auto_resize"]}},"deny-set-webview-background-color":{"identifier":"deny-set-webview-background-color","description":"Denies the set_webview_background_color command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_background_color"]}},"deny-set-webview-focus":{"identifier":"deny-set-webview-focus","description":"Denies the set_webview_focus command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_focus"]}},"deny-set-webview-position":{"identifier":"deny-set-webview-position","description":"Denies the set_webview_position command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_position"]}},"deny-set-webview-size":{"identifier":"deny-set-webview-size","description":"Denies the set_webview_size command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_size"]}},"deny-set-webview-zoom":{"identifier":"deny-set-webview-zoom","description":"Denies the set_webview_zoom command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_zoom"]}},"deny-webview-close":{"identifier":"deny-webview-close","description":"Denies the webview_close command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_close"]}},"deny-webview-hide":{"identifier":"deny-webview-hide","description":"Denies the webview_hide command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_hide"]}},"deny-webview-position":{"identifier":"deny-webview-position","description":"Denies the webview_position command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_position"]}},"deny-webview-show":{"identifier":"deny-webview-show","description":"Denies the webview_show command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_show"]}},"deny-webview-size":{"identifier":"deny-webview-size","description":"Denies the webview_size command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_size"]}}},"permission_sets":{},"global_scope_schema":null},"core:window":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-get-all-windows","allow-scale-factor","allow-inner-position","allow-outer-position","allow-inner-size","allow-outer-size","allow-is-fullscreen","allow-is-minimized","allow-is-maximized","allow-is-focused","allow-is-decorated","allow-is-resizable","allow-is-maximizable","allow-is-minimizable","allow-is-closable","allow-is-visible","allow-is-enabled","allow-title","allow-current-monitor","allow-primary-monitor","allow-monitor-from-point","allow-available-monitors","allow-cursor-position","allow-theme","allow-is-always-on-top","allow-activity-name","allow-scene-identifier","allow-internal-toggle-maximize"]},"permissions":{"allow-activity-name":{"identifier":"allow-activity-name","description":"Enables the activity_name command without any pre-configured scope.","commands":{"allow":["activity_name"],"deny":[]}},"allow-available-monitors":{"identifier":"allow-available-monitors","description":"Enables the available_monitors command without any pre-configured scope.","commands":{"allow":["available_monitors"],"deny":[]}},"allow-center":{"identifier":"allow-center","description":"Enables the center command without any pre-configured scope.","commands":{"allow":["center"],"deny":[]}},"allow-close":{"identifier":"allow-close","description":"Enables the close command without any pre-configured scope.","commands":{"allow":["close"],"deny":[]}},"allow-create":{"identifier":"allow-create","description":"Enables the create command without any pre-configured scope.","commands":{"allow":["create"],"deny":[]}},"allow-current-monitor":{"identifier":"allow-current-monitor","description":"Enables the current_monitor command without any pre-configured scope.","commands":{"allow":["current_monitor"],"deny":[]}},"allow-cursor-position":{"identifier":"allow-cursor-position","description":"Enables the cursor_position command without any pre-configured scope.","commands":{"allow":["cursor_position"],"deny":[]}},"allow-destroy":{"identifier":"allow-destroy","description":"Enables the destroy command without any pre-configured scope.","commands":{"allow":["destroy"],"deny":[]}},"allow-get-all-windows":{"identifier":"allow-get-all-windows","description":"Enables the get_all_windows command without any pre-configured scope.","commands":{"allow":["get_all_windows"],"deny":[]}},"allow-hide":{"identifier":"allow-hide","description":"Enables the hide command without any pre-configured scope.","commands":{"allow":["hide"],"deny":[]}},"allow-inner-position":{"identifier":"allow-inner-position","description":"Enables the inner_position command without any pre-configured scope.","commands":{"allow":["inner_position"],"deny":[]}},"allow-inner-size":{"identifier":"allow-inner-size","description":"Enables the inner_size command without any pre-configured scope.","commands":{"allow":["inner_size"],"deny":[]}},"allow-internal-toggle-maximize":{"identifier":"allow-internal-toggle-maximize","description":"Enables the internal_toggle_maximize command without any pre-configured scope.","commands":{"allow":["internal_toggle_maximize"],"deny":[]}},"allow-is-always-on-top":{"identifier":"allow-is-always-on-top","description":"Enables the is_always_on_top command without any pre-configured scope.","commands":{"allow":["is_always_on_top"],"deny":[]}},"allow-is-closable":{"identifier":"allow-is-closable","description":"Enables the is_closable command without any pre-configured scope.","commands":{"allow":["is_closable"],"deny":[]}},"allow-is-decorated":{"identifier":"allow-is-decorated","description":"Enables the is_decorated command without any pre-configured scope.","commands":{"allow":["is_decorated"],"deny":[]}},"allow-is-enabled":{"identifier":"allow-is-enabled","description":"Enables the is_enabled command without any pre-configured scope.","commands":{"allow":["is_enabled"],"deny":[]}},"allow-is-focused":{"identifier":"allow-is-focused","description":"Enables the is_focused command without any pre-configured scope.","commands":{"allow":["is_focused"],"deny":[]}},"allow-is-fullscreen":{"identifier":"allow-is-fullscreen","description":"Enables the is_fullscreen command without any pre-configured scope.","commands":{"allow":["is_fullscreen"],"deny":[]}},"allow-is-maximizable":{"identifier":"allow-is-maximizable","description":"Enables the is_maximizable command without any pre-configured scope.","commands":{"allow":["is_maximizable"],"deny":[]}},"allow-is-maximized":{"identifier":"allow-is-maximized","description":"Enables the is_maximized command without any pre-configured scope.","commands":{"allow":["is_maximized"],"deny":[]}},"allow-is-minimizable":{"identifier":"allow-is-minimizable","description":"Enables the is_minimizable command without any pre-configured scope.","commands":{"allow":["is_minimizable"],"deny":[]}},"allow-is-minimized":{"identifier":"allow-is-minimized","description":"Enables the is_minimized command without any pre-configured scope.","commands":{"allow":["is_minimized"],"deny":[]}},"allow-is-resizable":{"identifier":"allow-is-resizable","description":"Enables the is_resizable command without any pre-configured scope.","commands":{"allow":["is_resizable"],"deny":[]}},"allow-is-visible":{"identifier":"allow-is-visible","description":"Enables the is_visible command without any pre-configured scope.","commands":{"allow":["is_visible"],"deny":[]}},"allow-maximize":{"identifier":"allow-maximize","description":"Enables the maximize command without any pre-configured scope.","commands":{"allow":["maximize"],"deny":[]}},"allow-minimize":{"identifier":"allow-minimize","description":"Enables the minimize command without any pre-configured scope.","commands":{"allow":["minimize"],"deny":[]}},"allow-monitor-from-point":{"identifier":"allow-monitor-from-point","description":"Enables the monitor_from_point command without any pre-configured scope.","commands":{"allow":["monitor_from_point"],"deny":[]}},"allow-outer-position":{"identifier":"allow-outer-position","description":"Enables the outer_position command without any pre-configured scope.","commands":{"allow":["outer_position"],"deny":[]}},"allow-outer-size":{"identifier":"allow-outer-size","description":"Enables the outer_size command without any pre-configured scope.","commands":{"allow":["outer_size"],"deny":[]}},"allow-primary-monitor":{"identifier":"allow-primary-monitor","description":"Enables the primary_monitor command without any pre-configured scope.","commands":{"allow":["primary_monitor"],"deny":[]}},"allow-request-user-attention":{"identifier":"allow-request-user-attention","description":"Enables the request_user_attention command without any pre-configured scope.","commands":{"allow":["request_user_attention"],"deny":[]}},"allow-scale-factor":{"identifier":"allow-scale-factor","description":"Enables the scale_factor command without any pre-configured scope.","commands":{"allow":["scale_factor"],"deny":[]}},"allow-scene-identifier":{"identifier":"allow-scene-identifier","description":"Enables the scene_identifier command without any pre-configured scope.","commands":{"allow":["scene_identifier"],"deny":[]}},"allow-set-always-on-bottom":{"identifier":"allow-set-always-on-bottom","description":"Enables the set_always_on_bottom command without any pre-configured scope.","commands":{"allow":["set_always_on_bottom"],"deny":[]}},"allow-set-always-on-top":{"identifier":"allow-set-always-on-top","description":"Enables the set_always_on_top command without any pre-configured scope.","commands":{"allow":["set_always_on_top"],"deny":[]}},"allow-set-background-color":{"identifier":"allow-set-background-color","description":"Enables the set_background_color command without any pre-configured scope.","commands":{"allow":["set_background_color"],"deny":[]}},"allow-set-badge-count":{"identifier":"allow-set-badge-count","description":"Enables the set_badge_count command without any pre-configured scope.","commands":{"allow":["set_badge_count"],"deny":[]}},"allow-set-badge-label":{"identifier":"allow-set-badge-label","description":"Enables the set_badge_label command without any pre-configured scope.","commands":{"allow":["set_badge_label"],"deny":[]}},"allow-set-closable":{"identifier":"allow-set-closable","description":"Enables the set_closable command without any pre-configured scope.","commands":{"allow":["set_closable"],"deny":[]}},"allow-set-content-protected":{"identifier":"allow-set-content-protected","description":"Enables the set_content_protected command without any pre-configured scope.","commands":{"allow":["set_content_protected"],"deny":[]}},"allow-set-cursor-grab":{"identifier":"allow-set-cursor-grab","description":"Enables the set_cursor_grab command without any pre-configured scope.","commands":{"allow":["set_cursor_grab"],"deny":[]}},"allow-set-cursor-icon":{"identifier":"allow-set-cursor-icon","description":"Enables the set_cursor_icon command without any pre-configured scope.","commands":{"allow":["set_cursor_icon"],"deny":[]}},"allow-set-cursor-position":{"identifier":"allow-set-cursor-position","description":"Enables the set_cursor_position command without any pre-configured scope.","commands":{"allow":["set_cursor_position"],"deny":[]}},"allow-set-cursor-visible":{"identifier":"allow-set-cursor-visible","description":"Enables the set_cursor_visible command without any pre-configured scope.","commands":{"allow":["set_cursor_visible"],"deny":[]}},"allow-set-decorations":{"identifier":"allow-set-decorations","description":"Enables the set_decorations command without any pre-configured scope.","commands":{"allow":["set_decorations"],"deny":[]}},"allow-set-effects":{"identifier":"allow-set-effects","description":"Enables the set_effects command without any pre-configured scope.","commands":{"allow":["set_effects"],"deny":[]}},"allow-set-enabled":{"identifier":"allow-set-enabled","description":"Enables the set_enabled command without any pre-configured scope.","commands":{"allow":["set_enabled"],"deny":[]}},"allow-set-focus":{"identifier":"allow-set-focus","description":"Enables the set_focus command without any pre-configured scope.","commands":{"allow":["set_focus"],"deny":[]}},"allow-set-focusable":{"identifier":"allow-set-focusable","description":"Enables the set_focusable command without any pre-configured scope.","commands":{"allow":["set_focusable"],"deny":[]}},"allow-set-fullscreen":{"identifier":"allow-set-fullscreen","description":"Enables the set_fullscreen command without any pre-configured scope.","commands":{"allow":["set_fullscreen"],"deny":[]}},"allow-set-icon":{"identifier":"allow-set-icon","description":"Enables the set_icon command without any pre-configured scope.","commands":{"allow":["set_icon"],"deny":[]}},"allow-set-ignore-cursor-events":{"identifier":"allow-set-ignore-cursor-events","description":"Enables the set_ignore_cursor_events command without any pre-configured scope.","commands":{"allow":["set_ignore_cursor_events"],"deny":[]}},"allow-set-max-size":{"identifier":"allow-set-max-size","description":"Enables the set_max_size command without any pre-configured scope.","commands":{"allow":["set_max_size"],"deny":[]}},"allow-set-maximizable":{"identifier":"allow-set-maximizable","description":"Enables the set_maximizable command without any pre-configured scope.","commands":{"allow":["set_maximizable"],"deny":[]}},"allow-set-min-size":{"identifier":"allow-set-min-size","description":"Enables the set_min_size command without any pre-configured scope.","commands":{"allow":["set_min_size"],"deny":[]}},"allow-set-minimizable":{"identifier":"allow-set-minimizable","description":"Enables the set_minimizable command without any pre-configured scope.","commands":{"allow":["set_minimizable"],"deny":[]}},"allow-set-overlay-icon":{"identifier":"allow-set-overlay-icon","description":"Enables the set_overlay_icon command without any pre-configured scope.","commands":{"allow":["set_overlay_icon"],"deny":[]}},"allow-set-position":{"identifier":"allow-set-position","description":"Enables the set_position command without any pre-configured scope.","commands":{"allow":["set_position"],"deny":[]}},"allow-set-progress-bar":{"identifier":"allow-set-progress-bar","description":"Enables the set_progress_bar command without any pre-configured scope.","commands":{"allow":["set_progress_bar"],"deny":[]}},"allow-set-resizable":{"identifier":"allow-set-resizable","description":"Enables the set_resizable command without any pre-configured scope.","commands":{"allow":["set_resizable"],"deny":[]}},"allow-set-shadow":{"identifier":"allow-set-shadow","description":"Enables the set_shadow command without any pre-configured scope.","commands":{"allow":["set_shadow"],"deny":[]}},"allow-set-simple-fullscreen":{"identifier":"allow-set-simple-fullscreen","description":"Enables the set_simple_fullscreen command without any pre-configured scope.","commands":{"allow":["set_simple_fullscreen"],"deny":[]}},"allow-set-size":{"identifier":"allow-set-size","description":"Enables the set_size command without any pre-configured scope.","commands":{"allow":["set_size"],"deny":[]}},"allow-set-size-constraints":{"identifier":"allow-set-size-constraints","description":"Enables the set_size_constraints command without any pre-configured scope.","commands":{"allow":["set_size_constraints"],"deny":[]}},"allow-set-skip-taskbar":{"identifier":"allow-set-skip-taskbar","description":"Enables the set_skip_taskbar command without any pre-configured scope.","commands":{"allow":["set_skip_taskbar"],"deny":[]}},"allow-set-theme":{"identifier":"allow-set-theme","description":"Enables the set_theme command without any pre-configured scope.","commands":{"allow":["set_theme"],"deny":[]}},"allow-set-title":{"identifier":"allow-set-title","description":"Enables the set_title command without any pre-configured scope.","commands":{"allow":["set_title"],"deny":[]}},"allow-set-title-bar-style":{"identifier":"allow-set-title-bar-style","description":"Enables the set_title_bar_style command without any pre-configured scope.","commands":{"allow":["set_title_bar_style"],"deny":[]}},"allow-set-visible-on-all-workspaces":{"identifier":"allow-set-visible-on-all-workspaces","description":"Enables the set_visible_on_all_workspaces command without any pre-configured scope.","commands":{"allow":["set_visible_on_all_workspaces"],"deny":[]}},"allow-show":{"identifier":"allow-show","description":"Enables the show command without any pre-configured scope.","commands":{"allow":["show"],"deny":[]}},"allow-start-dragging":{"identifier":"allow-start-dragging","description":"Enables the start_dragging command without any pre-configured scope.","commands":{"allow":["start_dragging"],"deny":[]}},"allow-start-resize-dragging":{"identifier":"allow-start-resize-dragging","description":"Enables the start_resize_dragging command without any pre-configured scope.","commands":{"allow":["start_resize_dragging"],"deny":[]}},"allow-theme":{"identifier":"allow-theme","description":"Enables the theme command without any pre-configured scope.","commands":{"allow":["theme"],"deny":[]}},"allow-title":{"identifier":"allow-title","description":"Enables the title command without any pre-configured scope.","commands":{"allow":["title"],"deny":[]}},"allow-toggle-maximize":{"identifier":"allow-toggle-maximize","description":"Enables the toggle_maximize command without any pre-configured scope.","commands":{"allow":["toggle_maximize"],"deny":[]}},"allow-unmaximize":{"identifier":"allow-unmaximize","description":"Enables the unmaximize command without any pre-configured scope.","commands":{"allow":["unmaximize"],"deny":[]}},"allow-unminimize":{"identifier":"allow-unminimize","description":"Enables the unminimize command without any pre-configured scope.","commands":{"allow":["unminimize"],"deny":[]}},"deny-activity-name":{"identifier":"deny-activity-name","description":"Denies the activity_name command without any pre-configured scope.","commands":{"allow":[],"deny":["activity_name"]}},"deny-available-monitors":{"identifier":"deny-available-monitors","description":"Denies the available_monitors command without any pre-configured scope.","commands":{"allow":[],"deny":["available_monitors"]}},"deny-center":{"identifier":"deny-center","description":"Denies the center command without any pre-configured scope.","commands":{"allow":[],"deny":["center"]}},"deny-close":{"identifier":"deny-close","description":"Denies the close command without any pre-configured scope.","commands":{"allow":[],"deny":["close"]}},"deny-create":{"identifier":"deny-create","description":"Denies the create command without any pre-configured scope.","commands":{"allow":[],"deny":["create"]}},"deny-current-monitor":{"identifier":"deny-current-monitor","description":"Denies the current_monitor command without any pre-configured scope.","commands":{"allow":[],"deny":["current_monitor"]}},"deny-cursor-position":{"identifier":"deny-cursor-position","description":"Denies the cursor_position command without any pre-configured scope.","commands":{"allow":[],"deny":["cursor_position"]}},"deny-destroy":{"identifier":"deny-destroy","description":"Denies the destroy command without any pre-configured scope.","commands":{"allow":[],"deny":["destroy"]}},"deny-get-all-windows":{"identifier":"deny-get-all-windows","description":"Denies the get_all_windows command without any pre-configured scope.","commands":{"allow":[],"deny":["get_all_windows"]}},"deny-hide":{"identifier":"deny-hide","description":"Denies the hide command without any pre-configured scope.","commands":{"allow":[],"deny":["hide"]}},"deny-inner-position":{"identifier":"deny-inner-position","description":"Denies the inner_position command without any pre-configured scope.","commands":{"allow":[],"deny":["inner_position"]}},"deny-inner-size":{"identifier":"deny-inner-size","description":"Denies the inner_size command without any pre-configured scope.","commands":{"allow":[],"deny":["inner_size"]}},"deny-internal-toggle-maximize":{"identifier":"deny-internal-toggle-maximize","description":"Denies the internal_toggle_maximize command without any pre-configured scope.","commands":{"allow":[],"deny":["internal_toggle_maximize"]}},"deny-is-always-on-top":{"identifier":"deny-is-always-on-top","description":"Denies the is_always_on_top command without any pre-configured scope.","commands":{"allow":[],"deny":["is_always_on_top"]}},"deny-is-closable":{"identifier":"deny-is-closable","description":"Denies the is_closable command without any pre-configured scope.","commands":{"allow":[],"deny":["is_closable"]}},"deny-is-decorated":{"identifier":"deny-is-decorated","description":"Denies the is_decorated command without any pre-configured scope.","commands":{"allow":[],"deny":["is_decorated"]}},"deny-is-enabled":{"identifier":"deny-is-enabled","description":"Denies the is_enabled command without any pre-configured scope.","commands":{"allow":[],"deny":["is_enabled"]}},"deny-is-focused":{"identifier":"deny-is-focused","description":"Denies the is_focused command without any pre-configured scope.","commands":{"allow":[],"deny":["is_focused"]}},"deny-is-fullscreen":{"identifier":"deny-is-fullscreen","description":"Denies the is_fullscreen command without any pre-configured scope.","commands":{"allow":[],"deny":["is_fullscreen"]}},"deny-is-maximizable":{"identifier":"deny-is-maximizable","description":"Denies the is_maximizable command without any pre-configured scope.","commands":{"allow":[],"deny":["is_maximizable"]}},"deny-is-maximized":{"identifier":"deny-is-maximized","description":"Denies the is_maximized command without any pre-configured scope.","commands":{"allow":[],"deny":["is_maximized"]}},"deny-is-minimizable":{"identifier":"deny-is-minimizable","description":"Denies the is_minimizable command without any pre-configured scope.","commands":{"allow":[],"deny":["is_minimizable"]}},"deny-is-minimized":{"identifier":"deny-is-minimized","description":"Denies the is_minimized command without any pre-configured scope.","commands":{"allow":[],"deny":["is_minimized"]}},"deny-is-resizable":{"identifier":"deny-is-resizable","description":"Denies the is_resizable command without any pre-configured scope.","commands":{"allow":[],"deny":["is_resizable"]}},"deny-is-visible":{"identifier":"deny-is-visible","description":"Denies the is_visible command without any pre-configured scope.","commands":{"allow":[],"deny":["is_visible"]}},"deny-maximize":{"identifier":"deny-maximize","description":"Denies the maximize command without any pre-configured scope.","commands":{"allow":[],"deny":["maximize"]}},"deny-minimize":{"identifier":"deny-minimize","description":"Denies the minimize command without any pre-configured scope.","commands":{"allow":[],"deny":["minimize"]}},"deny-monitor-from-point":{"identifier":"deny-monitor-from-point","description":"Denies the monitor_from_point command without any pre-configured scope.","commands":{"allow":[],"deny":["monitor_from_point"]}},"deny-outer-position":{"identifier":"deny-outer-position","description":"Denies the outer_position command without any pre-configured scope.","commands":{"allow":[],"deny":["outer_position"]}},"deny-outer-size":{"identifier":"deny-outer-size","description":"Denies the outer_size command without any pre-configured scope.","commands":{"allow":[],"deny":["outer_size"]}},"deny-primary-monitor":{"identifier":"deny-primary-monitor","description":"Denies the primary_monitor command without any pre-configured scope.","commands":{"allow":[],"deny":["primary_monitor"]}},"deny-request-user-attention":{"identifier":"deny-request-user-attention","description":"Denies the request_user_attention command without any pre-configured scope.","commands":{"allow":[],"deny":["request_user_attention"]}},"deny-scale-factor":{"identifier":"deny-scale-factor","description":"Denies the scale_factor command without any pre-configured scope.","commands":{"allow":[],"deny":["scale_factor"]}},"deny-scene-identifier":{"identifier":"deny-scene-identifier","description":"Denies the scene_identifier command without any pre-configured scope.","commands":{"allow":[],"deny":["scene_identifier"]}},"deny-set-always-on-bottom":{"identifier":"deny-set-always-on-bottom","description":"Denies the set_always_on_bottom command without any pre-configured scope.","commands":{"allow":[],"deny":["set_always_on_bottom"]}},"deny-set-always-on-top":{"identifier":"deny-set-always-on-top","description":"Denies the set_always_on_top command without any pre-configured scope.","commands":{"allow":[],"deny":["set_always_on_top"]}},"deny-set-background-color":{"identifier":"deny-set-background-color","description":"Denies the set_background_color command without any pre-configured scope.","commands":{"allow":[],"deny":["set_background_color"]}},"deny-set-badge-count":{"identifier":"deny-set-badge-count","description":"Denies the set_badge_count command without any pre-configured scope.","commands":{"allow":[],"deny":["set_badge_count"]}},"deny-set-badge-label":{"identifier":"deny-set-badge-label","description":"Denies the set_badge_label command without any pre-configured scope.","commands":{"allow":[],"deny":["set_badge_label"]}},"deny-set-closable":{"identifier":"deny-set-closable","description":"Denies the set_closable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_closable"]}},"deny-set-content-protected":{"identifier":"deny-set-content-protected","description":"Denies the set_content_protected command without any pre-configured scope.","commands":{"allow":[],"deny":["set_content_protected"]}},"deny-set-cursor-grab":{"identifier":"deny-set-cursor-grab","description":"Denies the set_cursor_grab command without any pre-configured scope.","commands":{"allow":[],"deny":["set_cursor_grab"]}},"deny-set-cursor-icon":{"identifier":"deny-set-cursor-icon","description":"Denies the set_cursor_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_cursor_icon"]}},"deny-set-cursor-position":{"identifier":"deny-set-cursor-position","description":"Denies the set_cursor_position command without any pre-configured scope.","commands":{"allow":[],"deny":["set_cursor_position"]}},"deny-set-cursor-visible":{"identifier":"deny-set-cursor-visible","description":"Denies the set_cursor_visible command without any pre-configured scope.","commands":{"allow":[],"deny":["set_cursor_visible"]}},"deny-set-decorations":{"identifier":"deny-set-decorations","description":"Denies the set_decorations command without any pre-configured scope.","commands":{"allow":[],"deny":["set_decorations"]}},"deny-set-effects":{"identifier":"deny-set-effects","description":"Denies the set_effects command without any pre-configured scope.","commands":{"allow":[],"deny":["set_effects"]}},"deny-set-enabled":{"identifier":"deny-set-enabled","description":"Denies the set_enabled command without any pre-configured scope.","commands":{"allow":[],"deny":["set_enabled"]}},"deny-set-focus":{"identifier":"deny-set-focus","description":"Denies the set_focus command without any pre-configured scope.","commands":{"allow":[],"deny":["set_focus"]}},"deny-set-focusable":{"identifier":"deny-set-focusable","description":"Denies the set_focusable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_focusable"]}},"deny-set-fullscreen":{"identifier":"deny-set-fullscreen","description":"Denies the set_fullscreen command without any pre-configured scope.","commands":{"allow":[],"deny":["set_fullscreen"]}},"deny-set-icon":{"identifier":"deny-set-icon","description":"Denies the set_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon"]}},"deny-set-ignore-cursor-events":{"identifier":"deny-set-ignore-cursor-events","description":"Denies the set_ignore_cursor_events command without any pre-configured scope.","commands":{"allow":[],"deny":["set_ignore_cursor_events"]}},"deny-set-max-size":{"identifier":"deny-set-max-size","description":"Denies the set_max_size command without any pre-configured scope.","commands":{"allow":[],"deny":["set_max_size"]}},"deny-set-maximizable":{"identifier":"deny-set-maximizable","description":"Denies the set_maximizable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_maximizable"]}},"deny-set-min-size":{"identifier":"deny-set-min-size","description":"Denies the set_min_size command without any pre-configured scope.","commands":{"allow":[],"deny":["set_min_size"]}},"deny-set-minimizable":{"identifier":"deny-set-minimizable","description":"Denies the set_minimizable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_minimizable"]}},"deny-set-overlay-icon":{"identifier":"deny-set-overlay-icon","description":"Denies the set_overlay_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_overlay_icon"]}},"deny-set-position":{"identifier":"deny-set-position","description":"Denies the set_position command without any pre-configured scope.","commands":{"allow":[],"deny":["set_position"]}},"deny-set-progress-bar":{"identifier":"deny-set-progress-bar","description":"Denies the set_progress_bar command without any pre-configured scope.","commands":{"allow":[],"deny":["set_progress_bar"]}},"deny-set-resizable":{"identifier":"deny-set-resizable","description":"Denies the set_resizable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_resizable"]}},"deny-set-shadow":{"identifier":"deny-set-shadow","description":"Denies the set_shadow command without any pre-configured scope.","commands":{"allow":[],"deny":["set_shadow"]}},"deny-set-simple-fullscreen":{"identifier":"deny-set-simple-fullscreen","description":"Denies the set_simple_fullscreen command without any pre-configured scope.","commands":{"allow":[],"deny":["set_simple_fullscreen"]}},"deny-set-size":{"identifier":"deny-set-size","description":"Denies the set_size command without any pre-configured scope.","commands":{"allow":[],"deny":["set_size"]}},"deny-set-size-constraints":{"identifier":"deny-set-size-constraints","description":"Denies the set_size_constraints command without any pre-configured scope.","commands":{"allow":[],"deny":["set_size_constraints"]}},"deny-set-skip-taskbar":{"identifier":"deny-set-skip-taskbar","description":"Denies the set_skip_taskbar command without any pre-configured scope.","commands":{"allow":[],"deny":["set_skip_taskbar"]}},"deny-set-theme":{"identifier":"deny-set-theme","description":"Denies the set_theme command without any pre-configured scope.","commands":{"allow":[],"deny":["set_theme"]}},"deny-set-title":{"identifier":"deny-set-title","description":"Denies the set_title command without any pre-configured scope.","commands":{"allow":[],"deny":["set_title"]}},"deny-set-title-bar-style":{"identifier":"deny-set-title-bar-style","description":"Denies the set_title_bar_style command without any pre-configured scope.","commands":{"allow":[],"deny":["set_title_bar_style"]}},"deny-set-visible-on-all-workspaces":{"identifier":"deny-set-visible-on-all-workspaces","description":"Denies the set_visible_on_all_workspaces command without any pre-configured scope.","commands":{"allow":[],"deny":["set_visible_on_all_workspaces"]}},"deny-show":{"identifier":"deny-show","description":"Denies the show command without any pre-configured scope.","commands":{"allow":[],"deny":["show"]}},"deny-start-dragging":{"identifier":"deny-start-dragging","description":"Denies the start_dragging command without any pre-configured scope.","commands":{"allow":[],"deny":["start_dragging"]}},"deny-start-resize-dragging":{"identifier":"deny-start-resize-dragging","description":"Denies the start_resize_dragging command without any pre-configured scope.","commands":{"allow":[],"deny":["start_resize_dragging"]}},"deny-theme":{"identifier":"deny-theme","description":"Denies the theme command without any pre-configured scope.","commands":{"allow":[],"deny":["theme"]}},"deny-title":{"identifier":"deny-title","description":"Denies the title command without any pre-configured scope.","commands":{"allow":[],"deny":["title"]}},"deny-toggle-maximize":{"identifier":"deny-toggle-maximize","description":"Denies the toggle_maximize command without any pre-configured scope.","commands":{"allow":[],"deny":["toggle_maximize"]}},"deny-unmaximize":{"identifier":"deny-unmaximize","description":"Denies the unmaximize command without any pre-configured scope.","commands":{"allow":[],"deny":["unmaximize"]}},"deny-unminimize":{"identifier":"deny-unminimize","description":"Denies the unminimize command without any pre-configured scope.","commands":{"allow":[],"deny":["unminimize"]}}},"permission_sets":{},"global_scope_schema":null},"dialog":{"default_permission":{"identifier":"default","description":"This permission set configures the types of dialogs\navailable from the dialog plugin.\n\n#### Granted Permissions\n\nAll dialog types are enabled.\n\n\n","permissions":["allow-message","allow-save","allow-open"]},"permissions":{"allow-ask":{"identifier":"allow-ask","description":"Enables the ask command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `allow-message` and will be removed in v3)","commands":{"allow":["message"],"deny":[]}},"allow-confirm":{"identifier":"allow-confirm","description":"Enables the confirm command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `allow-message` and will be removed in v3)","commands":{"allow":["message"],"deny":[]}},"allow-message":{"identifier":"allow-message","description":"Enables the message command without any pre-configured scope.","commands":{"allow":["message"],"deny":[]}},"allow-open":{"identifier":"allow-open","description":"Enables the open command without any pre-configured scope.","commands":{"allow":["open"],"deny":[]}},"allow-save":{"identifier":"allow-save","description":"Enables the save command without any pre-configured scope.","commands":{"allow":["save"],"deny":[]}},"deny-ask":{"identifier":"deny-ask","description":"Denies the ask command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `deny-message` and will be removed in v3)","commands":{"allow":[],"deny":["message"]}},"deny-confirm":{"identifier":"deny-confirm","description":"Denies the confirm command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `deny-message` and will be removed in v3)","commands":{"allow":[],"deny":["message"]}},"deny-message":{"identifier":"deny-message","description":"Denies the message command without any pre-configured scope.","commands":{"allow":[],"deny":["message"]}},"deny-open":{"identifier":"deny-open","description":"Denies the open command without any pre-configured scope.","commands":{"allow":[],"deny":["open"]}},"deny-save":{"identifier":"deny-save","description":"Denies the save command without any pre-configured scope.","commands":{"allow":[],"deny":["save"]}}},"permission_sets":{},"global_scope_schema":null}} \ No newline at end of file diff --git a/apps/windows-client/src-tauri/gen/schemas/capabilities.json b/apps/windows-client/src-tauri/gen/schemas/capabilities.json deleted file mode 100644 index 62e9233..0000000 --- a/apps/windows-client/src-tauri/gen/schemas/capabilities.json +++ /dev/null @@ -1 +0,0 @@ -{"default":{"identifier":"default","description":"Default capability for the main VPN Proxy Windows shell. Task 8 keeps helper/install launch explicit: no shell or sidecar permission is granted here until a packaged helper is declared.","local":true,"windows":["main"],"permissions":["core:default","dialog:allow-open"]}} \ No newline at end of file diff --git a/apps/windows-client/src-tauri/gen/schemas/desktop-schema.json b/apps/windows-client/src-tauri/gen/schemas/desktop-schema.json deleted file mode 100644 index 24c9001..0000000 --- a/apps/windows-client/src-tauri/gen/schemas/desktop-schema.json +++ /dev/null @@ -1,2358 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "CapabilityFile", - "description": "Capability formats accepted in a capability file.", - "anyOf": [ - { - "description": "A single capability.", - "allOf": [ - { - "$ref": "#/definitions/Capability" - } - ] - }, - { - "description": "A list of capabilities.", - "type": "array", - "items": { - "$ref": "#/definitions/Capability" - } - }, - { - "description": "A list of capabilities.", - "type": "object", - "required": [ - "capabilities" - ], - "properties": { - "capabilities": { - "description": "The list of capabilities.", - "type": "array", - "items": { - "$ref": "#/definitions/Capability" - } - } - } - } - ], - "definitions": { - "Capability": { - "description": "A grouping and boundary mechanism developers can use to isolate access to the IPC layer.\n\nIt controls application windows' and webviews' fine grained access to the Tauri core, application, or plugin commands. If a webview or its window is not matching any capability then it has no access to the IPC layer at all.\n\nThis can be done to create groups of windows, based on their required system access, which can reduce impact of frontend vulnerabilities in less privileged windows. Windows can be added to a capability by exact name (e.g. `main-window`) or glob patterns like `*` or `admin-*`. A Window can have none, one, or multiple associated capabilities.\n\n## Example\n\n```json { \"identifier\": \"main-user-files-write\", \"description\": \"This capability allows the `main` window on macOS and Windows access to `filesystem` write related commands and `dialog` commands to enable programmatic access to files selected by the user.\", \"windows\": [ \"main\" ], \"permissions\": [ \"core:default\", \"dialog:open\", { \"identifier\": \"fs:allow-write-text-file\", \"allow\": [{ \"path\": \"$HOME/test.txt\" }] }, ], \"platforms\": [\"macOS\",\"windows\"] } ```", - "type": "object", - "required": [ - "identifier", - "permissions" - ], - "properties": { - "identifier": { - "description": "Identifier of the capability.\n\n## Example\n\n`main-user-files-write`", - "type": "string" - }, - "description": { - "description": "Description of what the capability is intended to allow on associated windows.\n\nIt should contain a description of what the grouped permissions should allow.\n\n## Example\n\nThis capability allows the `main` window access to `filesystem` write related commands and `dialog` commands to enable programmatic access to files selected by the user.", - "default": "", - "type": "string" - }, - "remote": { - "description": "Configure remote URLs that can use the capability permissions.\n\nThis setting is optional and defaults to not being set, as our default use case is that the content is served from our local application.\n\n:::caution Make sure you understand the security implications of providing remote sources with local system access. :::\n\n## Example\n\n```json { \"urls\": [\"https://*.mydomain.dev\"] } ```", - "anyOf": [ - { - "$ref": "#/definitions/CapabilityRemote" - }, - { - "type": "null" - } - ] - }, - "local": { - "description": "Whether this capability is enabled for local app URLs or not. Defaults to `true`.", - "default": true, - "type": "boolean" - }, - "windows": { - "description": "List of windows that are affected by this capability. Can be a glob pattern.\n\nIf a window label matches any of the patterns in this list, the capability will be enabled on all the webviews of that window, regardless of the value of [`Self::webviews`].\n\nOn multiwebview windows, prefer specifying [`Self::webviews`] and omitting [`Self::windows`] for a fine grained access control.\n\n## Example\n\n`[\"main\"]`", - "type": "array", - "items": { - "type": "string" - } - }, - "webviews": { - "description": "List of webviews that are affected by this capability. Can be a glob pattern.\n\nThe capability will be enabled on all the webviews whose label matches any of the patterns in this list, regardless of whether the webview's window label matches a pattern in [`Self::windows`].\n\n## Example\n\n`[\"sub-webview-one\", \"sub-webview-two\"]`", - "type": "array", - "items": { - "type": "string" - } - }, - "permissions": { - "description": "List of permissions attached to this capability.\n\nMust include the plugin name as prefix in the form of `${plugin-name}:${permission-name}`. For commands directly implemented in the application itself only `${permission-name}` is required.\n\n## Example\n\n```json [ \"core:default\", \"shell:allow-open\", \"dialog:open\", { \"identifier\": \"fs:allow-write-text-file\", \"allow\": [{ \"path\": \"$HOME/test.txt\" }] } ] ```", - "type": "array", - "items": { - "$ref": "#/definitions/PermissionEntry" - }, - "uniqueItems": true - }, - "platforms": { - "description": "Limit which target platforms this capability applies to.\n\nBy default all platforms are targeted.\n\n## Example\n\n`[\"macOS\",\"windows\"]`", - "type": [ - "array", - "null" - ], - "items": { - "$ref": "#/definitions/Target" - } - } - } - }, - "CapabilityRemote": { - "description": "Configuration for remote URLs that are associated with the capability.", - "type": "object", - "required": [ - "urls" - ], - "properties": { - "urls": { - "description": "Remote domains this capability refers to using the [URLPattern standard](https://urlpattern.spec.whatwg.org/).\n\n## Examples\n\n- \"https://*.mydomain.dev\": allows subdomains of mydomain.dev - \"https://mydomain.dev/api/*\": allows any subpath of mydomain.dev/api", - "type": "array", - "items": { - "type": "string" - } - } - } - }, - "PermissionEntry": { - "description": "An entry for a permission value in a [`Capability`] can be either a raw permission [`Identifier`] or an object that references a permission and extends its scope.", - "anyOf": [ - { - "description": "Reference a permission or permission set by identifier.", - "allOf": [ - { - "$ref": "#/definitions/Identifier" - } - ] - }, - { - "description": "Reference a permission or permission set by identifier and extends its scope.", - "type": "object", - "allOf": [ - { - "properties": { - "identifier": { - "description": "Identifier of the permission or permission set.", - "allOf": [ - { - "$ref": "#/definitions/Identifier" - } - ] - }, - "allow": { - "description": "Data that defines what is allowed by the scope.", - "type": [ - "array", - "null" - ], - "items": { - "$ref": "#/definitions/Value" - } - }, - "deny": { - "description": "Data that defines what is denied by the scope. This should be prioritized by validation logic.", - "type": [ - "array", - "null" - ], - "items": { - "$ref": "#/definitions/Value" - } - } - } - } - ], - "required": [ - "identifier" - ] - } - ] - }, - "Identifier": { - "description": "Permission identifier", - "oneOf": [ - { - "description": "Default core plugins set.\n#### This default permission set includes:\n\n- `core:path:default`\n- `core:event:default`\n- `core:window:default`\n- `core:webview:default`\n- `core:app:default`\n- `core:image:default`\n- `core:resources:default`\n- `core:menu:default`\n- `core:tray:default`", - "type": "string", - "const": "core:default", - "markdownDescription": "Default core plugins set.\n#### This default permission set includes:\n\n- `core:path:default`\n- `core:event:default`\n- `core:window:default`\n- `core:webview:default`\n- `core:app:default`\n- `core:image:default`\n- `core:resources:default`\n- `core:menu:default`\n- `core:tray:default`" - }, - { - "description": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-version`\n- `allow-name`\n- `allow-tauri-version`\n- `allow-identifier`\n- `allow-bundle-type`\n- `allow-register-listener`\n- `allow-remove-listener`\n- `allow-supports-multiple-windows`", - "type": "string", - "const": "core:app:default", - "markdownDescription": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-version`\n- `allow-name`\n- `allow-tauri-version`\n- `allow-identifier`\n- `allow-bundle-type`\n- `allow-register-listener`\n- `allow-remove-listener`\n- `allow-supports-multiple-windows`" - }, - { - "description": "Enables the app_hide command without any pre-configured scope.", - "type": "string", - "const": "core:app:allow-app-hide", - "markdownDescription": "Enables the app_hide command without any pre-configured scope." - }, - { - "description": "Enables the app_show command without any pre-configured scope.", - "type": "string", - "const": "core:app:allow-app-show", - "markdownDescription": "Enables the app_show command without any pre-configured scope." - }, - { - "description": "Enables the bundle_type command without any pre-configured scope.", - "type": "string", - "const": "core:app:allow-bundle-type", - "markdownDescription": "Enables the bundle_type command without any pre-configured scope." - }, - { - "description": "Enables the default_window_icon command without any pre-configured scope.", - "type": "string", - "const": "core:app:allow-default-window-icon", - "markdownDescription": "Enables the default_window_icon command without any pre-configured scope." - }, - { - "description": "Enables the fetch_data_store_identifiers command without any pre-configured scope.", - "type": "string", - "const": "core:app:allow-fetch-data-store-identifiers", - "markdownDescription": "Enables the fetch_data_store_identifiers command without any pre-configured scope." - }, - { - "description": "Enables the identifier command without any pre-configured scope.", - "type": "string", - "const": "core:app:allow-identifier", - "markdownDescription": "Enables the identifier command without any pre-configured scope." - }, - { - "description": "Enables the name command without any pre-configured scope.", - "type": "string", - "const": "core:app:allow-name", - "markdownDescription": "Enables the name command without any pre-configured scope." - }, - { - "description": "Enables the register_listener command without any pre-configured scope.", - "type": "string", - "const": "core:app:allow-register-listener", - "markdownDescription": "Enables the register_listener command without any pre-configured scope." - }, - { - "description": "Enables the remove_data_store command without any pre-configured scope.", - "type": "string", - "const": "core:app:allow-remove-data-store", - "markdownDescription": "Enables the remove_data_store command without any pre-configured scope." - }, - { - "description": "Enables the remove_listener command without any pre-configured scope.", - "type": "string", - "const": "core:app:allow-remove-listener", - "markdownDescription": "Enables the remove_listener command without any pre-configured scope." - }, - { - "description": "Enables the set_app_theme command without any pre-configured scope.", - "type": "string", - "const": "core:app:allow-set-app-theme", - "markdownDescription": "Enables the set_app_theme command without any pre-configured scope." - }, - { - "description": "Enables the set_dock_visibility command without any pre-configured scope.", - "type": "string", - "const": "core:app:allow-set-dock-visibility", - "markdownDescription": "Enables the set_dock_visibility command without any pre-configured scope." - }, - { - "description": "Enables the supports_multiple_windows command without any pre-configured scope.", - "type": "string", - "const": "core:app:allow-supports-multiple-windows", - "markdownDescription": "Enables the supports_multiple_windows command without any pre-configured scope." - }, - { - "description": "Enables the tauri_version command without any pre-configured scope.", - "type": "string", - "const": "core:app:allow-tauri-version", - "markdownDescription": "Enables the tauri_version command without any pre-configured scope." - }, - { - "description": "Enables the version command without any pre-configured scope.", - "type": "string", - "const": "core:app:allow-version", - "markdownDescription": "Enables the version command without any pre-configured scope." - }, - { - "description": "Denies the app_hide command without any pre-configured scope.", - "type": "string", - "const": "core:app:deny-app-hide", - "markdownDescription": "Denies the app_hide command without any pre-configured scope." - }, - { - "description": "Denies the app_show command without any pre-configured scope.", - "type": "string", - "const": "core:app:deny-app-show", - "markdownDescription": "Denies the app_show command without any pre-configured scope." - }, - { - "description": "Denies the bundle_type command without any pre-configured scope.", - "type": "string", - "const": "core:app:deny-bundle-type", - "markdownDescription": "Denies the bundle_type command without any pre-configured scope." - }, - { - "description": "Denies the default_window_icon command without any pre-configured scope.", - "type": "string", - "const": "core:app:deny-default-window-icon", - "markdownDescription": "Denies the default_window_icon command without any pre-configured scope." - }, - { - "description": "Denies the fetch_data_store_identifiers command without any pre-configured scope.", - "type": "string", - "const": "core:app:deny-fetch-data-store-identifiers", - "markdownDescription": "Denies the fetch_data_store_identifiers command without any pre-configured scope." - }, - { - "description": "Denies the identifier command without any pre-configured scope.", - "type": "string", - "const": "core:app:deny-identifier", - "markdownDescription": "Denies the identifier command without any pre-configured scope." - }, - { - "description": "Denies the name command without any pre-configured scope.", - "type": "string", - "const": "core:app:deny-name", - "markdownDescription": "Denies the name command without any pre-configured scope." - }, - { - "description": "Denies the register_listener command without any pre-configured scope.", - "type": "string", - "const": "core:app:deny-register-listener", - "markdownDescription": "Denies the register_listener command without any pre-configured scope." - }, - { - "description": "Denies the remove_data_store command without any pre-configured scope.", - "type": "string", - "const": "core:app:deny-remove-data-store", - "markdownDescription": "Denies the remove_data_store command without any pre-configured scope." - }, - { - "description": "Denies the remove_listener command without any pre-configured scope.", - "type": "string", - "const": "core:app:deny-remove-listener", - "markdownDescription": "Denies the remove_listener command without any pre-configured scope." - }, - { - "description": "Denies the set_app_theme command without any pre-configured scope.", - "type": "string", - "const": "core:app:deny-set-app-theme", - "markdownDescription": "Denies the set_app_theme command without any pre-configured scope." - }, - { - "description": "Denies the set_dock_visibility command without any pre-configured scope.", - "type": "string", - "const": "core:app:deny-set-dock-visibility", - "markdownDescription": "Denies the set_dock_visibility command without any pre-configured scope." - }, - { - "description": "Denies the supports_multiple_windows command without any pre-configured scope.", - "type": "string", - "const": "core:app:deny-supports-multiple-windows", - "markdownDescription": "Denies the supports_multiple_windows command without any pre-configured scope." - }, - { - "description": "Denies the tauri_version command without any pre-configured scope.", - "type": "string", - "const": "core:app:deny-tauri-version", - "markdownDescription": "Denies the tauri_version command without any pre-configured scope." - }, - { - "description": "Denies the version command without any pre-configured scope.", - "type": "string", - "const": "core:app:deny-version", - "markdownDescription": "Denies the version command without any pre-configured scope." - }, - { - "description": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-listen`\n- `allow-unlisten`\n- `allow-emit`\n- `allow-emit-to`", - "type": "string", - "const": "core:event:default", - "markdownDescription": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-listen`\n- `allow-unlisten`\n- `allow-emit`\n- `allow-emit-to`" - }, - { - "description": "Enables the emit command without any pre-configured scope.", - "type": "string", - "const": "core:event:allow-emit", - "markdownDescription": "Enables the emit command without any pre-configured scope." - }, - { - "description": "Enables the emit_to command without any pre-configured scope.", - "type": "string", - "const": "core:event:allow-emit-to", - "markdownDescription": "Enables the emit_to command without any pre-configured scope." - }, - { - "description": "Enables the listen command without any pre-configured scope.", - "type": "string", - "const": "core:event:allow-listen", - "markdownDescription": "Enables the listen command without any pre-configured scope." - }, - { - "description": "Enables the unlisten command without any pre-configured scope.", - "type": "string", - "const": "core:event:allow-unlisten", - "markdownDescription": "Enables the unlisten command without any pre-configured scope." - }, - { - "description": "Denies the emit command without any pre-configured scope.", - "type": "string", - "const": "core:event:deny-emit", - "markdownDescription": "Denies the emit command without any pre-configured scope." - }, - { - "description": "Denies the emit_to command without any pre-configured scope.", - "type": "string", - "const": "core:event:deny-emit-to", - "markdownDescription": "Denies the emit_to command without any pre-configured scope." - }, - { - "description": "Denies the listen command without any pre-configured scope.", - "type": "string", - "const": "core:event:deny-listen", - "markdownDescription": "Denies the listen command without any pre-configured scope." - }, - { - "description": "Denies the unlisten command without any pre-configured scope.", - "type": "string", - "const": "core:event:deny-unlisten", - "markdownDescription": "Denies the unlisten command without any pre-configured scope." - }, - { - "description": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-new`\n- `allow-from-bytes`\n- `allow-from-path`\n- `allow-rgba`\n- `allow-size`", - "type": "string", - "const": "core:image:default", - "markdownDescription": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-new`\n- `allow-from-bytes`\n- `allow-from-path`\n- `allow-rgba`\n- `allow-size`" - }, - { - "description": "Enables the from_bytes command without any pre-configured scope.", - "type": "string", - "const": "core:image:allow-from-bytes", - "markdownDescription": "Enables the from_bytes command without any pre-configured scope." - }, - { - "description": "Enables the from_path command without any pre-configured scope.", - "type": "string", - "const": "core:image:allow-from-path", - "markdownDescription": "Enables the from_path command without any pre-configured scope." - }, - { - "description": "Enables the new command without any pre-configured scope.", - "type": "string", - "const": "core:image:allow-new", - "markdownDescription": "Enables the new command without any pre-configured scope." - }, - { - "description": "Enables the rgba command without any pre-configured scope.", - "type": "string", - "const": "core:image:allow-rgba", - "markdownDescription": "Enables the rgba command without any pre-configured scope." - }, - { - "description": "Enables the size command without any pre-configured scope.", - "type": "string", - "const": "core:image:allow-size", - "markdownDescription": "Enables the size command without any pre-configured scope." - }, - { - "description": "Denies the from_bytes command without any pre-configured scope.", - "type": "string", - "const": "core:image:deny-from-bytes", - "markdownDescription": "Denies the from_bytes command without any pre-configured scope." - }, - { - "description": "Denies the from_path command without any pre-configured scope.", - "type": "string", - "const": "core:image:deny-from-path", - "markdownDescription": "Denies the from_path command without any pre-configured scope." - }, - { - "description": "Denies the new command without any pre-configured scope.", - "type": "string", - "const": "core:image:deny-new", - "markdownDescription": "Denies the new command without any pre-configured scope." - }, - { - "description": "Denies the rgba command without any pre-configured scope.", - "type": "string", - "const": "core:image:deny-rgba", - "markdownDescription": "Denies the rgba command without any pre-configured scope." - }, - { - "description": "Denies the size command without any pre-configured scope.", - "type": "string", - "const": "core:image:deny-size", - "markdownDescription": "Denies the size command without any pre-configured scope." - }, - { - "description": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-new`\n- `allow-append`\n- `allow-prepend`\n- `allow-insert`\n- `allow-remove`\n- `allow-remove-at`\n- `allow-items`\n- `allow-get`\n- `allow-popup`\n- `allow-create-default`\n- `allow-set-as-app-menu`\n- `allow-set-as-window-menu`\n- `allow-text`\n- `allow-set-text`\n- `allow-is-enabled`\n- `allow-set-enabled`\n- `allow-set-accelerator`\n- `allow-set-as-windows-menu-for-nsapp`\n- `allow-set-as-help-menu-for-nsapp`\n- `allow-is-checked`\n- `allow-set-checked`\n- `allow-set-icon`", - "type": "string", - "const": "core:menu:default", - "markdownDescription": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-new`\n- `allow-append`\n- `allow-prepend`\n- `allow-insert`\n- `allow-remove`\n- `allow-remove-at`\n- `allow-items`\n- `allow-get`\n- `allow-popup`\n- `allow-create-default`\n- `allow-set-as-app-menu`\n- `allow-set-as-window-menu`\n- `allow-text`\n- `allow-set-text`\n- `allow-is-enabled`\n- `allow-set-enabled`\n- `allow-set-accelerator`\n- `allow-set-as-windows-menu-for-nsapp`\n- `allow-set-as-help-menu-for-nsapp`\n- `allow-is-checked`\n- `allow-set-checked`\n- `allow-set-icon`" - }, - { - "description": "Enables the append command without any pre-configured scope.", - "type": "string", - "const": "core:menu:allow-append", - "markdownDescription": "Enables the append command without any pre-configured scope." - }, - { - "description": "Enables the create_default command without any pre-configured scope.", - "type": "string", - "const": "core:menu:allow-create-default", - "markdownDescription": "Enables the create_default command without any pre-configured scope." - }, - { - "description": "Enables the get command without any pre-configured scope.", - "type": "string", - "const": "core:menu:allow-get", - "markdownDescription": "Enables the get command without any pre-configured scope." - }, - { - "description": "Enables the insert command without any pre-configured scope.", - "type": "string", - "const": "core:menu:allow-insert", - "markdownDescription": "Enables the insert command without any pre-configured scope." - }, - { - "description": "Enables the is_checked command without any pre-configured scope.", - "type": "string", - "const": "core:menu:allow-is-checked", - "markdownDescription": "Enables the is_checked command without any pre-configured scope." - }, - { - "description": "Enables the is_enabled command without any pre-configured scope.", - "type": "string", - "const": "core:menu:allow-is-enabled", - "markdownDescription": "Enables the is_enabled command without any pre-configured scope." - }, - { - "description": "Enables the items command without any pre-configured scope.", - "type": "string", - "const": "core:menu:allow-items", - "markdownDescription": "Enables the items command without any pre-configured scope." - }, - { - "description": "Enables the new command without any pre-configured scope.", - "type": "string", - "const": "core:menu:allow-new", - "markdownDescription": "Enables the new command without any pre-configured scope." - }, - { - "description": "Enables the popup command without any pre-configured scope.", - "type": "string", - "const": "core:menu:allow-popup", - "markdownDescription": "Enables the popup command without any pre-configured scope." - }, - { - "description": "Enables the prepend command without any pre-configured scope.", - "type": "string", - "const": "core:menu:allow-prepend", - "markdownDescription": "Enables the prepend command without any pre-configured scope." - }, - { - "description": "Enables the remove command without any pre-configured scope.", - "type": "string", - "const": "core:menu:allow-remove", - "markdownDescription": "Enables the remove command without any pre-configured scope." - }, - { - "description": "Enables the remove_at command without any pre-configured scope.", - "type": "string", - "const": "core:menu:allow-remove-at", - "markdownDescription": "Enables the remove_at command without any pre-configured scope." - }, - { - "description": "Enables the set_accelerator command without any pre-configured scope.", - "type": "string", - "const": "core:menu:allow-set-accelerator", - "markdownDescription": "Enables the set_accelerator command without any pre-configured scope." - }, - { - "description": "Enables the set_as_app_menu command without any pre-configured scope.", - "type": "string", - "const": "core:menu:allow-set-as-app-menu", - "markdownDescription": "Enables the set_as_app_menu command without any pre-configured scope." - }, - { - "description": "Enables the set_as_help_menu_for_nsapp command without any pre-configured scope.", - "type": "string", - "const": "core:menu:allow-set-as-help-menu-for-nsapp", - "markdownDescription": "Enables the set_as_help_menu_for_nsapp command without any pre-configured scope." - }, - { - "description": "Enables the set_as_window_menu command without any pre-configured scope.", - "type": "string", - "const": "core:menu:allow-set-as-window-menu", - "markdownDescription": "Enables the set_as_window_menu command without any pre-configured scope." - }, - { - "description": "Enables the set_as_windows_menu_for_nsapp command without any pre-configured scope.", - "type": "string", - "const": "core:menu:allow-set-as-windows-menu-for-nsapp", - "markdownDescription": "Enables the set_as_windows_menu_for_nsapp command without any pre-configured scope." - }, - { - "description": "Enables the set_checked command without any pre-configured scope.", - "type": "string", - "const": "core:menu:allow-set-checked", - "markdownDescription": "Enables the set_checked command without any pre-configured scope." - }, - { - "description": "Enables the set_enabled command without any pre-configured scope.", - "type": "string", - "const": "core:menu:allow-set-enabled", - "markdownDescription": "Enables the set_enabled command without any pre-configured scope." - }, - { - "description": "Enables the set_icon command without any pre-configured scope.", - "type": "string", - "const": "core:menu:allow-set-icon", - "markdownDescription": "Enables the set_icon command without any pre-configured scope." - }, - { - "description": "Enables the set_text command without any pre-configured scope.", - "type": "string", - "const": "core:menu:allow-set-text", - "markdownDescription": "Enables the set_text command without any pre-configured scope." - }, - { - "description": "Enables the text command without any pre-configured scope.", - "type": "string", - "const": "core:menu:allow-text", - "markdownDescription": "Enables the text command without any pre-configured scope." - }, - { - "description": "Denies the append command without any pre-configured scope.", - "type": "string", - "const": "core:menu:deny-append", - "markdownDescription": "Denies the append command without any pre-configured scope." - }, - { - "description": "Denies the create_default command without any pre-configured scope.", - "type": "string", - "const": "core:menu:deny-create-default", - "markdownDescription": "Denies the create_default command without any pre-configured scope." - }, - { - "description": "Denies the get command without any pre-configured scope.", - "type": "string", - "const": "core:menu:deny-get", - "markdownDescription": "Denies the get command without any pre-configured scope." - }, - { - "description": "Denies the insert command without any pre-configured scope.", - "type": "string", - "const": "core:menu:deny-insert", - "markdownDescription": "Denies the insert command without any pre-configured scope." - }, - { - "description": "Denies the is_checked command without any pre-configured scope.", - "type": "string", - "const": "core:menu:deny-is-checked", - "markdownDescription": "Denies the is_checked command without any pre-configured scope." - }, - { - "description": "Denies the is_enabled command without any pre-configured scope.", - "type": "string", - "const": "core:menu:deny-is-enabled", - "markdownDescription": "Denies the is_enabled command without any pre-configured scope." - }, - { - "description": "Denies the items command without any pre-configured scope.", - "type": "string", - "const": "core:menu:deny-items", - "markdownDescription": "Denies the items command without any pre-configured scope." - }, - { - "description": "Denies the new command without any pre-configured scope.", - "type": "string", - "const": "core:menu:deny-new", - "markdownDescription": "Denies the new command without any pre-configured scope." - }, - { - "description": "Denies the popup command without any pre-configured scope.", - "type": "string", - "const": "core:menu:deny-popup", - "markdownDescription": "Denies the popup command without any pre-configured scope." - }, - { - "description": "Denies the prepend command without any pre-configured scope.", - "type": "string", - "const": "core:menu:deny-prepend", - "markdownDescription": "Denies the prepend command without any pre-configured scope." - }, - { - "description": "Denies the remove command without any pre-configured scope.", - "type": "string", - "const": "core:menu:deny-remove", - "markdownDescription": "Denies the remove command without any pre-configured scope." - }, - { - "description": "Denies the remove_at command without any pre-configured scope.", - "type": "string", - "const": "core:menu:deny-remove-at", - "markdownDescription": "Denies the remove_at command without any pre-configured scope." - }, - { - "description": "Denies the set_accelerator command without any pre-configured scope.", - "type": "string", - "const": "core:menu:deny-set-accelerator", - "markdownDescription": "Denies the set_accelerator command without any pre-configured scope." - }, - { - "description": "Denies the set_as_app_menu command without any pre-configured scope.", - "type": "string", - "const": "core:menu:deny-set-as-app-menu", - "markdownDescription": "Denies the set_as_app_menu command without any pre-configured scope." - }, - { - "description": "Denies the set_as_help_menu_for_nsapp command without any pre-configured scope.", - "type": "string", - "const": "core:menu:deny-set-as-help-menu-for-nsapp", - "markdownDescription": "Denies the set_as_help_menu_for_nsapp command without any pre-configured scope." - }, - { - "description": "Denies the set_as_window_menu command without any pre-configured scope.", - "type": "string", - "const": "core:menu:deny-set-as-window-menu", - "markdownDescription": "Denies the set_as_window_menu command without any pre-configured scope." - }, - { - "description": "Denies the set_as_windows_menu_for_nsapp command without any pre-configured scope.", - "type": "string", - "const": "core:menu:deny-set-as-windows-menu-for-nsapp", - "markdownDescription": "Denies the set_as_windows_menu_for_nsapp command without any pre-configured scope." - }, - { - "description": "Denies the set_checked command without any pre-configured scope.", - "type": "string", - "const": "core:menu:deny-set-checked", - "markdownDescription": "Denies the set_checked command without any pre-configured scope." - }, - { - "description": "Denies the set_enabled command without any pre-configured scope.", - "type": "string", - "const": "core:menu:deny-set-enabled", - "markdownDescription": "Denies the set_enabled command without any pre-configured scope." - }, - { - "description": "Denies the set_icon command without any pre-configured scope.", - "type": "string", - "const": "core:menu:deny-set-icon", - "markdownDescription": "Denies the set_icon command without any pre-configured scope." - }, - { - "description": "Denies the set_text command without any pre-configured scope.", - "type": "string", - "const": "core:menu:deny-set-text", - "markdownDescription": "Denies the set_text command without any pre-configured scope." - }, - { - "description": "Denies the text command without any pre-configured scope.", - "type": "string", - "const": "core:menu:deny-text", - "markdownDescription": "Denies the text command without any pre-configured scope." - }, - { - "description": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-resolve-directory`\n- `allow-resolve`\n- `allow-normalize`\n- `allow-join`\n- `allow-dirname`\n- `allow-extname`\n- `allow-basename`\n- `allow-is-absolute`", - "type": "string", - "const": "core:path:default", - "markdownDescription": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-resolve-directory`\n- `allow-resolve`\n- `allow-normalize`\n- `allow-join`\n- `allow-dirname`\n- `allow-extname`\n- `allow-basename`\n- `allow-is-absolute`" - }, - { - "description": "Enables the basename command without any pre-configured scope.", - "type": "string", - "const": "core:path:allow-basename", - "markdownDescription": "Enables the basename command without any pre-configured scope." - }, - { - "description": "Enables the dirname command without any pre-configured scope.", - "type": "string", - "const": "core:path:allow-dirname", - "markdownDescription": "Enables the dirname command without any pre-configured scope." - }, - { - "description": "Enables the extname command without any pre-configured scope.", - "type": "string", - "const": "core:path:allow-extname", - "markdownDescription": "Enables the extname command without any pre-configured scope." - }, - { - "description": "Enables the is_absolute command without any pre-configured scope.", - "type": "string", - "const": "core:path:allow-is-absolute", - "markdownDescription": "Enables the is_absolute command without any pre-configured scope." - }, - { - "description": "Enables the join command without any pre-configured scope.", - "type": "string", - "const": "core:path:allow-join", - "markdownDescription": "Enables the join command without any pre-configured scope." - }, - { - "description": "Enables the normalize command without any pre-configured scope.", - "type": "string", - "const": "core:path:allow-normalize", - "markdownDescription": "Enables the normalize command without any pre-configured scope." - }, - { - "description": "Enables the resolve command without any pre-configured scope.", - "type": "string", - "const": "core:path:allow-resolve", - "markdownDescription": "Enables the resolve command without any pre-configured scope." - }, - { - "description": "Enables the resolve_directory command without any pre-configured scope.", - "type": "string", - "const": "core:path:allow-resolve-directory", - "markdownDescription": "Enables the resolve_directory command without any pre-configured scope." - }, - { - "description": "Denies the basename command without any pre-configured scope.", - "type": "string", - "const": "core:path:deny-basename", - "markdownDescription": "Denies the basename command without any pre-configured scope." - }, - { - "description": "Denies the dirname command without any pre-configured scope.", - "type": "string", - "const": "core:path:deny-dirname", - "markdownDescription": "Denies the dirname command without any pre-configured scope." - }, - { - "description": "Denies the extname command without any pre-configured scope.", - "type": "string", - "const": "core:path:deny-extname", - "markdownDescription": "Denies the extname command without any pre-configured scope." - }, - { - "description": "Denies the is_absolute command without any pre-configured scope.", - "type": "string", - "const": "core:path:deny-is-absolute", - "markdownDescription": "Denies the is_absolute command without any pre-configured scope." - }, - { - "description": "Denies the join command without any pre-configured scope.", - "type": "string", - "const": "core:path:deny-join", - "markdownDescription": "Denies the join command without any pre-configured scope." - }, - { - "description": "Denies the normalize command without any pre-configured scope.", - "type": "string", - "const": "core:path:deny-normalize", - "markdownDescription": "Denies the normalize command without any pre-configured scope." - }, - { - "description": "Denies the resolve command without any pre-configured scope.", - "type": "string", - "const": "core:path:deny-resolve", - "markdownDescription": "Denies the resolve command without any pre-configured scope." - }, - { - "description": "Denies the resolve_directory command without any pre-configured scope.", - "type": "string", - "const": "core:path:deny-resolve-directory", - "markdownDescription": "Denies the resolve_directory command without any pre-configured scope." - }, - { - "description": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-close`", - "type": "string", - "const": "core:resources:default", - "markdownDescription": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-close`" - }, - { - "description": "Enables the close command without any pre-configured scope.", - "type": "string", - "const": "core:resources:allow-close", - "markdownDescription": "Enables the close command without any pre-configured scope." - }, - { - "description": "Denies the close command without any pre-configured scope.", - "type": "string", - "const": "core:resources:deny-close", - "markdownDescription": "Denies the close command without any pre-configured scope." - }, - { - "description": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-new`\n- `allow-get-by-id`\n- `allow-remove-by-id`\n- `allow-set-icon`\n- `allow-set-menu`\n- `allow-set-tooltip`\n- `allow-set-title`\n- `allow-set-visible`\n- `allow-set-temp-dir-path`\n- `allow-set-icon-as-template`\n- `allow-set-icon-with-as-template`\n- `allow-set-show-menu-on-left-click`", - "type": "string", - "const": "core:tray:default", - "markdownDescription": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-new`\n- `allow-get-by-id`\n- `allow-remove-by-id`\n- `allow-set-icon`\n- `allow-set-menu`\n- `allow-set-tooltip`\n- `allow-set-title`\n- `allow-set-visible`\n- `allow-set-temp-dir-path`\n- `allow-set-icon-as-template`\n- `allow-set-icon-with-as-template`\n- `allow-set-show-menu-on-left-click`" - }, - { - "description": "Enables the get_by_id command without any pre-configured scope.", - "type": "string", - "const": "core:tray:allow-get-by-id", - "markdownDescription": "Enables the get_by_id command without any pre-configured scope." - }, - { - "description": "Enables the new command without any pre-configured scope.", - "type": "string", - "const": "core:tray:allow-new", - "markdownDescription": "Enables the new command without any pre-configured scope." - }, - { - "description": "Enables the remove_by_id command without any pre-configured scope.", - "type": "string", - "const": "core:tray:allow-remove-by-id", - "markdownDescription": "Enables the remove_by_id command without any pre-configured scope." - }, - { - "description": "Enables the set_icon command without any pre-configured scope.", - "type": "string", - "const": "core:tray:allow-set-icon", - "markdownDescription": "Enables the set_icon command without any pre-configured scope." - }, - { - "description": "Enables the set_icon_as_template command without any pre-configured scope.", - "type": "string", - "const": "core:tray:allow-set-icon-as-template", - "markdownDescription": "Enables the set_icon_as_template command without any pre-configured scope." - }, - { - "description": "Enables the set_icon_with_as_template command without any pre-configured scope.", - "type": "string", - "const": "core:tray:allow-set-icon-with-as-template", - "markdownDescription": "Enables the set_icon_with_as_template command without any pre-configured scope." - }, - { - "description": "Enables the set_menu command without any pre-configured scope.", - "type": "string", - "const": "core:tray:allow-set-menu", - "markdownDescription": "Enables the set_menu command without any pre-configured scope." - }, - { - "description": "Enables the set_show_menu_on_left_click command without any pre-configured scope.", - "type": "string", - "const": "core:tray:allow-set-show-menu-on-left-click", - "markdownDescription": "Enables the set_show_menu_on_left_click command without any pre-configured scope." - }, - { - "description": "Enables the set_temp_dir_path command without any pre-configured scope.", - "type": "string", - "const": "core:tray:allow-set-temp-dir-path", - "markdownDescription": "Enables the set_temp_dir_path command without any pre-configured scope." - }, - { - "description": "Enables the set_title command without any pre-configured scope.", - "type": "string", - "const": "core:tray:allow-set-title", - "markdownDescription": "Enables the set_title command without any pre-configured scope." - }, - { - "description": "Enables the set_tooltip command without any pre-configured scope.", - "type": "string", - "const": "core:tray:allow-set-tooltip", - "markdownDescription": "Enables the set_tooltip command without any pre-configured scope." - }, - { - "description": "Enables the set_visible command without any pre-configured scope.", - "type": "string", - "const": "core:tray:allow-set-visible", - "markdownDescription": "Enables the set_visible command without any pre-configured scope." - }, - { - "description": "Denies the get_by_id command without any pre-configured scope.", - "type": "string", - "const": "core:tray:deny-get-by-id", - "markdownDescription": "Denies the get_by_id command without any pre-configured scope." - }, - { - "description": "Denies the new command without any pre-configured scope.", - "type": "string", - "const": "core:tray:deny-new", - "markdownDescription": "Denies the new command without any pre-configured scope." - }, - { - "description": "Denies the remove_by_id command without any pre-configured scope.", - "type": "string", - "const": "core:tray:deny-remove-by-id", - "markdownDescription": "Denies the remove_by_id command without any pre-configured scope." - }, - { - "description": "Denies the set_icon command without any pre-configured scope.", - "type": "string", - "const": "core:tray:deny-set-icon", - "markdownDescription": "Denies the set_icon command without any pre-configured scope." - }, - { - "description": "Denies the set_icon_as_template command without any pre-configured scope.", - "type": "string", - "const": "core:tray:deny-set-icon-as-template", - "markdownDescription": "Denies the set_icon_as_template command without any pre-configured scope." - }, - { - "description": "Denies the set_icon_with_as_template command without any pre-configured scope.", - "type": "string", - "const": "core:tray:deny-set-icon-with-as-template", - "markdownDescription": "Denies the set_icon_with_as_template command without any pre-configured scope." - }, - { - "description": "Denies the set_menu command without any pre-configured scope.", - "type": "string", - "const": "core:tray:deny-set-menu", - "markdownDescription": "Denies the set_menu command without any pre-configured scope." - }, - { - "description": "Denies the set_show_menu_on_left_click command without any pre-configured scope.", - "type": "string", - "const": "core:tray:deny-set-show-menu-on-left-click", - "markdownDescription": "Denies the set_show_menu_on_left_click command without any pre-configured scope." - }, - { - "description": "Denies the set_temp_dir_path command without any pre-configured scope.", - "type": "string", - "const": "core:tray:deny-set-temp-dir-path", - "markdownDescription": "Denies the set_temp_dir_path command without any pre-configured scope." - }, - { - "description": "Denies the set_title command without any pre-configured scope.", - "type": "string", - "const": "core:tray:deny-set-title", - "markdownDescription": "Denies the set_title command without any pre-configured scope." - }, - { - "description": "Denies the set_tooltip command without any pre-configured scope.", - "type": "string", - "const": "core:tray:deny-set-tooltip", - "markdownDescription": "Denies the set_tooltip command without any pre-configured scope." - }, - { - "description": "Denies the set_visible command without any pre-configured scope.", - "type": "string", - "const": "core:tray:deny-set-visible", - "markdownDescription": "Denies the set_visible command without any pre-configured scope." - }, - { - "description": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-get-all-webviews`\n- `allow-webview-position`\n- `allow-webview-size`\n- `allow-internal-toggle-devtools`", - "type": "string", - "const": "core:webview:default", - "markdownDescription": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-get-all-webviews`\n- `allow-webview-position`\n- `allow-webview-size`\n- `allow-internal-toggle-devtools`" - }, - { - "description": "Enables the clear_all_browsing_data command without any pre-configured scope.", - "type": "string", - "const": "core:webview:allow-clear-all-browsing-data", - "markdownDescription": "Enables the clear_all_browsing_data command without any pre-configured scope." - }, - { - "description": "Enables the create_webview command without any pre-configured scope.", - "type": "string", - "const": "core:webview:allow-create-webview", - "markdownDescription": "Enables the create_webview command without any pre-configured scope." - }, - { - "description": "Enables the create_webview_window command without any pre-configured scope.", - "type": "string", - "const": "core:webview:allow-create-webview-window", - "markdownDescription": "Enables the create_webview_window command without any pre-configured scope." - }, - { - "description": "Enables the get_all_webviews command without any pre-configured scope.", - "type": "string", - "const": "core:webview:allow-get-all-webviews", - "markdownDescription": "Enables the get_all_webviews command without any pre-configured scope." - }, - { - "description": "Enables the internal_toggle_devtools command without any pre-configured scope.", - "type": "string", - "const": "core:webview:allow-internal-toggle-devtools", - "markdownDescription": "Enables the internal_toggle_devtools command without any pre-configured scope." - }, - { - "description": "Enables the print command without any pre-configured scope.", - "type": "string", - "const": "core:webview:allow-print", - "markdownDescription": "Enables the print command without any pre-configured scope." - }, - { - "description": "Enables the reparent command without any pre-configured scope.", - "type": "string", - "const": "core:webview:allow-reparent", - "markdownDescription": "Enables the reparent command without any pre-configured scope." - }, - { - "description": "Enables the set_webview_auto_resize command without any pre-configured scope.", - "type": "string", - "const": "core:webview:allow-set-webview-auto-resize", - "markdownDescription": "Enables the set_webview_auto_resize command without any pre-configured scope." - }, - { - "description": "Enables the set_webview_background_color command without any pre-configured scope.", - "type": "string", - "const": "core:webview:allow-set-webview-background-color", - "markdownDescription": "Enables the set_webview_background_color command without any pre-configured scope." - }, - { - "description": "Enables the set_webview_focus command without any pre-configured scope.", - "type": "string", - "const": "core:webview:allow-set-webview-focus", - "markdownDescription": "Enables the set_webview_focus command without any pre-configured scope." - }, - { - "description": "Enables the set_webview_position command without any pre-configured scope.", - "type": "string", - "const": "core:webview:allow-set-webview-position", - "markdownDescription": "Enables the set_webview_position command without any pre-configured scope." - }, - { - "description": "Enables the set_webview_size command without any pre-configured scope.", - "type": "string", - "const": "core:webview:allow-set-webview-size", - "markdownDescription": "Enables the set_webview_size command without any pre-configured scope." - }, - { - "description": "Enables the set_webview_zoom command without any pre-configured scope.", - "type": "string", - "const": "core:webview:allow-set-webview-zoom", - "markdownDescription": "Enables the set_webview_zoom command without any pre-configured scope." - }, - { - "description": "Enables the webview_close command without any pre-configured scope.", - "type": "string", - "const": "core:webview:allow-webview-close", - "markdownDescription": "Enables the webview_close command without any pre-configured scope." - }, - { - "description": "Enables the webview_hide command without any pre-configured scope.", - "type": "string", - "const": "core:webview:allow-webview-hide", - "markdownDescription": "Enables the webview_hide command without any pre-configured scope." - }, - { - "description": "Enables the webview_position command without any pre-configured scope.", - "type": "string", - "const": "core:webview:allow-webview-position", - "markdownDescription": "Enables the webview_position command without any pre-configured scope." - }, - { - "description": "Enables the webview_show command without any pre-configured scope.", - "type": "string", - "const": "core:webview:allow-webview-show", - "markdownDescription": "Enables the webview_show command without any pre-configured scope." - }, - { - "description": "Enables the webview_size command without any pre-configured scope.", - "type": "string", - "const": "core:webview:allow-webview-size", - "markdownDescription": "Enables the webview_size command without any pre-configured scope." - }, - { - "description": "Denies the clear_all_browsing_data command without any pre-configured scope.", - "type": "string", - "const": "core:webview:deny-clear-all-browsing-data", - "markdownDescription": "Denies the clear_all_browsing_data command without any pre-configured scope." - }, - { - "description": "Denies the create_webview command without any pre-configured scope.", - "type": "string", - "const": "core:webview:deny-create-webview", - "markdownDescription": "Denies the create_webview command without any pre-configured scope." - }, - { - "description": "Denies the create_webview_window command without any pre-configured scope.", - "type": "string", - "const": "core:webview:deny-create-webview-window", - "markdownDescription": "Denies the create_webview_window command without any pre-configured scope." - }, - { - "description": "Denies the get_all_webviews command without any pre-configured scope.", - "type": "string", - "const": "core:webview:deny-get-all-webviews", - "markdownDescription": "Denies the get_all_webviews command without any pre-configured scope." - }, - { - "description": "Denies the internal_toggle_devtools command without any pre-configured scope.", - "type": "string", - "const": "core:webview:deny-internal-toggle-devtools", - "markdownDescription": "Denies the internal_toggle_devtools command without any pre-configured scope." - }, - { - "description": "Denies the print command without any pre-configured scope.", - "type": "string", - "const": "core:webview:deny-print", - "markdownDescription": "Denies the print command without any pre-configured scope." - }, - { - "description": "Denies the reparent command without any pre-configured scope.", - "type": "string", - "const": "core:webview:deny-reparent", - "markdownDescription": "Denies the reparent command without any pre-configured scope." - }, - { - "description": "Denies the set_webview_auto_resize command without any pre-configured scope.", - "type": "string", - "const": "core:webview:deny-set-webview-auto-resize", - "markdownDescription": "Denies the set_webview_auto_resize command without any pre-configured scope." - }, - { - "description": "Denies the set_webview_background_color command without any pre-configured scope.", - "type": "string", - "const": "core:webview:deny-set-webview-background-color", - "markdownDescription": "Denies the set_webview_background_color command without any pre-configured scope." - }, - { - "description": "Denies the set_webview_focus command without any pre-configured scope.", - "type": "string", - "const": "core:webview:deny-set-webview-focus", - "markdownDescription": "Denies the set_webview_focus command without any pre-configured scope." - }, - { - "description": "Denies the set_webview_position command without any pre-configured scope.", - "type": "string", - "const": "core:webview:deny-set-webview-position", - "markdownDescription": "Denies the set_webview_position command without any pre-configured scope." - }, - { - "description": "Denies the set_webview_size command without any pre-configured scope.", - "type": "string", - "const": "core:webview:deny-set-webview-size", - "markdownDescription": "Denies the set_webview_size command without any pre-configured scope." - }, - { - "description": "Denies the set_webview_zoom command without any pre-configured scope.", - "type": "string", - "const": "core:webview:deny-set-webview-zoom", - "markdownDescription": "Denies the set_webview_zoom command without any pre-configured scope." - }, - { - "description": "Denies the webview_close command without any pre-configured scope.", - "type": "string", - "const": "core:webview:deny-webview-close", - "markdownDescription": "Denies the webview_close command without any pre-configured scope." - }, - { - "description": "Denies the webview_hide command without any pre-configured scope.", - "type": "string", - "const": "core:webview:deny-webview-hide", - "markdownDescription": "Denies the webview_hide command without any pre-configured scope." - }, - { - "description": "Denies the webview_position command without any pre-configured scope.", - "type": "string", - "const": "core:webview:deny-webview-position", - "markdownDescription": "Denies the webview_position command without any pre-configured scope." - }, - { - "description": "Denies the webview_show command without any pre-configured scope.", - "type": "string", - "const": "core:webview:deny-webview-show", - "markdownDescription": "Denies the webview_show command without any pre-configured scope." - }, - { - "description": "Denies the webview_size command without any pre-configured scope.", - "type": "string", - "const": "core:webview:deny-webview-size", - "markdownDescription": "Denies the webview_size command without any pre-configured scope." - }, - { - "description": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-get-all-windows`\n- `allow-scale-factor`\n- `allow-inner-position`\n- `allow-outer-position`\n- `allow-inner-size`\n- `allow-outer-size`\n- `allow-is-fullscreen`\n- `allow-is-minimized`\n- `allow-is-maximized`\n- `allow-is-focused`\n- `allow-is-decorated`\n- `allow-is-resizable`\n- `allow-is-maximizable`\n- `allow-is-minimizable`\n- `allow-is-closable`\n- `allow-is-visible`\n- `allow-is-enabled`\n- `allow-title`\n- `allow-current-monitor`\n- `allow-primary-monitor`\n- `allow-monitor-from-point`\n- `allow-available-monitors`\n- `allow-cursor-position`\n- `allow-theme`\n- `allow-is-always-on-top`\n- `allow-activity-name`\n- `allow-scene-identifier`\n- `allow-internal-toggle-maximize`", - "type": "string", - "const": "core:window:default", - "markdownDescription": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-get-all-windows`\n- `allow-scale-factor`\n- `allow-inner-position`\n- `allow-outer-position`\n- `allow-inner-size`\n- `allow-outer-size`\n- `allow-is-fullscreen`\n- `allow-is-minimized`\n- `allow-is-maximized`\n- `allow-is-focused`\n- `allow-is-decorated`\n- `allow-is-resizable`\n- `allow-is-maximizable`\n- `allow-is-minimizable`\n- `allow-is-closable`\n- `allow-is-visible`\n- `allow-is-enabled`\n- `allow-title`\n- `allow-current-monitor`\n- `allow-primary-monitor`\n- `allow-monitor-from-point`\n- `allow-available-monitors`\n- `allow-cursor-position`\n- `allow-theme`\n- `allow-is-always-on-top`\n- `allow-activity-name`\n- `allow-scene-identifier`\n- `allow-internal-toggle-maximize`" - }, - { - "description": "Enables the activity_name command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-activity-name", - "markdownDescription": "Enables the activity_name command without any pre-configured scope." - }, - { - "description": "Enables the available_monitors command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-available-monitors", - "markdownDescription": "Enables the available_monitors command without any pre-configured scope." - }, - { - "description": "Enables the center command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-center", - "markdownDescription": "Enables the center command without any pre-configured scope." - }, - { - "description": "Enables the close command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-close", - "markdownDescription": "Enables the close command without any pre-configured scope." - }, - { - "description": "Enables the create command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-create", - "markdownDescription": "Enables the create command without any pre-configured scope." - }, - { - "description": "Enables the current_monitor command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-current-monitor", - "markdownDescription": "Enables the current_monitor command without any pre-configured scope." - }, - { - "description": "Enables the cursor_position command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-cursor-position", - "markdownDescription": "Enables the cursor_position command without any pre-configured scope." - }, - { - "description": "Enables the destroy command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-destroy", - "markdownDescription": "Enables the destroy command without any pre-configured scope." - }, - { - "description": "Enables the get_all_windows command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-get-all-windows", - "markdownDescription": "Enables the get_all_windows command without any pre-configured scope." - }, - { - "description": "Enables the hide command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-hide", - "markdownDescription": "Enables the hide command without any pre-configured scope." - }, - { - "description": "Enables the inner_position command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-inner-position", - "markdownDescription": "Enables the inner_position command without any pre-configured scope." - }, - { - "description": "Enables the inner_size command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-inner-size", - "markdownDescription": "Enables the inner_size command without any pre-configured scope." - }, - { - "description": "Enables the internal_toggle_maximize command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-internal-toggle-maximize", - "markdownDescription": "Enables the internal_toggle_maximize command without any pre-configured scope." - }, - { - "description": "Enables the is_always_on_top command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-is-always-on-top", - "markdownDescription": "Enables the is_always_on_top command without any pre-configured scope." - }, - { - "description": "Enables the is_closable command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-is-closable", - "markdownDescription": "Enables the is_closable command without any pre-configured scope." - }, - { - "description": "Enables the is_decorated command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-is-decorated", - "markdownDescription": "Enables the is_decorated command without any pre-configured scope." - }, - { - "description": "Enables the is_enabled command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-is-enabled", - "markdownDescription": "Enables the is_enabled command without any pre-configured scope." - }, - { - "description": "Enables the is_focused command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-is-focused", - "markdownDescription": "Enables the is_focused command without any pre-configured scope." - }, - { - "description": "Enables the is_fullscreen command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-is-fullscreen", - "markdownDescription": "Enables the is_fullscreen command without any pre-configured scope." - }, - { - "description": "Enables the is_maximizable command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-is-maximizable", - "markdownDescription": "Enables the is_maximizable command without any pre-configured scope." - }, - { - "description": "Enables the is_maximized command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-is-maximized", - "markdownDescription": "Enables the is_maximized command without any pre-configured scope." - }, - { - "description": "Enables the is_minimizable command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-is-minimizable", - "markdownDescription": "Enables the is_minimizable command without any pre-configured scope." - }, - { - "description": "Enables the is_minimized command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-is-minimized", - "markdownDescription": "Enables the is_minimized command without any pre-configured scope." - }, - { - "description": "Enables the is_resizable command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-is-resizable", - "markdownDescription": "Enables the is_resizable command without any pre-configured scope." - }, - { - "description": "Enables the is_visible command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-is-visible", - "markdownDescription": "Enables the is_visible command without any pre-configured scope." - }, - { - "description": "Enables the maximize command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-maximize", - "markdownDescription": "Enables the maximize command without any pre-configured scope." - }, - { - "description": "Enables the minimize command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-minimize", - "markdownDescription": "Enables the minimize command without any pre-configured scope." - }, - { - "description": "Enables the monitor_from_point command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-monitor-from-point", - "markdownDescription": "Enables the monitor_from_point command without any pre-configured scope." - }, - { - "description": "Enables the outer_position command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-outer-position", - "markdownDescription": "Enables the outer_position command without any pre-configured scope." - }, - { - "description": "Enables the outer_size command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-outer-size", - "markdownDescription": "Enables the outer_size command without any pre-configured scope." - }, - { - "description": "Enables the primary_monitor command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-primary-monitor", - "markdownDescription": "Enables the primary_monitor command without any pre-configured scope." - }, - { - "description": "Enables the request_user_attention command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-request-user-attention", - "markdownDescription": "Enables the request_user_attention command without any pre-configured scope." - }, - { - "description": "Enables the scale_factor command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-scale-factor", - "markdownDescription": "Enables the scale_factor command without any pre-configured scope." - }, - { - "description": "Enables the scene_identifier command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-scene-identifier", - "markdownDescription": "Enables the scene_identifier command without any pre-configured scope." - }, - { - "description": "Enables the set_always_on_bottom command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-always-on-bottom", - "markdownDescription": "Enables the set_always_on_bottom command without any pre-configured scope." - }, - { - "description": "Enables the set_always_on_top command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-always-on-top", - "markdownDescription": "Enables the set_always_on_top command without any pre-configured scope." - }, - { - "description": "Enables the set_background_color command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-background-color", - "markdownDescription": "Enables the set_background_color command without any pre-configured scope." - }, - { - "description": "Enables the set_badge_count command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-badge-count", - "markdownDescription": "Enables the set_badge_count command without any pre-configured scope." - }, - { - "description": "Enables the set_badge_label command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-badge-label", - "markdownDescription": "Enables the set_badge_label command without any pre-configured scope." - }, - { - "description": "Enables the set_closable command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-closable", - "markdownDescription": "Enables the set_closable command without any pre-configured scope." - }, - { - "description": "Enables the set_content_protected command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-content-protected", - "markdownDescription": "Enables the set_content_protected command without any pre-configured scope." - }, - { - "description": "Enables the set_cursor_grab command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-cursor-grab", - "markdownDescription": "Enables the set_cursor_grab command without any pre-configured scope." - }, - { - "description": "Enables the set_cursor_icon command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-cursor-icon", - "markdownDescription": "Enables the set_cursor_icon command without any pre-configured scope." - }, - { - "description": "Enables the set_cursor_position command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-cursor-position", - "markdownDescription": "Enables the set_cursor_position command without any pre-configured scope." - }, - { - "description": "Enables the set_cursor_visible command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-cursor-visible", - "markdownDescription": "Enables the set_cursor_visible command without any pre-configured scope." - }, - { - "description": "Enables the set_decorations command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-decorations", - "markdownDescription": "Enables the set_decorations command without any pre-configured scope." - }, - { - "description": "Enables the set_effects command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-effects", - "markdownDescription": "Enables the set_effects command without any pre-configured scope." - }, - { - "description": "Enables the set_enabled command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-enabled", - "markdownDescription": "Enables the set_enabled command without any pre-configured scope." - }, - { - "description": "Enables the set_focus command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-focus", - "markdownDescription": "Enables the set_focus command without any pre-configured scope." - }, - { - "description": "Enables the set_focusable command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-focusable", - "markdownDescription": "Enables the set_focusable command without any pre-configured scope." - }, - { - "description": "Enables the set_fullscreen command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-fullscreen", - "markdownDescription": "Enables the set_fullscreen command without any pre-configured scope." - }, - { - "description": "Enables the set_icon command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-icon", - "markdownDescription": "Enables the set_icon command without any pre-configured scope." - }, - { - "description": "Enables the set_ignore_cursor_events command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-ignore-cursor-events", - "markdownDescription": "Enables the set_ignore_cursor_events command without any pre-configured scope." - }, - { - "description": "Enables the set_max_size command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-max-size", - "markdownDescription": "Enables the set_max_size command without any pre-configured scope." - }, - { - "description": "Enables the set_maximizable command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-maximizable", - "markdownDescription": "Enables the set_maximizable command without any pre-configured scope." - }, - { - "description": "Enables the set_min_size command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-min-size", - "markdownDescription": "Enables the set_min_size command without any pre-configured scope." - }, - { - "description": "Enables the set_minimizable command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-minimizable", - "markdownDescription": "Enables the set_minimizable command without any pre-configured scope." - }, - { - "description": "Enables the set_overlay_icon command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-overlay-icon", - "markdownDescription": "Enables the set_overlay_icon command without any pre-configured scope." - }, - { - "description": "Enables the set_position command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-position", - "markdownDescription": "Enables the set_position command without any pre-configured scope." - }, - { - "description": "Enables the set_progress_bar command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-progress-bar", - "markdownDescription": "Enables the set_progress_bar command without any pre-configured scope." - }, - { - "description": "Enables the set_resizable command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-resizable", - "markdownDescription": "Enables the set_resizable command without any pre-configured scope." - }, - { - "description": "Enables the set_shadow command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-shadow", - "markdownDescription": "Enables the set_shadow command without any pre-configured scope." - }, - { - "description": "Enables the set_simple_fullscreen command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-simple-fullscreen", - "markdownDescription": "Enables the set_simple_fullscreen command without any pre-configured scope." - }, - { - "description": "Enables the set_size command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-size", - "markdownDescription": "Enables the set_size command without any pre-configured scope." - }, - { - "description": "Enables the set_size_constraints command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-size-constraints", - "markdownDescription": "Enables the set_size_constraints command without any pre-configured scope." - }, - { - "description": "Enables the set_skip_taskbar command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-skip-taskbar", - "markdownDescription": "Enables the set_skip_taskbar command without any pre-configured scope." - }, - { - "description": "Enables the set_theme command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-theme", - "markdownDescription": "Enables the set_theme command without any pre-configured scope." - }, - { - "description": "Enables the set_title command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-title", - "markdownDescription": "Enables the set_title command without any pre-configured scope." - }, - { - "description": "Enables the set_title_bar_style command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-title-bar-style", - "markdownDescription": "Enables the set_title_bar_style command without any pre-configured scope." - }, - { - "description": "Enables the set_visible_on_all_workspaces command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-visible-on-all-workspaces", - "markdownDescription": "Enables the set_visible_on_all_workspaces command without any pre-configured scope." - }, - { - "description": "Enables the show command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-show", - "markdownDescription": "Enables the show command without any pre-configured scope." - }, - { - "description": "Enables the start_dragging command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-start-dragging", - "markdownDescription": "Enables the start_dragging command without any pre-configured scope." - }, - { - "description": "Enables the start_resize_dragging command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-start-resize-dragging", - "markdownDescription": "Enables the start_resize_dragging command without any pre-configured scope." - }, - { - "description": "Enables the theme command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-theme", - "markdownDescription": "Enables the theme command without any pre-configured scope." - }, - { - "description": "Enables the title command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-title", - "markdownDescription": "Enables the title command without any pre-configured scope." - }, - { - "description": "Enables the toggle_maximize command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-toggle-maximize", - "markdownDescription": "Enables the toggle_maximize command without any pre-configured scope." - }, - { - "description": "Enables the unmaximize command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-unmaximize", - "markdownDescription": "Enables the unmaximize command without any pre-configured scope." - }, - { - "description": "Enables the unminimize command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-unminimize", - "markdownDescription": "Enables the unminimize command without any pre-configured scope." - }, - { - "description": "Denies the activity_name command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-activity-name", - "markdownDescription": "Denies the activity_name command without any pre-configured scope." - }, - { - "description": "Denies the available_monitors command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-available-monitors", - "markdownDescription": "Denies the available_monitors command without any pre-configured scope." - }, - { - "description": "Denies the center command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-center", - "markdownDescription": "Denies the center command without any pre-configured scope." - }, - { - "description": "Denies the close command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-close", - "markdownDescription": "Denies the close command without any pre-configured scope." - }, - { - "description": "Denies the create command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-create", - "markdownDescription": "Denies the create command without any pre-configured scope." - }, - { - "description": "Denies the current_monitor command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-current-monitor", - "markdownDescription": "Denies the current_monitor command without any pre-configured scope." - }, - { - "description": "Denies the cursor_position command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-cursor-position", - "markdownDescription": "Denies the cursor_position command without any pre-configured scope." - }, - { - "description": "Denies the destroy command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-destroy", - "markdownDescription": "Denies the destroy command without any pre-configured scope." - }, - { - "description": "Denies the get_all_windows command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-get-all-windows", - "markdownDescription": "Denies the get_all_windows command without any pre-configured scope." - }, - { - "description": "Denies the hide command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-hide", - "markdownDescription": "Denies the hide command without any pre-configured scope." - }, - { - "description": "Denies the inner_position command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-inner-position", - "markdownDescription": "Denies the inner_position command without any pre-configured scope." - }, - { - "description": "Denies the inner_size command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-inner-size", - "markdownDescription": "Denies the inner_size command without any pre-configured scope." - }, - { - "description": "Denies the internal_toggle_maximize command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-internal-toggle-maximize", - "markdownDescription": "Denies the internal_toggle_maximize command without any pre-configured scope." - }, - { - "description": "Denies the is_always_on_top command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-is-always-on-top", - "markdownDescription": "Denies the is_always_on_top command without any pre-configured scope." - }, - { - "description": "Denies the is_closable command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-is-closable", - "markdownDescription": "Denies the is_closable command without any pre-configured scope." - }, - { - "description": "Denies the is_decorated command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-is-decorated", - "markdownDescription": "Denies the is_decorated command without any pre-configured scope." - }, - { - "description": "Denies the is_enabled command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-is-enabled", - "markdownDescription": "Denies the is_enabled command without any pre-configured scope." - }, - { - "description": "Denies the is_focused command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-is-focused", - "markdownDescription": "Denies the is_focused command without any pre-configured scope." - }, - { - "description": "Denies the is_fullscreen command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-is-fullscreen", - "markdownDescription": "Denies the is_fullscreen command without any pre-configured scope." - }, - { - "description": "Denies the is_maximizable command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-is-maximizable", - "markdownDescription": "Denies the is_maximizable command without any pre-configured scope." - }, - { - "description": "Denies the is_maximized command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-is-maximized", - "markdownDescription": "Denies the is_maximized command without any pre-configured scope." - }, - { - "description": "Denies the is_minimizable command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-is-minimizable", - "markdownDescription": "Denies the is_minimizable command without any pre-configured scope." - }, - { - "description": "Denies the is_minimized command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-is-minimized", - "markdownDescription": "Denies the is_minimized command without any pre-configured scope." - }, - { - "description": "Denies the is_resizable command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-is-resizable", - "markdownDescription": "Denies the is_resizable command without any pre-configured scope." - }, - { - "description": "Denies the is_visible command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-is-visible", - "markdownDescription": "Denies the is_visible command without any pre-configured scope." - }, - { - "description": "Denies the maximize command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-maximize", - "markdownDescription": "Denies the maximize command without any pre-configured scope." - }, - { - "description": "Denies the minimize command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-minimize", - "markdownDescription": "Denies the minimize command without any pre-configured scope." - }, - { - "description": "Denies the monitor_from_point command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-monitor-from-point", - "markdownDescription": "Denies the monitor_from_point command without any pre-configured scope." - }, - { - "description": "Denies the outer_position command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-outer-position", - "markdownDescription": "Denies the outer_position command without any pre-configured scope." - }, - { - "description": "Denies the outer_size command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-outer-size", - "markdownDescription": "Denies the outer_size command without any pre-configured scope." - }, - { - "description": "Denies the primary_monitor command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-primary-monitor", - "markdownDescription": "Denies the primary_monitor command without any pre-configured scope." - }, - { - "description": "Denies the request_user_attention command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-request-user-attention", - "markdownDescription": "Denies the request_user_attention command without any pre-configured scope." - }, - { - "description": "Denies the scale_factor command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-scale-factor", - "markdownDescription": "Denies the scale_factor command without any pre-configured scope." - }, - { - "description": "Denies the scene_identifier command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-scene-identifier", - "markdownDescription": "Denies the scene_identifier command without any pre-configured scope." - }, - { - "description": "Denies the set_always_on_bottom command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-always-on-bottom", - "markdownDescription": "Denies the set_always_on_bottom command without any pre-configured scope." - }, - { - "description": "Denies the set_always_on_top command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-always-on-top", - "markdownDescription": "Denies the set_always_on_top command without any pre-configured scope." - }, - { - "description": "Denies the set_background_color command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-background-color", - "markdownDescription": "Denies the set_background_color command without any pre-configured scope." - }, - { - "description": "Denies the set_badge_count command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-badge-count", - "markdownDescription": "Denies the set_badge_count command without any pre-configured scope." - }, - { - "description": "Denies the set_badge_label command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-badge-label", - "markdownDescription": "Denies the set_badge_label command without any pre-configured scope." - }, - { - "description": "Denies the set_closable command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-closable", - "markdownDescription": "Denies the set_closable command without any pre-configured scope." - }, - { - "description": "Denies the set_content_protected command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-content-protected", - "markdownDescription": "Denies the set_content_protected command without any pre-configured scope." - }, - { - "description": "Denies the set_cursor_grab command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-cursor-grab", - "markdownDescription": "Denies the set_cursor_grab command without any pre-configured scope." - }, - { - "description": "Denies the set_cursor_icon command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-cursor-icon", - "markdownDescription": "Denies the set_cursor_icon command without any pre-configured scope." - }, - { - "description": "Denies the set_cursor_position command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-cursor-position", - "markdownDescription": "Denies the set_cursor_position command without any pre-configured scope." - }, - { - "description": "Denies the set_cursor_visible command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-cursor-visible", - "markdownDescription": "Denies the set_cursor_visible command without any pre-configured scope." - }, - { - "description": "Denies the set_decorations command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-decorations", - "markdownDescription": "Denies the set_decorations command without any pre-configured scope." - }, - { - "description": "Denies the set_effects command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-effects", - "markdownDescription": "Denies the set_effects command without any pre-configured scope." - }, - { - "description": "Denies the set_enabled command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-enabled", - "markdownDescription": "Denies the set_enabled command without any pre-configured scope." - }, - { - "description": "Denies the set_focus command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-focus", - "markdownDescription": "Denies the set_focus command without any pre-configured scope." - }, - { - "description": "Denies the set_focusable command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-focusable", - "markdownDescription": "Denies the set_focusable command without any pre-configured scope." - }, - { - "description": "Denies the set_fullscreen command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-fullscreen", - "markdownDescription": "Denies the set_fullscreen command without any pre-configured scope." - }, - { - "description": "Denies the set_icon command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-icon", - "markdownDescription": "Denies the set_icon command without any pre-configured scope." - }, - { - "description": "Denies the set_ignore_cursor_events command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-ignore-cursor-events", - "markdownDescription": "Denies the set_ignore_cursor_events command without any pre-configured scope." - }, - { - "description": "Denies the set_max_size command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-max-size", - "markdownDescription": "Denies the set_max_size command without any pre-configured scope." - }, - { - "description": "Denies the set_maximizable command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-maximizable", - "markdownDescription": "Denies the set_maximizable command without any pre-configured scope." - }, - { - "description": "Denies the set_min_size command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-min-size", - "markdownDescription": "Denies the set_min_size command without any pre-configured scope." - }, - { - "description": "Denies the set_minimizable command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-minimizable", - "markdownDescription": "Denies the set_minimizable command without any pre-configured scope." - }, - { - "description": "Denies the set_overlay_icon command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-overlay-icon", - "markdownDescription": "Denies the set_overlay_icon command without any pre-configured scope." - }, - { - "description": "Denies the set_position command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-position", - "markdownDescription": "Denies the set_position command without any pre-configured scope." - }, - { - "description": "Denies the set_progress_bar command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-progress-bar", - "markdownDescription": "Denies the set_progress_bar command without any pre-configured scope." - }, - { - "description": "Denies the set_resizable command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-resizable", - "markdownDescription": "Denies the set_resizable command without any pre-configured scope." - }, - { - "description": "Denies the set_shadow command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-shadow", - "markdownDescription": "Denies the set_shadow command without any pre-configured scope." - }, - { - "description": "Denies the set_simple_fullscreen command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-simple-fullscreen", - "markdownDescription": "Denies the set_simple_fullscreen command without any pre-configured scope." - }, - { - "description": "Denies the set_size command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-size", - "markdownDescription": "Denies the set_size command without any pre-configured scope." - }, - { - "description": "Denies the set_size_constraints command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-size-constraints", - "markdownDescription": "Denies the set_size_constraints command without any pre-configured scope." - }, - { - "description": "Denies the set_skip_taskbar command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-skip-taskbar", - "markdownDescription": "Denies the set_skip_taskbar command without any pre-configured scope." - }, - { - "description": "Denies the set_theme command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-theme", - "markdownDescription": "Denies the set_theme command without any pre-configured scope." - }, - { - "description": "Denies the set_title command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-title", - "markdownDescription": "Denies the set_title command without any pre-configured scope." - }, - { - "description": "Denies the set_title_bar_style command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-title-bar-style", - "markdownDescription": "Denies the set_title_bar_style command without any pre-configured scope." - }, - { - "description": "Denies the set_visible_on_all_workspaces command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-visible-on-all-workspaces", - "markdownDescription": "Denies the set_visible_on_all_workspaces command without any pre-configured scope." - }, - { - "description": "Denies the show command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-show", - "markdownDescription": "Denies the show command without any pre-configured scope." - }, - { - "description": "Denies the start_dragging command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-start-dragging", - "markdownDescription": "Denies the start_dragging command without any pre-configured scope." - }, - { - "description": "Denies the start_resize_dragging command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-start-resize-dragging", - "markdownDescription": "Denies the start_resize_dragging command without any pre-configured scope." - }, - { - "description": "Denies the theme command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-theme", - "markdownDescription": "Denies the theme command without any pre-configured scope." - }, - { - "description": "Denies the title command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-title", - "markdownDescription": "Denies the title command without any pre-configured scope." - }, - { - "description": "Denies the toggle_maximize command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-toggle-maximize", - "markdownDescription": "Denies the toggle_maximize command without any pre-configured scope." - }, - { - "description": "Denies the unmaximize command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-unmaximize", - "markdownDescription": "Denies the unmaximize command without any pre-configured scope." - }, - { - "description": "Denies the unminimize command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-unminimize", - "markdownDescription": "Denies the unminimize command without any pre-configured scope." - }, - { - "description": "This permission set configures the types of dialogs\navailable from the dialog plugin.\n\n#### Granted Permissions\n\nAll dialog types are enabled.\n\n\n\n#### This default permission set includes:\n\n- `allow-message`\n- `allow-save`\n- `allow-open`", - "type": "string", - "const": "dialog:default", - "markdownDescription": "This permission set configures the types of dialogs\navailable from the dialog plugin.\n\n#### Granted Permissions\n\nAll dialog types are enabled.\n\n\n\n#### This default permission set includes:\n\n- `allow-message`\n- `allow-save`\n- `allow-open`" - }, - { - "description": "Enables the ask command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `allow-message` and will be removed in v3)", - "type": "string", - "const": "dialog:allow-ask", - "markdownDescription": "Enables the ask command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `allow-message` and will be removed in v3)" - }, - { - "description": "Enables the confirm command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `allow-message` and will be removed in v3)", - "type": "string", - "const": "dialog:allow-confirm", - "markdownDescription": "Enables the confirm command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `allow-message` and will be removed in v3)" - }, - { - "description": "Enables the message command without any pre-configured scope.", - "type": "string", - "const": "dialog:allow-message", - "markdownDescription": "Enables the message command without any pre-configured scope." - }, - { - "description": "Enables the open command without any pre-configured scope.", - "type": "string", - "const": "dialog:allow-open", - "markdownDescription": "Enables the open command without any pre-configured scope." - }, - { - "description": "Enables the save command without any pre-configured scope.", - "type": "string", - "const": "dialog:allow-save", - "markdownDescription": "Enables the save command without any pre-configured scope." - }, - { - "description": "Denies the ask command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `deny-message` and will be removed in v3)", - "type": "string", - "const": "dialog:deny-ask", - "markdownDescription": "Denies the ask command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `deny-message` and will be removed in v3)" - }, - { - "description": "Denies the confirm command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `deny-message` and will be removed in v3)", - "type": "string", - "const": "dialog:deny-confirm", - "markdownDescription": "Denies the confirm command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `deny-message` and will be removed in v3)" - }, - { - "description": "Denies the message command without any pre-configured scope.", - "type": "string", - "const": "dialog:deny-message", - "markdownDescription": "Denies the message command without any pre-configured scope." - }, - { - "description": "Denies the open command without any pre-configured scope.", - "type": "string", - "const": "dialog:deny-open", - "markdownDescription": "Denies the open command without any pre-configured scope." - }, - { - "description": "Denies the save command without any pre-configured scope.", - "type": "string", - "const": "dialog:deny-save", - "markdownDescription": "Denies the save command without any pre-configured scope." - } - ] - }, - "Value": { - "description": "All supported ACL values.", - "anyOf": [ - { - "description": "Represents a null JSON value.", - "type": "null" - }, - { - "description": "Represents a [`bool`].", - "type": "boolean" - }, - { - "description": "Represents a valid ACL [`Number`].", - "allOf": [ - { - "$ref": "#/definitions/Number" - } - ] - }, - { - "description": "Represents a [`String`].", - "type": "string" - }, - { - "description": "Represents a list of other [`Value`]s.", - "type": "array", - "items": { - "$ref": "#/definitions/Value" - } - }, - { - "description": "Represents a map of [`String`] keys to [`Value`]s.", - "type": "object", - "additionalProperties": { - "$ref": "#/definitions/Value" - } - } - ] - }, - "Number": { - "description": "A valid ACL number.", - "anyOf": [ - { - "description": "Represents an [`i64`].", - "type": "integer", - "format": "int64" - }, - { - "description": "Represents a [`f64`].", - "type": "number", - "format": "double" - } - ] - }, - "Target": { - "description": "Platform target.", - "oneOf": [ - { - "description": "MacOS.", - "type": "string", - "enum": [ - "macOS" - ] - }, - { - "description": "Windows.", - "type": "string", - "enum": [ - "windows" - ] - }, - { - "description": "Linux.", - "type": "string", - "enum": [ - "linux" - ] - }, - { - "description": "Android.", - "type": "string", - "enum": [ - "android" - ] - }, - { - "description": "iOS.", - "type": "string", - "enum": [ - "iOS" - ] - } - ] - } - } -} \ No newline at end of file diff --git a/apps/windows-client/src-tauri/gen/schemas/windows-schema.json b/apps/windows-client/src-tauri/gen/schemas/windows-schema.json deleted file mode 100644 index 24c9001..0000000 --- a/apps/windows-client/src-tauri/gen/schemas/windows-schema.json +++ /dev/null @@ -1,2358 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "CapabilityFile", - "description": "Capability formats accepted in a capability file.", - "anyOf": [ - { - "description": "A single capability.", - "allOf": [ - { - "$ref": "#/definitions/Capability" - } - ] - }, - { - "description": "A list of capabilities.", - "type": "array", - "items": { - "$ref": "#/definitions/Capability" - } - }, - { - "description": "A list of capabilities.", - "type": "object", - "required": [ - "capabilities" - ], - "properties": { - "capabilities": { - "description": "The list of capabilities.", - "type": "array", - "items": { - "$ref": "#/definitions/Capability" - } - } - } - } - ], - "definitions": { - "Capability": { - "description": "A grouping and boundary mechanism developers can use to isolate access to the IPC layer.\n\nIt controls application windows' and webviews' fine grained access to the Tauri core, application, or plugin commands. If a webview or its window is not matching any capability then it has no access to the IPC layer at all.\n\nThis can be done to create groups of windows, based on their required system access, which can reduce impact of frontend vulnerabilities in less privileged windows. Windows can be added to a capability by exact name (e.g. `main-window`) or glob patterns like `*` or `admin-*`. A Window can have none, one, or multiple associated capabilities.\n\n## Example\n\n```json { \"identifier\": \"main-user-files-write\", \"description\": \"This capability allows the `main` window on macOS and Windows access to `filesystem` write related commands and `dialog` commands to enable programmatic access to files selected by the user.\", \"windows\": [ \"main\" ], \"permissions\": [ \"core:default\", \"dialog:open\", { \"identifier\": \"fs:allow-write-text-file\", \"allow\": [{ \"path\": \"$HOME/test.txt\" }] }, ], \"platforms\": [\"macOS\",\"windows\"] } ```", - "type": "object", - "required": [ - "identifier", - "permissions" - ], - "properties": { - "identifier": { - "description": "Identifier of the capability.\n\n## Example\n\n`main-user-files-write`", - "type": "string" - }, - "description": { - "description": "Description of what the capability is intended to allow on associated windows.\n\nIt should contain a description of what the grouped permissions should allow.\n\n## Example\n\nThis capability allows the `main` window access to `filesystem` write related commands and `dialog` commands to enable programmatic access to files selected by the user.", - "default": "", - "type": "string" - }, - "remote": { - "description": "Configure remote URLs that can use the capability permissions.\n\nThis setting is optional and defaults to not being set, as our default use case is that the content is served from our local application.\n\n:::caution Make sure you understand the security implications of providing remote sources with local system access. :::\n\n## Example\n\n```json { \"urls\": [\"https://*.mydomain.dev\"] } ```", - "anyOf": [ - { - "$ref": "#/definitions/CapabilityRemote" - }, - { - "type": "null" - } - ] - }, - "local": { - "description": "Whether this capability is enabled for local app URLs or not. Defaults to `true`.", - "default": true, - "type": "boolean" - }, - "windows": { - "description": "List of windows that are affected by this capability. Can be a glob pattern.\n\nIf a window label matches any of the patterns in this list, the capability will be enabled on all the webviews of that window, regardless of the value of [`Self::webviews`].\n\nOn multiwebview windows, prefer specifying [`Self::webviews`] and omitting [`Self::windows`] for a fine grained access control.\n\n## Example\n\n`[\"main\"]`", - "type": "array", - "items": { - "type": "string" - } - }, - "webviews": { - "description": "List of webviews that are affected by this capability. Can be a glob pattern.\n\nThe capability will be enabled on all the webviews whose label matches any of the patterns in this list, regardless of whether the webview's window label matches a pattern in [`Self::windows`].\n\n## Example\n\n`[\"sub-webview-one\", \"sub-webview-two\"]`", - "type": "array", - "items": { - "type": "string" - } - }, - "permissions": { - "description": "List of permissions attached to this capability.\n\nMust include the plugin name as prefix in the form of `${plugin-name}:${permission-name}`. For commands directly implemented in the application itself only `${permission-name}` is required.\n\n## Example\n\n```json [ \"core:default\", \"shell:allow-open\", \"dialog:open\", { \"identifier\": \"fs:allow-write-text-file\", \"allow\": [{ \"path\": \"$HOME/test.txt\" }] } ] ```", - "type": "array", - "items": { - "$ref": "#/definitions/PermissionEntry" - }, - "uniqueItems": true - }, - "platforms": { - "description": "Limit which target platforms this capability applies to.\n\nBy default all platforms are targeted.\n\n## Example\n\n`[\"macOS\",\"windows\"]`", - "type": [ - "array", - "null" - ], - "items": { - "$ref": "#/definitions/Target" - } - } - } - }, - "CapabilityRemote": { - "description": "Configuration for remote URLs that are associated with the capability.", - "type": "object", - "required": [ - "urls" - ], - "properties": { - "urls": { - "description": "Remote domains this capability refers to using the [URLPattern standard](https://urlpattern.spec.whatwg.org/).\n\n## Examples\n\n- \"https://*.mydomain.dev\": allows subdomains of mydomain.dev - \"https://mydomain.dev/api/*\": allows any subpath of mydomain.dev/api", - "type": "array", - "items": { - "type": "string" - } - } - } - }, - "PermissionEntry": { - "description": "An entry for a permission value in a [`Capability`] can be either a raw permission [`Identifier`] or an object that references a permission and extends its scope.", - "anyOf": [ - { - "description": "Reference a permission or permission set by identifier.", - "allOf": [ - { - "$ref": "#/definitions/Identifier" - } - ] - }, - { - "description": "Reference a permission or permission set by identifier and extends its scope.", - "type": "object", - "allOf": [ - { - "properties": { - "identifier": { - "description": "Identifier of the permission or permission set.", - "allOf": [ - { - "$ref": "#/definitions/Identifier" - } - ] - }, - "allow": { - "description": "Data that defines what is allowed by the scope.", - "type": [ - "array", - "null" - ], - "items": { - "$ref": "#/definitions/Value" - } - }, - "deny": { - "description": "Data that defines what is denied by the scope. This should be prioritized by validation logic.", - "type": [ - "array", - "null" - ], - "items": { - "$ref": "#/definitions/Value" - } - } - } - } - ], - "required": [ - "identifier" - ] - } - ] - }, - "Identifier": { - "description": "Permission identifier", - "oneOf": [ - { - "description": "Default core plugins set.\n#### This default permission set includes:\n\n- `core:path:default`\n- `core:event:default`\n- `core:window:default`\n- `core:webview:default`\n- `core:app:default`\n- `core:image:default`\n- `core:resources:default`\n- `core:menu:default`\n- `core:tray:default`", - "type": "string", - "const": "core:default", - "markdownDescription": "Default core plugins set.\n#### This default permission set includes:\n\n- `core:path:default`\n- `core:event:default`\n- `core:window:default`\n- `core:webview:default`\n- `core:app:default`\n- `core:image:default`\n- `core:resources:default`\n- `core:menu:default`\n- `core:tray:default`" - }, - { - "description": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-version`\n- `allow-name`\n- `allow-tauri-version`\n- `allow-identifier`\n- `allow-bundle-type`\n- `allow-register-listener`\n- `allow-remove-listener`\n- `allow-supports-multiple-windows`", - "type": "string", - "const": "core:app:default", - "markdownDescription": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-version`\n- `allow-name`\n- `allow-tauri-version`\n- `allow-identifier`\n- `allow-bundle-type`\n- `allow-register-listener`\n- `allow-remove-listener`\n- `allow-supports-multiple-windows`" - }, - { - "description": "Enables the app_hide command without any pre-configured scope.", - "type": "string", - "const": "core:app:allow-app-hide", - "markdownDescription": "Enables the app_hide command without any pre-configured scope." - }, - { - "description": "Enables the app_show command without any pre-configured scope.", - "type": "string", - "const": "core:app:allow-app-show", - "markdownDescription": "Enables the app_show command without any pre-configured scope." - }, - { - "description": "Enables the bundle_type command without any pre-configured scope.", - "type": "string", - "const": "core:app:allow-bundle-type", - "markdownDescription": "Enables the bundle_type command without any pre-configured scope." - }, - { - "description": "Enables the default_window_icon command without any pre-configured scope.", - "type": "string", - "const": "core:app:allow-default-window-icon", - "markdownDescription": "Enables the default_window_icon command without any pre-configured scope." - }, - { - "description": "Enables the fetch_data_store_identifiers command without any pre-configured scope.", - "type": "string", - "const": "core:app:allow-fetch-data-store-identifiers", - "markdownDescription": "Enables the fetch_data_store_identifiers command without any pre-configured scope." - }, - { - "description": "Enables the identifier command without any pre-configured scope.", - "type": "string", - "const": "core:app:allow-identifier", - "markdownDescription": "Enables the identifier command without any pre-configured scope." - }, - { - "description": "Enables the name command without any pre-configured scope.", - "type": "string", - "const": "core:app:allow-name", - "markdownDescription": "Enables the name command without any pre-configured scope." - }, - { - "description": "Enables the register_listener command without any pre-configured scope.", - "type": "string", - "const": "core:app:allow-register-listener", - "markdownDescription": "Enables the register_listener command without any pre-configured scope." - }, - { - "description": "Enables the remove_data_store command without any pre-configured scope.", - "type": "string", - "const": "core:app:allow-remove-data-store", - "markdownDescription": "Enables the remove_data_store command without any pre-configured scope." - }, - { - "description": "Enables the remove_listener command without any pre-configured scope.", - "type": "string", - "const": "core:app:allow-remove-listener", - "markdownDescription": "Enables the remove_listener command without any pre-configured scope." - }, - { - "description": "Enables the set_app_theme command without any pre-configured scope.", - "type": "string", - "const": "core:app:allow-set-app-theme", - "markdownDescription": "Enables the set_app_theme command without any pre-configured scope." - }, - { - "description": "Enables the set_dock_visibility command without any pre-configured scope.", - "type": "string", - "const": "core:app:allow-set-dock-visibility", - "markdownDescription": "Enables the set_dock_visibility command without any pre-configured scope." - }, - { - "description": "Enables the supports_multiple_windows command without any pre-configured scope.", - "type": "string", - "const": "core:app:allow-supports-multiple-windows", - "markdownDescription": "Enables the supports_multiple_windows command without any pre-configured scope." - }, - { - "description": "Enables the tauri_version command without any pre-configured scope.", - "type": "string", - "const": "core:app:allow-tauri-version", - "markdownDescription": "Enables the tauri_version command without any pre-configured scope." - }, - { - "description": "Enables the version command without any pre-configured scope.", - "type": "string", - "const": "core:app:allow-version", - "markdownDescription": "Enables the version command without any pre-configured scope." - }, - { - "description": "Denies the app_hide command without any pre-configured scope.", - "type": "string", - "const": "core:app:deny-app-hide", - "markdownDescription": "Denies the app_hide command without any pre-configured scope." - }, - { - "description": "Denies the app_show command without any pre-configured scope.", - "type": "string", - "const": "core:app:deny-app-show", - "markdownDescription": "Denies the app_show command without any pre-configured scope." - }, - { - "description": "Denies the bundle_type command without any pre-configured scope.", - "type": "string", - "const": "core:app:deny-bundle-type", - "markdownDescription": "Denies the bundle_type command without any pre-configured scope." - }, - { - "description": "Denies the default_window_icon command without any pre-configured scope.", - "type": "string", - "const": "core:app:deny-default-window-icon", - "markdownDescription": "Denies the default_window_icon command without any pre-configured scope." - }, - { - "description": "Denies the fetch_data_store_identifiers command without any pre-configured scope.", - "type": "string", - "const": "core:app:deny-fetch-data-store-identifiers", - "markdownDescription": "Denies the fetch_data_store_identifiers command without any pre-configured scope." - }, - { - "description": "Denies the identifier command without any pre-configured scope.", - "type": "string", - "const": "core:app:deny-identifier", - "markdownDescription": "Denies the identifier command without any pre-configured scope." - }, - { - "description": "Denies the name command without any pre-configured scope.", - "type": "string", - "const": "core:app:deny-name", - "markdownDescription": "Denies the name command without any pre-configured scope." - }, - { - "description": "Denies the register_listener command without any pre-configured scope.", - "type": "string", - "const": "core:app:deny-register-listener", - "markdownDescription": "Denies the register_listener command without any pre-configured scope." - }, - { - "description": "Denies the remove_data_store command without any pre-configured scope.", - "type": "string", - "const": "core:app:deny-remove-data-store", - "markdownDescription": "Denies the remove_data_store command without any pre-configured scope." - }, - { - "description": "Denies the remove_listener command without any pre-configured scope.", - "type": "string", - "const": "core:app:deny-remove-listener", - "markdownDescription": "Denies the remove_listener command without any pre-configured scope." - }, - { - "description": "Denies the set_app_theme command without any pre-configured scope.", - "type": "string", - "const": "core:app:deny-set-app-theme", - "markdownDescription": "Denies the set_app_theme command without any pre-configured scope." - }, - { - "description": "Denies the set_dock_visibility command without any pre-configured scope.", - "type": "string", - "const": "core:app:deny-set-dock-visibility", - "markdownDescription": "Denies the set_dock_visibility command without any pre-configured scope." - }, - { - "description": "Denies the supports_multiple_windows command without any pre-configured scope.", - "type": "string", - "const": "core:app:deny-supports-multiple-windows", - "markdownDescription": "Denies the supports_multiple_windows command without any pre-configured scope." - }, - { - "description": "Denies the tauri_version command without any pre-configured scope.", - "type": "string", - "const": "core:app:deny-tauri-version", - "markdownDescription": "Denies the tauri_version command without any pre-configured scope." - }, - { - "description": "Denies the version command without any pre-configured scope.", - "type": "string", - "const": "core:app:deny-version", - "markdownDescription": "Denies the version command without any pre-configured scope." - }, - { - "description": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-listen`\n- `allow-unlisten`\n- `allow-emit`\n- `allow-emit-to`", - "type": "string", - "const": "core:event:default", - "markdownDescription": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-listen`\n- `allow-unlisten`\n- `allow-emit`\n- `allow-emit-to`" - }, - { - "description": "Enables the emit command without any pre-configured scope.", - "type": "string", - "const": "core:event:allow-emit", - "markdownDescription": "Enables the emit command without any pre-configured scope." - }, - { - "description": "Enables the emit_to command without any pre-configured scope.", - "type": "string", - "const": "core:event:allow-emit-to", - "markdownDescription": "Enables the emit_to command without any pre-configured scope." - }, - { - "description": "Enables the listen command without any pre-configured scope.", - "type": "string", - "const": "core:event:allow-listen", - "markdownDescription": "Enables the listen command without any pre-configured scope." - }, - { - "description": "Enables the unlisten command without any pre-configured scope.", - "type": "string", - "const": "core:event:allow-unlisten", - "markdownDescription": "Enables the unlisten command without any pre-configured scope." - }, - { - "description": "Denies the emit command without any pre-configured scope.", - "type": "string", - "const": "core:event:deny-emit", - "markdownDescription": "Denies the emit command without any pre-configured scope." - }, - { - "description": "Denies the emit_to command without any pre-configured scope.", - "type": "string", - "const": "core:event:deny-emit-to", - "markdownDescription": "Denies the emit_to command without any pre-configured scope." - }, - { - "description": "Denies the listen command without any pre-configured scope.", - "type": "string", - "const": "core:event:deny-listen", - "markdownDescription": "Denies the listen command without any pre-configured scope." - }, - { - "description": "Denies the unlisten command without any pre-configured scope.", - "type": "string", - "const": "core:event:deny-unlisten", - "markdownDescription": "Denies the unlisten command without any pre-configured scope." - }, - { - "description": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-new`\n- `allow-from-bytes`\n- `allow-from-path`\n- `allow-rgba`\n- `allow-size`", - "type": "string", - "const": "core:image:default", - "markdownDescription": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-new`\n- `allow-from-bytes`\n- `allow-from-path`\n- `allow-rgba`\n- `allow-size`" - }, - { - "description": "Enables the from_bytes command without any pre-configured scope.", - "type": "string", - "const": "core:image:allow-from-bytes", - "markdownDescription": "Enables the from_bytes command without any pre-configured scope." - }, - { - "description": "Enables the from_path command without any pre-configured scope.", - "type": "string", - "const": "core:image:allow-from-path", - "markdownDescription": "Enables the from_path command without any pre-configured scope." - }, - { - "description": "Enables the new command without any pre-configured scope.", - "type": "string", - "const": "core:image:allow-new", - "markdownDescription": "Enables the new command without any pre-configured scope." - }, - { - "description": "Enables the rgba command without any pre-configured scope.", - "type": "string", - "const": "core:image:allow-rgba", - "markdownDescription": "Enables the rgba command without any pre-configured scope." - }, - { - "description": "Enables the size command without any pre-configured scope.", - "type": "string", - "const": "core:image:allow-size", - "markdownDescription": "Enables the size command without any pre-configured scope." - }, - { - "description": "Denies the from_bytes command without any pre-configured scope.", - "type": "string", - "const": "core:image:deny-from-bytes", - "markdownDescription": "Denies the from_bytes command without any pre-configured scope." - }, - { - "description": "Denies the from_path command without any pre-configured scope.", - "type": "string", - "const": "core:image:deny-from-path", - "markdownDescription": "Denies the from_path command without any pre-configured scope." - }, - { - "description": "Denies the new command without any pre-configured scope.", - "type": "string", - "const": "core:image:deny-new", - "markdownDescription": "Denies the new command without any pre-configured scope." - }, - { - "description": "Denies the rgba command without any pre-configured scope.", - "type": "string", - "const": "core:image:deny-rgba", - "markdownDescription": "Denies the rgba command without any pre-configured scope." - }, - { - "description": "Denies the size command without any pre-configured scope.", - "type": "string", - "const": "core:image:deny-size", - "markdownDescription": "Denies the size command without any pre-configured scope." - }, - { - "description": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-new`\n- `allow-append`\n- `allow-prepend`\n- `allow-insert`\n- `allow-remove`\n- `allow-remove-at`\n- `allow-items`\n- `allow-get`\n- `allow-popup`\n- `allow-create-default`\n- `allow-set-as-app-menu`\n- `allow-set-as-window-menu`\n- `allow-text`\n- `allow-set-text`\n- `allow-is-enabled`\n- `allow-set-enabled`\n- `allow-set-accelerator`\n- `allow-set-as-windows-menu-for-nsapp`\n- `allow-set-as-help-menu-for-nsapp`\n- `allow-is-checked`\n- `allow-set-checked`\n- `allow-set-icon`", - "type": "string", - "const": "core:menu:default", - "markdownDescription": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-new`\n- `allow-append`\n- `allow-prepend`\n- `allow-insert`\n- `allow-remove`\n- `allow-remove-at`\n- `allow-items`\n- `allow-get`\n- `allow-popup`\n- `allow-create-default`\n- `allow-set-as-app-menu`\n- `allow-set-as-window-menu`\n- `allow-text`\n- `allow-set-text`\n- `allow-is-enabled`\n- `allow-set-enabled`\n- `allow-set-accelerator`\n- `allow-set-as-windows-menu-for-nsapp`\n- `allow-set-as-help-menu-for-nsapp`\n- `allow-is-checked`\n- `allow-set-checked`\n- `allow-set-icon`" - }, - { - "description": "Enables the append command without any pre-configured scope.", - "type": "string", - "const": "core:menu:allow-append", - "markdownDescription": "Enables the append command without any pre-configured scope." - }, - { - "description": "Enables the create_default command without any pre-configured scope.", - "type": "string", - "const": "core:menu:allow-create-default", - "markdownDescription": "Enables the create_default command without any pre-configured scope." - }, - { - "description": "Enables the get command without any pre-configured scope.", - "type": "string", - "const": "core:menu:allow-get", - "markdownDescription": "Enables the get command without any pre-configured scope." - }, - { - "description": "Enables the insert command without any pre-configured scope.", - "type": "string", - "const": "core:menu:allow-insert", - "markdownDescription": "Enables the insert command without any pre-configured scope." - }, - { - "description": "Enables the is_checked command without any pre-configured scope.", - "type": "string", - "const": "core:menu:allow-is-checked", - "markdownDescription": "Enables the is_checked command without any pre-configured scope." - }, - { - "description": "Enables the is_enabled command without any pre-configured scope.", - "type": "string", - "const": "core:menu:allow-is-enabled", - "markdownDescription": "Enables the is_enabled command without any pre-configured scope." - }, - { - "description": "Enables the items command without any pre-configured scope.", - "type": "string", - "const": "core:menu:allow-items", - "markdownDescription": "Enables the items command without any pre-configured scope." - }, - { - "description": "Enables the new command without any pre-configured scope.", - "type": "string", - "const": "core:menu:allow-new", - "markdownDescription": "Enables the new command without any pre-configured scope." - }, - { - "description": "Enables the popup command without any pre-configured scope.", - "type": "string", - "const": "core:menu:allow-popup", - "markdownDescription": "Enables the popup command without any pre-configured scope." - }, - { - "description": "Enables the prepend command without any pre-configured scope.", - "type": "string", - "const": "core:menu:allow-prepend", - "markdownDescription": "Enables the prepend command without any pre-configured scope." - }, - { - "description": "Enables the remove command without any pre-configured scope.", - "type": "string", - "const": "core:menu:allow-remove", - "markdownDescription": "Enables the remove command without any pre-configured scope." - }, - { - "description": "Enables the remove_at command without any pre-configured scope.", - "type": "string", - "const": "core:menu:allow-remove-at", - "markdownDescription": "Enables the remove_at command without any pre-configured scope." - }, - { - "description": "Enables the set_accelerator command without any pre-configured scope.", - "type": "string", - "const": "core:menu:allow-set-accelerator", - "markdownDescription": "Enables the set_accelerator command without any pre-configured scope." - }, - { - "description": "Enables the set_as_app_menu command without any pre-configured scope.", - "type": "string", - "const": "core:menu:allow-set-as-app-menu", - "markdownDescription": "Enables the set_as_app_menu command without any pre-configured scope." - }, - { - "description": "Enables the set_as_help_menu_for_nsapp command without any pre-configured scope.", - "type": "string", - "const": "core:menu:allow-set-as-help-menu-for-nsapp", - "markdownDescription": "Enables the set_as_help_menu_for_nsapp command without any pre-configured scope." - }, - { - "description": "Enables the set_as_window_menu command without any pre-configured scope.", - "type": "string", - "const": "core:menu:allow-set-as-window-menu", - "markdownDescription": "Enables the set_as_window_menu command without any pre-configured scope." - }, - { - "description": "Enables the set_as_windows_menu_for_nsapp command without any pre-configured scope.", - "type": "string", - "const": "core:menu:allow-set-as-windows-menu-for-nsapp", - "markdownDescription": "Enables the set_as_windows_menu_for_nsapp command without any pre-configured scope." - }, - { - "description": "Enables the set_checked command without any pre-configured scope.", - "type": "string", - "const": "core:menu:allow-set-checked", - "markdownDescription": "Enables the set_checked command without any pre-configured scope." - }, - { - "description": "Enables the set_enabled command without any pre-configured scope.", - "type": "string", - "const": "core:menu:allow-set-enabled", - "markdownDescription": "Enables the set_enabled command without any pre-configured scope." - }, - { - "description": "Enables the set_icon command without any pre-configured scope.", - "type": "string", - "const": "core:menu:allow-set-icon", - "markdownDescription": "Enables the set_icon command without any pre-configured scope." - }, - { - "description": "Enables the set_text command without any pre-configured scope.", - "type": "string", - "const": "core:menu:allow-set-text", - "markdownDescription": "Enables the set_text command without any pre-configured scope." - }, - { - "description": "Enables the text command without any pre-configured scope.", - "type": "string", - "const": "core:menu:allow-text", - "markdownDescription": "Enables the text command without any pre-configured scope." - }, - { - "description": "Denies the append command without any pre-configured scope.", - "type": "string", - "const": "core:menu:deny-append", - "markdownDescription": "Denies the append command without any pre-configured scope." - }, - { - "description": "Denies the create_default command without any pre-configured scope.", - "type": "string", - "const": "core:menu:deny-create-default", - "markdownDescription": "Denies the create_default command without any pre-configured scope." - }, - { - "description": "Denies the get command without any pre-configured scope.", - "type": "string", - "const": "core:menu:deny-get", - "markdownDescription": "Denies the get command without any pre-configured scope." - }, - { - "description": "Denies the insert command without any pre-configured scope.", - "type": "string", - "const": "core:menu:deny-insert", - "markdownDescription": "Denies the insert command without any pre-configured scope." - }, - { - "description": "Denies the is_checked command without any pre-configured scope.", - "type": "string", - "const": "core:menu:deny-is-checked", - "markdownDescription": "Denies the is_checked command without any pre-configured scope." - }, - { - "description": "Denies the is_enabled command without any pre-configured scope.", - "type": "string", - "const": "core:menu:deny-is-enabled", - "markdownDescription": "Denies the is_enabled command without any pre-configured scope." - }, - { - "description": "Denies the items command without any pre-configured scope.", - "type": "string", - "const": "core:menu:deny-items", - "markdownDescription": "Denies the items command without any pre-configured scope." - }, - { - "description": "Denies the new command without any pre-configured scope.", - "type": "string", - "const": "core:menu:deny-new", - "markdownDescription": "Denies the new command without any pre-configured scope." - }, - { - "description": "Denies the popup command without any pre-configured scope.", - "type": "string", - "const": "core:menu:deny-popup", - "markdownDescription": "Denies the popup command without any pre-configured scope." - }, - { - "description": "Denies the prepend command without any pre-configured scope.", - "type": "string", - "const": "core:menu:deny-prepend", - "markdownDescription": "Denies the prepend command without any pre-configured scope." - }, - { - "description": "Denies the remove command without any pre-configured scope.", - "type": "string", - "const": "core:menu:deny-remove", - "markdownDescription": "Denies the remove command without any pre-configured scope." - }, - { - "description": "Denies the remove_at command without any pre-configured scope.", - "type": "string", - "const": "core:menu:deny-remove-at", - "markdownDescription": "Denies the remove_at command without any pre-configured scope." - }, - { - "description": "Denies the set_accelerator command without any pre-configured scope.", - "type": "string", - "const": "core:menu:deny-set-accelerator", - "markdownDescription": "Denies the set_accelerator command without any pre-configured scope." - }, - { - "description": "Denies the set_as_app_menu command without any pre-configured scope.", - "type": "string", - "const": "core:menu:deny-set-as-app-menu", - "markdownDescription": "Denies the set_as_app_menu command without any pre-configured scope." - }, - { - "description": "Denies the set_as_help_menu_for_nsapp command without any pre-configured scope.", - "type": "string", - "const": "core:menu:deny-set-as-help-menu-for-nsapp", - "markdownDescription": "Denies the set_as_help_menu_for_nsapp command without any pre-configured scope." - }, - { - "description": "Denies the set_as_window_menu command without any pre-configured scope.", - "type": "string", - "const": "core:menu:deny-set-as-window-menu", - "markdownDescription": "Denies the set_as_window_menu command without any pre-configured scope." - }, - { - "description": "Denies the set_as_windows_menu_for_nsapp command without any pre-configured scope.", - "type": "string", - "const": "core:menu:deny-set-as-windows-menu-for-nsapp", - "markdownDescription": "Denies the set_as_windows_menu_for_nsapp command without any pre-configured scope." - }, - { - "description": "Denies the set_checked command without any pre-configured scope.", - "type": "string", - "const": "core:menu:deny-set-checked", - "markdownDescription": "Denies the set_checked command without any pre-configured scope." - }, - { - "description": "Denies the set_enabled command without any pre-configured scope.", - "type": "string", - "const": "core:menu:deny-set-enabled", - "markdownDescription": "Denies the set_enabled command without any pre-configured scope." - }, - { - "description": "Denies the set_icon command without any pre-configured scope.", - "type": "string", - "const": "core:menu:deny-set-icon", - "markdownDescription": "Denies the set_icon command without any pre-configured scope." - }, - { - "description": "Denies the set_text command without any pre-configured scope.", - "type": "string", - "const": "core:menu:deny-set-text", - "markdownDescription": "Denies the set_text command without any pre-configured scope." - }, - { - "description": "Denies the text command without any pre-configured scope.", - "type": "string", - "const": "core:menu:deny-text", - "markdownDescription": "Denies the text command without any pre-configured scope." - }, - { - "description": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-resolve-directory`\n- `allow-resolve`\n- `allow-normalize`\n- `allow-join`\n- `allow-dirname`\n- `allow-extname`\n- `allow-basename`\n- `allow-is-absolute`", - "type": "string", - "const": "core:path:default", - "markdownDescription": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-resolve-directory`\n- `allow-resolve`\n- `allow-normalize`\n- `allow-join`\n- `allow-dirname`\n- `allow-extname`\n- `allow-basename`\n- `allow-is-absolute`" - }, - { - "description": "Enables the basename command without any pre-configured scope.", - "type": "string", - "const": "core:path:allow-basename", - "markdownDescription": "Enables the basename command without any pre-configured scope." - }, - { - "description": "Enables the dirname command without any pre-configured scope.", - "type": "string", - "const": "core:path:allow-dirname", - "markdownDescription": "Enables the dirname command without any pre-configured scope." - }, - { - "description": "Enables the extname command without any pre-configured scope.", - "type": "string", - "const": "core:path:allow-extname", - "markdownDescription": "Enables the extname command without any pre-configured scope." - }, - { - "description": "Enables the is_absolute command without any pre-configured scope.", - "type": "string", - "const": "core:path:allow-is-absolute", - "markdownDescription": "Enables the is_absolute command without any pre-configured scope." - }, - { - "description": "Enables the join command without any pre-configured scope.", - "type": "string", - "const": "core:path:allow-join", - "markdownDescription": "Enables the join command without any pre-configured scope." - }, - { - "description": "Enables the normalize command without any pre-configured scope.", - "type": "string", - "const": "core:path:allow-normalize", - "markdownDescription": "Enables the normalize command without any pre-configured scope." - }, - { - "description": "Enables the resolve command without any pre-configured scope.", - "type": "string", - "const": "core:path:allow-resolve", - "markdownDescription": "Enables the resolve command without any pre-configured scope." - }, - { - "description": "Enables the resolve_directory command without any pre-configured scope.", - "type": "string", - "const": "core:path:allow-resolve-directory", - "markdownDescription": "Enables the resolve_directory command without any pre-configured scope." - }, - { - "description": "Denies the basename command without any pre-configured scope.", - "type": "string", - "const": "core:path:deny-basename", - "markdownDescription": "Denies the basename command without any pre-configured scope." - }, - { - "description": "Denies the dirname command without any pre-configured scope.", - "type": "string", - "const": "core:path:deny-dirname", - "markdownDescription": "Denies the dirname command without any pre-configured scope." - }, - { - "description": "Denies the extname command without any pre-configured scope.", - "type": "string", - "const": "core:path:deny-extname", - "markdownDescription": "Denies the extname command without any pre-configured scope." - }, - { - "description": "Denies the is_absolute command without any pre-configured scope.", - "type": "string", - "const": "core:path:deny-is-absolute", - "markdownDescription": "Denies the is_absolute command without any pre-configured scope." - }, - { - "description": "Denies the join command without any pre-configured scope.", - "type": "string", - "const": "core:path:deny-join", - "markdownDescription": "Denies the join command without any pre-configured scope." - }, - { - "description": "Denies the normalize command without any pre-configured scope.", - "type": "string", - "const": "core:path:deny-normalize", - "markdownDescription": "Denies the normalize command without any pre-configured scope." - }, - { - "description": "Denies the resolve command without any pre-configured scope.", - "type": "string", - "const": "core:path:deny-resolve", - "markdownDescription": "Denies the resolve command without any pre-configured scope." - }, - { - "description": "Denies the resolve_directory command without any pre-configured scope.", - "type": "string", - "const": "core:path:deny-resolve-directory", - "markdownDescription": "Denies the resolve_directory command without any pre-configured scope." - }, - { - "description": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-close`", - "type": "string", - "const": "core:resources:default", - "markdownDescription": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-close`" - }, - { - "description": "Enables the close command without any pre-configured scope.", - "type": "string", - "const": "core:resources:allow-close", - "markdownDescription": "Enables the close command without any pre-configured scope." - }, - { - "description": "Denies the close command without any pre-configured scope.", - "type": "string", - "const": "core:resources:deny-close", - "markdownDescription": "Denies the close command without any pre-configured scope." - }, - { - "description": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-new`\n- `allow-get-by-id`\n- `allow-remove-by-id`\n- `allow-set-icon`\n- `allow-set-menu`\n- `allow-set-tooltip`\n- `allow-set-title`\n- `allow-set-visible`\n- `allow-set-temp-dir-path`\n- `allow-set-icon-as-template`\n- `allow-set-icon-with-as-template`\n- `allow-set-show-menu-on-left-click`", - "type": "string", - "const": "core:tray:default", - "markdownDescription": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-new`\n- `allow-get-by-id`\n- `allow-remove-by-id`\n- `allow-set-icon`\n- `allow-set-menu`\n- `allow-set-tooltip`\n- `allow-set-title`\n- `allow-set-visible`\n- `allow-set-temp-dir-path`\n- `allow-set-icon-as-template`\n- `allow-set-icon-with-as-template`\n- `allow-set-show-menu-on-left-click`" - }, - { - "description": "Enables the get_by_id command without any pre-configured scope.", - "type": "string", - "const": "core:tray:allow-get-by-id", - "markdownDescription": "Enables the get_by_id command without any pre-configured scope." - }, - { - "description": "Enables the new command without any pre-configured scope.", - "type": "string", - "const": "core:tray:allow-new", - "markdownDescription": "Enables the new command without any pre-configured scope." - }, - { - "description": "Enables the remove_by_id command without any pre-configured scope.", - "type": "string", - "const": "core:tray:allow-remove-by-id", - "markdownDescription": "Enables the remove_by_id command without any pre-configured scope." - }, - { - "description": "Enables the set_icon command without any pre-configured scope.", - "type": "string", - "const": "core:tray:allow-set-icon", - "markdownDescription": "Enables the set_icon command without any pre-configured scope." - }, - { - "description": "Enables the set_icon_as_template command without any pre-configured scope.", - "type": "string", - "const": "core:tray:allow-set-icon-as-template", - "markdownDescription": "Enables the set_icon_as_template command without any pre-configured scope." - }, - { - "description": "Enables the set_icon_with_as_template command without any pre-configured scope.", - "type": "string", - "const": "core:tray:allow-set-icon-with-as-template", - "markdownDescription": "Enables the set_icon_with_as_template command without any pre-configured scope." - }, - { - "description": "Enables the set_menu command without any pre-configured scope.", - "type": "string", - "const": "core:tray:allow-set-menu", - "markdownDescription": "Enables the set_menu command without any pre-configured scope." - }, - { - "description": "Enables the set_show_menu_on_left_click command without any pre-configured scope.", - "type": "string", - "const": "core:tray:allow-set-show-menu-on-left-click", - "markdownDescription": "Enables the set_show_menu_on_left_click command without any pre-configured scope." - }, - { - "description": "Enables the set_temp_dir_path command without any pre-configured scope.", - "type": "string", - "const": "core:tray:allow-set-temp-dir-path", - "markdownDescription": "Enables the set_temp_dir_path command without any pre-configured scope." - }, - { - "description": "Enables the set_title command without any pre-configured scope.", - "type": "string", - "const": "core:tray:allow-set-title", - "markdownDescription": "Enables the set_title command without any pre-configured scope." - }, - { - "description": "Enables the set_tooltip command without any pre-configured scope.", - "type": "string", - "const": "core:tray:allow-set-tooltip", - "markdownDescription": "Enables the set_tooltip command without any pre-configured scope." - }, - { - "description": "Enables the set_visible command without any pre-configured scope.", - "type": "string", - "const": "core:tray:allow-set-visible", - "markdownDescription": "Enables the set_visible command without any pre-configured scope." - }, - { - "description": "Denies the get_by_id command without any pre-configured scope.", - "type": "string", - "const": "core:tray:deny-get-by-id", - "markdownDescription": "Denies the get_by_id command without any pre-configured scope." - }, - { - "description": "Denies the new command without any pre-configured scope.", - "type": "string", - "const": "core:tray:deny-new", - "markdownDescription": "Denies the new command without any pre-configured scope." - }, - { - "description": "Denies the remove_by_id command without any pre-configured scope.", - "type": "string", - "const": "core:tray:deny-remove-by-id", - "markdownDescription": "Denies the remove_by_id command without any pre-configured scope." - }, - { - "description": "Denies the set_icon command without any pre-configured scope.", - "type": "string", - "const": "core:tray:deny-set-icon", - "markdownDescription": "Denies the set_icon command without any pre-configured scope." - }, - { - "description": "Denies the set_icon_as_template command without any pre-configured scope.", - "type": "string", - "const": "core:tray:deny-set-icon-as-template", - "markdownDescription": "Denies the set_icon_as_template command without any pre-configured scope." - }, - { - "description": "Denies the set_icon_with_as_template command without any pre-configured scope.", - "type": "string", - "const": "core:tray:deny-set-icon-with-as-template", - "markdownDescription": "Denies the set_icon_with_as_template command without any pre-configured scope." - }, - { - "description": "Denies the set_menu command without any pre-configured scope.", - "type": "string", - "const": "core:tray:deny-set-menu", - "markdownDescription": "Denies the set_menu command without any pre-configured scope." - }, - { - "description": "Denies the set_show_menu_on_left_click command without any pre-configured scope.", - "type": "string", - "const": "core:tray:deny-set-show-menu-on-left-click", - "markdownDescription": "Denies the set_show_menu_on_left_click command without any pre-configured scope." - }, - { - "description": "Denies the set_temp_dir_path command without any pre-configured scope.", - "type": "string", - "const": "core:tray:deny-set-temp-dir-path", - "markdownDescription": "Denies the set_temp_dir_path command without any pre-configured scope." - }, - { - "description": "Denies the set_title command without any pre-configured scope.", - "type": "string", - "const": "core:tray:deny-set-title", - "markdownDescription": "Denies the set_title command without any pre-configured scope." - }, - { - "description": "Denies the set_tooltip command without any pre-configured scope.", - "type": "string", - "const": "core:tray:deny-set-tooltip", - "markdownDescription": "Denies the set_tooltip command without any pre-configured scope." - }, - { - "description": "Denies the set_visible command without any pre-configured scope.", - "type": "string", - "const": "core:tray:deny-set-visible", - "markdownDescription": "Denies the set_visible command without any pre-configured scope." - }, - { - "description": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-get-all-webviews`\n- `allow-webview-position`\n- `allow-webview-size`\n- `allow-internal-toggle-devtools`", - "type": "string", - "const": "core:webview:default", - "markdownDescription": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-get-all-webviews`\n- `allow-webview-position`\n- `allow-webview-size`\n- `allow-internal-toggle-devtools`" - }, - { - "description": "Enables the clear_all_browsing_data command without any pre-configured scope.", - "type": "string", - "const": "core:webview:allow-clear-all-browsing-data", - "markdownDescription": "Enables the clear_all_browsing_data command without any pre-configured scope." - }, - { - "description": "Enables the create_webview command without any pre-configured scope.", - "type": "string", - "const": "core:webview:allow-create-webview", - "markdownDescription": "Enables the create_webview command without any pre-configured scope." - }, - { - "description": "Enables the create_webview_window command without any pre-configured scope.", - "type": "string", - "const": "core:webview:allow-create-webview-window", - "markdownDescription": "Enables the create_webview_window command without any pre-configured scope." - }, - { - "description": "Enables the get_all_webviews command without any pre-configured scope.", - "type": "string", - "const": "core:webview:allow-get-all-webviews", - "markdownDescription": "Enables the get_all_webviews command without any pre-configured scope." - }, - { - "description": "Enables the internal_toggle_devtools command without any pre-configured scope.", - "type": "string", - "const": "core:webview:allow-internal-toggle-devtools", - "markdownDescription": "Enables the internal_toggle_devtools command without any pre-configured scope." - }, - { - "description": "Enables the print command without any pre-configured scope.", - "type": "string", - "const": "core:webview:allow-print", - "markdownDescription": "Enables the print command without any pre-configured scope." - }, - { - "description": "Enables the reparent command without any pre-configured scope.", - "type": "string", - "const": "core:webview:allow-reparent", - "markdownDescription": "Enables the reparent command without any pre-configured scope." - }, - { - "description": "Enables the set_webview_auto_resize command without any pre-configured scope.", - "type": "string", - "const": "core:webview:allow-set-webview-auto-resize", - "markdownDescription": "Enables the set_webview_auto_resize command without any pre-configured scope." - }, - { - "description": "Enables the set_webview_background_color command without any pre-configured scope.", - "type": "string", - "const": "core:webview:allow-set-webview-background-color", - "markdownDescription": "Enables the set_webview_background_color command without any pre-configured scope." - }, - { - "description": "Enables the set_webview_focus command without any pre-configured scope.", - "type": "string", - "const": "core:webview:allow-set-webview-focus", - "markdownDescription": "Enables the set_webview_focus command without any pre-configured scope." - }, - { - "description": "Enables the set_webview_position command without any pre-configured scope.", - "type": "string", - "const": "core:webview:allow-set-webview-position", - "markdownDescription": "Enables the set_webview_position command without any pre-configured scope." - }, - { - "description": "Enables the set_webview_size command without any pre-configured scope.", - "type": "string", - "const": "core:webview:allow-set-webview-size", - "markdownDescription": "Enables the set_webview_size command without any pre-configured scope." - }, - { - "description": "Enables the set_webview_zoom command without any pre-configured scope.", - "type": "string", - "const": "core:webview:allow-set-webview-zoom", - "markdownDescription": "Enables the set_webview_zoom command without any pre-configured scope." - }, - { - "description": "Enables the webview_close command without any pre-configured scope.", - "type": "string", - "const": "core:webview:allow-webview-close", - "markdownDescription": "Enables the webview_close command without any pre-configured scope." - }, - { - "description": "Enables the webview_hide command without any pre-configured scope.", - "type": "string", - "const": "core:webview:allow-webview-hide", - "markdownDescription": "Enables the webview_hide command without any pre-configured scope." - }, - { - "description": "Enables the webview_position command without any pre-configured scope.", - "type": "string", - "const": "core:webview:allow-webview-position", - "markdownDescription": "Enables the webview_position command without any pre-configured scope." - }, - { - "description": "Enables the webview_show command without any pre-configured scope.", - "type": "string", - "const": "core:webview:allow-webview-show", - "markdownDescription": "Enables the webview_show command without any pre-configured scope." - }, - { - "description": "Enables the webview_size command without any pre-configured scope.", - "type": "string", - "const": "core:webview:allow-webview-size", - "markdownDescription": "Enables the webview_size command without any pre-configured scope." - }, - { - "description": "Denies the clear_all_browsing_data command without any pre-configured scope.", - "type": "string", - "const": "core:webview:deny-clear-all-browsing-data", - "markdownDescription": "Denies the clear_all_browsing_data command without any pre-configured scope." - }, - { - "description": "Denies the create_webview command without any pre-configured scope.", - "type": "string", - "const": "core:webview:deny-create-webview", - "markdownDescription": "Denies the create_webview command without any pre-configured scope." - }, - { - "description": "Denies the create_webview_window command without any pre-configured scope.", - "type": "string", - "const": "core:webview:deny-create-webview-window", - "markdownDescription": "Denies the create_webview_window command without any pre-configured scope." - }, - { - "description": "Denies the get_all_webviews command without any pre-configured scope.", - "type": "string", - "const": "core:webview:deny-get-all-webviews", - "markdownDescription": "Denies the get_all_webviews command without any pre-configured scope." - }, - { - "description": "Denies the internal_toggle_devtools command without any pre-configured scope.", - "type": "string", - "const": "core:webview:deny-internal-toggle-devtools", - "markdownDescription": "Denies the internal_toggle_devtools command without any pre-configured scope." - }, - { - "description": "Denies the print command without any pre-configured scope.", - "type": "string", - "const": "core:webview:deny-print", - "markdownDescription": "Denies the print command without any pre-configured scope." - }, - { - "description": "Denies the reparent command without any pre-configured scope.", - "type": "string", - "const": "core:webview:deny-reparent", - "markdownDescription": "Denies the reparent command without any pre-configured scope." - }, - { - "description": "Denies the set_webview_auto_resize command without any pre-configured scope.", - "type": "string", - "const": "core:webview:deny-set-webview-auto-resize", - "markdownDescription": "Denies the set_webview_auto_resize command without any pre-configured scope." - }, - { - "description": "Denies the set_webview_background_color command without any pre-configured scope.", - "type": "string", - "const": "core:webview:deny-set-webview-background-color", - "markdownDescription": "Denies the set_webview_background_color command without any pre-configured scope." - }, - { - "description": "Denies the set_webview_focus command without any pre-configured scope.", - "type": "string", - "const": "core:webview:deny-set-webview-focus", - "markdownDescription": "Denies the set_webview_focus command without any pre-configured scope." - }, - { - "description": "Denies the set_webview_position command without any pre-configured scope.", - "type": "string", - "const": "core:webview:deny-set-webview-position", - "markdownDescription": "Denies the set_webview_position command without any pre-configured scope." - }, - { - "description": "Denies the set_webview_size command without any pre-configured scope.", - "type": "string", - "const": "core:webview:deny-set-webview-size", - "markdownDescription": "Denies the set_webview_size command without any pre-configured scope." - }, - { - "description": "Denies the set_webview_zoom command without any pre-configured scope.", - "type": "string", - "const": "core:webview:deny-set-webview-zoom", - "markdownDescription": "Denies the set_webview_zoom command without any pre-configured scope." - }, - { - "description": "Denies the webview_close command without any pre-configured scope.", - "type": "string", - "const": "core:webview:deny-webview-close", - "markdownDescription": "Denies the webview_close command without any pre-configured scope." - }, - { - "description": "Denies the webview_hide command without any pre-configured scope.", - "type": "string", - "const": "core:webview:deny-webview-hide", - "markdownDescription": "Denies the webview_hide command without any pre-configured scope." - }, - { - "description": "Denies the webview_position command without any pre-configured scope.", - "type": "string", - "const": "core:webview:deny-webview-position", - "markdownDescription": "Denies the webview_position command without any pre-configured scope." - }, - { - "description": "Denies the webview_show command without any pre-configured scope.", - "type": "string", - "const": "core:webview:deny-webview-show", - "markdownDescription": "Denies the webview_show command without any pre-configured scope." - }, - { - "description": "Denies the webview_size command without any pre-configured scope.", - "type": "string", - "const": "core:webview:deny-webview-size", - "markdownDescription": "Denies the webview_size command without any pre-configured scope." - }, - { - "description": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-get-all-windows`\n- `allow-scale-factor`\n- `allow-inner-position`\n- `allow-outer-position`\n- `allow-inner-size`\n- `allow-outer-size`\n- `allow-is-fullscreen`\n- `allow-is-minimized`\n- `allow-is-maximized`\n- `allow-is-focused`\n- `allow-is-decorated`\n- `allow-is-resizable`\n- `allow-is-maximizable`\n- `allow-is-minimizable`\n- `allow-is-closable`\n- `allow-is-visible`\n- `allow-is-enabled`\n- `allow-title`\n- `allow-current-monitor`\n- `allow-primary-monitor`\n- `allow-monitor-from-point`\n- `allow-available-monitors`\n- `allow-cursor-position`\n- `allow-theme`\n- `allow-is-always-on-top`\n- `allow-activity-name`\n- `allow-scene-identifier`\n- `allow-internal-toggle-maximize`", - "type": "string", - "const": "core:window:default", - "markdownDescription": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-get-all-windows`\n- `allow-scale-factor`\n- `allow-inner-position`\n- `allow-outer-position`\n- `allow-inner-size`\n- `allow-outer-size`\n- `allow-is-fullscreen`\n- `allow-is-minimized`\n- `allow-is-maximized`\n- `allow-is-focused`\n- `allow-is-decorated`\n- `allow-is-resizable`\n- `allow-is-maximizable`\n- `allow-is-minimizable`\n- `allow-is-closable`\n- `allow-is-visible`\n- `allow-is-enabled`\n- `allow-title`\n- `allow-current-monitor`\n- `allow-primary-monitor`\n- `allow-monitor-from-point`\n- `allow-available-monitors`\n- `allow-cursor-position`\n- `allow-theme`\n- `allow-is-always-on-top`\n- `allow-activity-name`\n- `allow-scene-identifier`\n- `allow-internal-toggle-maximize`" - }, - { - "description": "Enables the activity_name command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-activity-name", - "markdownDescription": "Enables the activity_name command without any pre-configured scope." - }, - { - "description": "Enables the available_monitors command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-available-monitors", - "markdownDescription": "Enables the available_monitors command without any pre-configured scope." - }, - { - "description": "Enables the center command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-center", - "markdownDescription": "Enables the center command without any pre-configured scope." - }, - { - "description": "Enables the close command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-close", - "markdownDescription": "Enables the close command without any pre-configured scope." - }, - { - "description": "Enables the create command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-create", - "markdownDescription": "Enables the create command without any pre-configured scope." - }, - { - "description": "Enables the current_monitor command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-current-monitor", - "markdownDescription": "Enables the current_monitor command without any pre-configured scope." - }, - { - "description": "Enables the cursor_position command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-cursor-position", - "markdownDescription": "Enables the cursor_position command without any pre-configured scope." - }, - { - "description": "Enables the destroy command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-destroy", - "markdownDescription": "Enables the destroy command without any pre-configured scope." - }, - { - "description": "Enables the get_all_windows command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-get-all-windows", - "markdownDescription": "Enables the get_all_windows command without any pre-configured scope." - }, - { - "description": "Enables the hide command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-hide", - "markdownDescription": "Enables the hide command without any pre-configured scope." - }, - { - "description": "Enables the inner_position command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-inner-position", - "markdownDescription": "Enables the inner_position command without any pre-configured scope." - }, - { - "description": "Enables the inner_size command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-inner-size", - "markdownDescription": "Enables the inner_size command without any pre-configured scope." - }, - { - "description": "Enables the internal_toggle_maximize command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-internal-toggle-maximize", - "markdownDescription": "Enables the internal_toggle_maximize command without any pre-configured scope." - }, - { - "description": "Enables the is_always_on_top command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-is-always-on-top", - "markdownDescription": "Enables the is_always_on_top command without any pre-configured scope." - }, - { - "description": "Enables the is_closable command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-is-closable", - "markdownDescription": "Enables the is_closable command without any pre-configured scope." - }, - { - "description": "Enables the is_decorated command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-is-decorated", - "markdownDescription": "Enables the is_decorated command without any pre-configured scope." - }, - { - "description": "Enables the is_enabled command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-is-enabled", - "markdownDescription": "Enables the is_enabled command without any pre-configured scope." - }, - { - "description": "Enables the is_focused command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-is-focused", - "markdownDescription": "Enables the is_focused command without any pre-configured scope." - }, - { - "description": "Enables the is_fullscreen command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-is-fullscreen", - "markdownDescription": "Enables the is_fullscreen command without any pre-configured scope." - }, - { - "description": "Enables the is_maximizable command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-is-maximizable", - "markdownDescription": "Enables the is_maximizable command without any pre-configured scope." - }, - { - "description": "Enables the is_maximized command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-is-maximized", - "markdownDescription": "Enables the is_maximized command without any pre-configured scope." - }, - { - "description": "Enables the is_minimizable command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-is-minimizable", - "markdownDescription": "Enables the is_minimizable command without any pre-configured scope." - }, - { - "description": "Enables the is_minimized command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-is-minimized", - "markdownDescription": "Enables the is_minimized command without any pre-configured scope." - }, - { - "description": "Enables the is_resizable command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-is-resizable", - "markdownDescription": "Enables the is_resizable command without any pre-configured scope." - }, - { - "description": "Enables the is_visible command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-is-visible", - "markdownDescription": "Enables the is_visible command without any pre-configured scope." - }, - { - "description": "Enables the maximize command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-maximize", - "markdownDescription": "Enables the maximize command without any pre-configured scope." - }, - { - "description": "Enables the minimize command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-minimize", - "markdownDescription": "Enables the minimize command without any pre-configured scope." - }, - { - "description": "Enables the monitor_from_point command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-monitor-from-point", - "markdownDescription": "Enables the monitor_from_point command without any pre-configured scope." - }, - { - "description": "Enables the outer_position command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-outer-position", - "markdownDescription": "Enables the outer_position command without any pre-configured scope." - }, - { - "description": "Enables the outer_size command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-outer-size", - "markdownDescription": "Enables the outer_size command without any pre-configured scope." - }, - { - "description": "Enables the primary_monitor command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-primary-monitor", - "markdownDescription": "Enables the primary_monitor command without any pre-configured scope." - }, - { - "description": "Enables the request_user_attention command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-request-user-attention", - "markdownDescription": "Enables the request_user_attention command without any pre-configured scope." - }, - { - "description": "Enables the scale_factor command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-scale-factor", - "markdownDescription": "Enables the scale_factor command without any pre-configured scope." - }, - { - "description": "Enables the scene_identifier command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-scene-identifier", - "markdownDescription": "Enables the scene_identifier command without any pre-configured scope." - }, - { - "description": "Enables the set_always_on_bottom command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-always-on-bottom", - "markdownDescription": "Enables the set_always_on_bottom command without any pre-configured scope." - }, - { - "description": "Enables the set_always_on_top command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-always-on-top", - "markdownDescription": "Enables the set_always_on_top command without any pre-configured scope." - }, - { - "description": "Enables the set_background_color command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-background-color", - "markdownDescription": "Enables the set_background_color command without any pre-configured scope." - }, - { - "description": "Enables the set_badge_count command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-badge-count", - "markdownDescription": "Enables the set_badge_count command without any pre-configured scope." - }, - { - "description": "Enables the set_badge_label command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-badge-label", - "markdownDescription": "Enables the set_badge_label command without any pre-configured scope." - }, - { - "description": "Enables the set_closable command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-closable", - "markdownDescription": "Enables the set_closable command without any pre-configured scope." - }, - { - "description": "Enables the set_content_protected command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-content-protected", - "markdownDescription": "Enables the set_content_protected command without any pre-configured scope." - }, - { - "description": "Enables the set_cursor_grab command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-cursor-grab", - "markdownDescription": "Enables the set_cursor_grab command without any pre-configured scope." - }, - { - "description": "Enables the set_cursor_icon command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-cursor-icon", - "markdownDescription": "Enables the set_cursor_icon command without any pre-configured scope." - }, - { - "description": "Enables the set_cursor_position command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-cursor-position", - "markdownDescription": "Enables the set_cursor_position command without any pre-configured scope." - }, - { - "description": "Enables the set_cursor_visible command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-cursor-visible", - "markdownDescription": "Enables the set_cursor_visible command without any pre-configured scope." - }, - { - "description": "Enables the set_decorations command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-decorations", - "markdownDescription": "Enables the set_decorations command without any pre-configured scope." - }, - { - "description": "Enables the set_effects command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-effects", - "markdownDescription": "Enables the set_effects command without any pre-configured scope." - }, - { - "description": "Enables the set_enabled command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-enabled", - "markdownDescription": "Enables the set_enabled command without any pre-configured scope." - }, - { - "description": "Enables the set_focus command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-focus", - "markdownDescription": "Enables the set_focus command without any pre-configured scope." - }, - { - "description": "Enables the set_focusable command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-focusable", - "markdownDescription": "Enables the set_focusable command without any pre-configured scope." - }, - { - "description": "Enables the set_fullscreen command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-fullscreen", - "markdownDescription": "Enables the set_fullscreen command without any pre-configured scope." - }, - { - "description": "Enables the set_icon command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-icon", - "markdownDescription": "Enables the set_icon command without any pre-configured scope." - }, - { - "description": "Enables the set_ignore_cursor_events command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-ignore-cursor-events", - "markdownDescription": "Enables the set_ignore_cursor_events command without any pre-configured scope." - }, - { - "description": "Enables the set_max_size command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-max-size", - "markdownDescription": "Enables the set_max_size command without any pre-configured scope." - }, - { - "description": "Enables the set_maximizable command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-maximizable", - "markdownDescription": "Enables the set_maximizable command without any pre-configured scope." - }, - { - "description": "Enables the set_min_size command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-min-size", - "markdownDescription": "Enables the set_min_size command without any pre-configured scope." - }, - { - "description": "Enables the set_minimizable command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-minimizable", - "markdownDescription": "Enables the set_minimizable command without any pre-configured scope." - }, - { - "description": "Enables the set_overlay_icon command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-overlay-icon", - "markdownDescription": "Enables the set_overlay_icon command without any pre-configured scope." - }, - { - "description": "Enables the set_position command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-position", - "markdownDescription": "Enables the set_position command without any pre-configured scope." - }, - { - "description": "Enables the set_progress_bar command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-progress-bar", - "markdownDescription": "Enables the set_progress_bar command without any pre-configured scope." - }, - { - "description": "Enables the set_resizable command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-resizable", - "markdownDescription": "Enables the set_resizable command without any pre-configured scope." - }, - { - "description": "Enables the set_shadow command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-shadow", - "markdownDescription": "Enables the set_shadow command without any pre-configured scope." - }, - { - "description": "Enables the set_simple_fullscreen command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-simple-fullscreen", - "markdownDescription": "Enables the set_simple_fullscreen command without any pre-configured scope." - }, - { - "description": "Enables the set_size command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-size", - "markdownDescription": "Enables the set_size command without any pre-configured scope." - }, - { - "description": "Enables the set_size_constraints command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-size-constraints", - "markdownDescription": "Enables the set_size_constraints command without any pre-configured scope." - }, - { - "description": "Enables the set_skip_taskbar command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-skip-taskbar", - "markdownDescription": "Enables the set_skip_taskbar command without any pre-configured scope." - }, - { - "description": "Enables the set_theme command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-theme", - "markdownDescription": "Enables the set_theme command without any pre-configured scope." - }, - { - "description": "Enables the set_title command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-title", - "markdownDescription": "Enables the set_title command without any pre-configured scope." - }, - { - "description": "Enables the set_title_bar_style command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-title-bar-style", - "markdownDescription": "Enables the set_title_bar_style command without any pre-configured scope." - }, - { - "description": "Enables the set_visible_on_all_workspaces command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-visible-on-all-workspaces", - "markdownDescription": "Enables the set_visible_on_all_workspaces command without any pre-configured scope." - }, - { - "description": "Enables the show command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-show", - "markdownDescription": "Enables the show command without any pre-configured scope." - }, - { - "description": "Enables the start_dragging command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-start-dragging", - "markdownDescription": "Enables the start_dragging command without any pre-configured scope." - }, - { - "description": "Enables the start_resize_dragging command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-start-resize-dragging", - "markdownDescription": "Enables the start_resize_dragging command without any pre-configured scope." - }, - { - "description": "Enables the theme command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-theme", - "markdownDescription": "Enables the theme command without any pre-configured scope." - }, - { - "description": "Enables the title command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-title", - "markdownDescription": "Enables the title command without any pre-configured scope." - }, - { - "description": "Enables the toggle_maximize command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-toggle-maximize", - "markdownDescription": "Enables the toggle_maximize command without any pre-configured scope." - }, - { - "description": "Enables the unmaximize command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-unmaximize", - "markdownDescription": "Enables the unmaximize command without any pre-configured scope." - }, - { - "description": "Enables the unminimize command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-unminimize", - "markdownDescription": "Enables the unminimize command without any pre-configured scope." - }, - { - "description": "Denies the activity_name command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-activity-name", - "markdownDescription": "Denies the activity_name command without any pre-configured scope." - }, - { - "description": "Denies the available_monitors command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-available-monitors", - "markdownDescription": "Denies the available_monitors command without any pre-configured scope." - }, - { - "description": "Denies the center command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-center", - "markdownDescription": "Denies the center command without any pre-configured scope." - }, - { - "description": "Denies the close command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-close", - "markdownDescription": "Denies the close command without any pre-configured scope." - }, - { - "description": "Denies the create command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-create", - "markdownDescription": "Denies the create command without any pre-configured scope." - }, - { - "description": "Denies the current_monitor command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-current-monitor", - "markdownDescription": "Denies the current_monitor command without any pre-configured scope." - }, - { - "description": "Denies the cursor_position command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-cursor-position", - "markdownDescription": "Denies the cursor_position command without any pre-configured scope." - }, - { - "description": "Denies the destroy command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-destroy", - "markdownDescription": "Denies the destroy command without any pre-configured scope." - }, - { - "description": "Denies the get_all_windows command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-get-all-windows", - "markdownDescription": "Denies the get_all_windows command without any pre-configured scope." - }, - { - "description": "Denies the hide command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-hide", - "markdownDescription": "Denies the hide command without any pre-configured scope." - }, - { - "description": "Denies the inner_position command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-inner-position", - "markdownDescription": "Denies the inner_position command without any pre-configured scope." - }, - { - "description": "Denies the inner_size command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-inner-size", - "markdownDescription": "Denies the inner_size command without any pre-configured scope." - }, - { - "description": "Denies the internal_toggle_maximize command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-internal-toggle-maximize", - "markdownDescription": "Denies the internal_toggle_maximize command without any pre-configured scope." - }, - { - "description": "Denies the is_always_on_top command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-is-always-on-top", - "markdownDescription": "Denies the is_always_on_top command without any pre-configured scope." - }, - { - "description": "Denies the is_closable command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-is-closable", - "markdownDescription": "Denies the is_closable command without any pre-configured scope." - }, - { - "description": "Denies the is_decorated command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-is-decorated", - "markdownDescription": "Denies the is_decorated command without any pre-configured scope." - }, - { - "description": "Denies the is_enabled command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-is-enabled", - "markdownDescription": "Denies the is_enabled command without any pre-configured scope." - }, - { - "description": "Denies the is_focused command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-is-focused", - "markdownDescription": "Denies the is_focused command without any pre-configured scope." - }, - { - "description": "Denies the is_fullscreen command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-is-fullscreen", - "markdownDescription": "Denies the is_fullscreen command without any pre-configured scope." - }, - { - "description": "Denies the is_maximizable command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-is-maximizable", - "markdownDescription": "Denies the is_maximizable command without any pre-configured scope." - }, - { - "description": "Denies the is_maximized command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-is-maximized", - "markdownDescription": "Denies the is_maximized command without any pre-configured scope." - }, - { - "description": "Denies the is_minimizable command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-is-minimizable", - "markdownDescription": "Denies the is_minimizable command without any pre-configured scope." - }, - { - "description": "Denies the is_minimized command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-is-minimized", - "markdownDescription": "Denies the is_minimized command without any pre-configured scope." - }, - { - "description": "Denies the is_resizable command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-is-resizable", - "markdownDescription": "Denies the is_resizable command without any pre-configured scope." - }, - { - "description": "Denies the is_visible command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-is-visible", - "markdownDescription": "Denies the is_visible command without any pre-configured scope." - }, - { - "description": "Denies the maximize command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-maximize", - "markdownDescription": "Denies the maximize command without any pre-configured scope." - }, - { - "description": "Denies the minimize command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-minimize", - "markdownDescription": "Denies the minimize command without any pre-configured scope." - }, - { - "description": "Denies the monitor_from_point command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-monitor-from-point", - "markdownDescription": "Denies the monitor_from_point command without any pre-configured scope." - }, - { - "description": "Denies the outer_position command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-outer-position", - "markdownDescription": "Denies the outer_position command without any pre-configured scope." - }, - { - "description": "Denies the outer_size command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-outer-size", - "markdownDescription": "Denies the outer_size command without any pre-configured scope." - }, - { - "description": "Denies the primary_monitor command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-primary-monitor", - "markdownDescription": "Denies the primary_monitor command without any pre-configured scope." - }, - { - "description": "Denies the request_user_attention command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-request-user-attention", - "markdownDescription": "Denies the request_user_attention command without any pre-configured scope." - }, - { - "description": "Denies the scale_factor command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-scale-factor", - "markdownDescription": "Denies the scale_factor command without any pre-configured scope." - }, - { - "description": "Denies the scene_identifier command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-scene-identifier", - "markdownDescription": "Denies the scene_identifier command without any pre-configured scope." - }, - { - "description": "Denies the set_always_on_bottom command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-always-on-bottom", - "markdownDescription": "Denies the set_always_on_bottom command without any pre-configured scope." - }, - { - "description": "Denies the set_always_on_top command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-always-on-top", - "markdownDescription": "Denies the set_always_on_top command without any pre-configured scope." - }, - { - "description": "Denies the set_background_color command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-background-color", - "markdownDescription": "Denies the set_background_color command without any pre-configured scope." - }, - { - "description": "Denies the set_badge_count command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-badge-count", - "markdownDescription": "Denies the set_badge_count command without any pre-configured scope." - }, - { - "description": "Denies the set_badge_label command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-badge-label", - "markdownDescription": "Denies the set_badge_label command without any pre-configured scope." - }, - { - "description": "Denies the set_closable command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-closable", - "markdownDescription": "Denies the set_closable command without any pre-configured scope." - }, - { - "description": "Denies the set_content_protected command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-content-protected", - "markdownDescription": "Denies the set_content_protected command without any pre-configured scope." - }, - { - "description": "Denies the set_cursor_grab command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-cursor-grab", - "markdownDescription": "Denies the set_cursor_grab command without any pre-configured scope." - }, - { - "description": "Denies the set_cursor_icon command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-cursor-icon", - "markdownDescription": "Denies the set_cursor_icon command without any pre-configured scope." - }, - { - "description": "Denies the set_cursor_position command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-cursor-position", - "markdownDescription": "Denies the set_cursor_position command without any pre-configured scope." - }, - { - "description": "Denies the set_cursor_visible command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-cursor-visible", - "markdownDescription": "Denies the set_cursor_visible command without any pre-configured scope." - }, - { - "description": "Denies the set_decorations command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-decorations", - "markdownDescription": "Denies the set_decorations command without any pre-configured scope." - }, - { - "description": "Denies the set_effects command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-effects", - "markdownDescription": "Denies the set_effects command without any pre-configured scope." - }, - { - "description": "Denies the set_enabled command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-enabled", - "markdownDescription": "Denies the set_enabled command without any pre-configured scope." - }, - { - "description": "Denies the set_focus command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-focus", - "markdownDescription": "Denies the set_focus command without any pre-configured scope." - }, - { - "description": "Denies the set_focusable command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-focusable", - "markdownDescription": "Denies the set_focusable command without any pre-configured scope." - }, - { - "description": "Denies the set_fullscreen command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-fullscreen", - "markdownDescription": "Denies the set_fullscreen command without any pre-configured scope." - }, - { - "description": "Denies the set_icon command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-icon", - "markdownDescription": "Denies the set_icon command without any pre-configured scope." - }, - { - "description": "Denies the set_ignore_cursor_events command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-ignore-cursor-events", - "markdownDescription": "Denies the set_ignore_cursor_events command without any pre-configured scope." - }, - { - "description": "Denies the set_max_size command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-max-size", - "markdownDescription": "Denies the set_max_size command without any pre-configured scope." - }, - { - "description": "Denies the set_maximizable command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-maximizable", - "markdownDescription": "Denies the set_maximizable command without any pre-configured scope." - }, - { - "description": "Denies the set_min_size command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-min-size", - "markdownDescription": "Denies the set_min_size command without any pre-configured scope." - }, - { - "description": "Denies the set_minimizable command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-minimizable", - "markdownDescription": "Denies the set_minimizable command without any pre-configured scope." - }, - { - "description": "Denies the set_overlay_icon command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-overlay-icon", - "markdownDescription": "Denies the set_overlay_icon command without any pre-configured scope." - }, - { - "description": "Denies the set_position command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-position", - "markdownDescription": "Denies the set_position command without any pre-configured scope." - }, - { - "description": "Denies the set_progress_bar command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-progress-bar", - "markdownDescription": "Denies the set_progress_bar command without any pre-configured scope." - }, - { - "description": "Denies the set_resizable command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-resizable", - "markdownDescription": "Denies the set_resizable command without any pre-configured scope." - }, - { - "description": "Denies the set_shadow command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-shadow", - "markdownDescription": "Denies the set_shadow command without any pre-configured scope." - }, - { - "description": "Denies the set_simple_fullscreen command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-simple-fullscreen", - "markdownDescription": "Denies the set_simple_fullscreen command without any pre-configured scope." - }, - { - "description": "Denies the set_size command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-size", - "markdownDescription": "Denies the set_size command without any pre-configured scope." - }, - { - "description": "Denies the set_size_constraints command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-size-constraints", - "markdownDescription": "Denies the set_size_constraints command without any pre-configured scope." - }, - { - "description": "Denies the set_skip_taskbar command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-skip-taskbar", - "markdownDescription": "Denies the set_skip_taskbar command without any pre-configured scope." - }, - { - "description": "Denies the set_theme command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-theme", - "markdownDescription": "Denies the set_theme command without any pre-configured scope." - }, - { - "description": "Denies the set_title command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-title", - "markdownDescription": "Denies the set_title command without any pre-configured scope." - }, - { - "description": "Denies the set_title_bar_style command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-title-bar-style", - "markdownDescription": "Denies the set_title_bar_style command without any pre-configured scope." - }, - { - "description": "Denies the set_visible_on_all_workspaces command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-visible-on-all-workspaces", - "markdownDescription": "Denies the set_visible_on_all_workspaces command without any pre-configured scope." - }, - { - "description": "Denies the show command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-show", - "markdownDescription": "Denies the show command without any pre-configured scope." - }, - { - "description": "Denies the start_dragging command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-start-dragging", - "markdownDescription": "Denies the start_dragging command without any pre-configured scope." - }, - { - "description": "Denies the start_resize_dragging command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-start-resize-dragging", - "markdownDescription": "Denies the start_resize_dragging command without any pre-configured scope." - }, - { - "description": "Denies the theme command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-theme", - "markdownDescription": "Denies the theme command without any pre-configured scope." - }, - { - "description": "Denies the title command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-title", - "markdownDescription": "Denies the title command without any pre-configured scope." - }, - { - "description": "Denies the toggle_maximize command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-toggle-maximize", - "markdownDescription": "Denies the toggle_maximize command without any pre-configured scope." - }, - { - "description": "Denies the unmaximize command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-unmaximize", - "markdownDescription": "Denies the unmaximize command without any pre-configured scope." - }, - { - "description": "Denies the unminimize command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-unminimize", - "markdownDescription": "Denies the unminimize command without any pre-configured scope." - }, - { - "description": "This permission set configures the types of dialogs\navailable from the dialog plugin.\n\n#### Granted Permissions\n\nAll dialog types are enabled.\n\n\n\n#### This default permission set includes:\n\n- `allow-message`\n- `allow-save`\n- `allow-open`", - "type": "string", - "const": "dialog:default", - "markdownDescription": "This permission set configures the types of dialogs\navailable from the dialog plugin.\n\n#### Granted Permissions\n\nAll dialog types are enabled.\n\n\n\n#### This default permission set includes:\n\n- `allow-message`\n- `allow-save`\n- `allow-open`" - }, - { - "description": "Enables the ask command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `allow-message` and will be removed in v3)", - "type": "string", - "const": "dialog:allow-ask", - "markdownDescription": "Enables the ask command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `allow-message` and will be removed in v3)" - }, - { - "description": "Enables the confirm command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `allow-message` and will be removed in v3)", - "type": "string", - "const": "dialog:allow-confirm", - "markdownDescription": "Enables the confirm command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `allow-message` and will be removed in v3)" - }, - { - "description": "Enables the message command without any pre-configured scope.", - "type": "string", - "const": "dialog:allow-message", - "markdownDescription": "Enables the message command without any pre-configured scope." - }, - { - "description": "Enables the open command without any pre-configured scope.", - "type": "string", - "const": "dialog:allow-open", - "markdownDescription": "Enables the open command without any pre-configured scope." - }, - { - "description": "Enables the save command without any pre-configured scope.", - "type": "string", - "const": "dialog:allow-save", - "markdownDescription": "Enables the save command without any pre-configured scope." - }, - { - "description": "Denies the ask command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `deny-message` and will be removed in v3)", - "type": "string", - "const": "dialog:deny-ask", - "markdownDescription": "Denies the ask command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `deny-message` and will be removed in v3)" - }, - { - "description": "Denies the confirm command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `deny-message` and will be removed in v3)", - "type": "string", - "const": "dialog:deny-confirm", - "markdownDescription": "Denies the confirm command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `deny-message` and will be removed in v3)" - }, - { - "description": "Denies the message command without any pre-configured scope.", - "type": "string", - "const": "dialog:deny-message", - "markdownDescription": "Denies the message command without any pre-configured scope." - }, - { - "description": "Denies the open command without any pre-configured scope.", - "type": "string", - "const": "dialog:deny-open", - "markdownDescription": "Denies the open command without any pre-configured scope." - }, - { - "description": "Denies the save command without any pre-configured scope.", - "type": "string", - "const": "dialog:deny-save", - "markdownDescription": "Denies the save command without any pre-configured scope." - } - ] - }, - "Value": { - "description": "All supported ACL values.", - "anyOf": [ - { - "description": "Represents a null JSON value.", - "type": "null" - }, - { - "description": "Represents a [`bool`].", - "type": "boolean" - }, - { - "description": "Represents a valid ACL [`Number`].", - "allOf": [ - { - "$ref": "#/definitions/Number" - } - ] - }, - { - "description": "Represents a [`String`].", - "type": "string" - }, - { - "description": "Represents a list of other [`Value`]s.", - "type": "array", - "items": { - "$ref": "#/definitions/Value" - } - }, - { - "description": "Represents a map of [`String`] keys to [`Value`]s.", - "type": "object", - "additionalProperties": { - "$ref": "#/definitions/Value" - } - } - ] - }, - "Number": { - "description": "A valid ACL number.", - "anyOf": [ - { - "description": "Represents an [`i64`].", - "type": "integer", - "format": "int64" - }, - { - "description": "Represents a [`f64`].", - "type": "number", - "format": "double" - } - ] - }, - "Target": { - "description": "Platform target.", - "oneOf": [ - { - "description": "MacOS.", - "type": "string", - "enum": [ - "macOS" - ] - }, - { - "description": "Windows.", - "type": "string", - "enum": [ - "windows" - ] - }, - { - "description": "Linux.", - "type": "string", - "enum": [ - "linux" - ] - }, - { - "description": "Android.", - "type": "string", - "enum": [ - "android" - ] - }, - { - "description": "iOS.", - "type": "string", - "enum": [ - "iOS" - ] - } - ] - } - } -} \ No newline at end of file diff --git a/apps/windows-client/src-tauri/icons/128x128.png b/apps/windows-client/src-tauri/icons/128x128.png deleted file mode 100644 index a565b11..0000000 Binary files a/apps/windows-client/src-tauri/icons/128x128.png and /dev/null differ diff --git a/apps/windows-client/src-tauri/icons/128x128@2x.png b/apps/windows-client/src-tauri/icons/128x128@2x.png deleted file mode 100644 index 678a804..0000000 Binary files a/apps/windows-client/src-tauri/icons/128x128@2x.png and /dev/null differ diff --git a/apps/windows-client/src-tauri/icons/32x32.png b/apps/windows-client/src-tauri/icons/32x32.png deleted file mode 100644 index 87ba0c5..0000000 Binary files a/apps/windows-client/src-tauri/icons/32x32.png and /dev/null differ diff --git a/apps/windows-client/src-tauri/icons/icon.ico b/apps/windows-client/src-tauri/icons/icon.ico deleted file mode 100644 index 6f1424f..0000000 Binary files a/apps/windows-client/src-tauri/icons/icon.ico and /dev/null differ diff --git a/apps/windows-client/src-tauri/src/activity.rs b/apps/windows-client/src-tauri/src/activity.rs deleted file mode 100644 index 6c8cc28..0000000 --- a/apps/windows-client/src-tauri/src/activity.rs +++ /dev/null @@ -1,23 +0,0 @@ -use crate::models::ActivityEntry; - -pub const DEFAULT_ACTIVITY_LIMIT: usize = 200; - -pub fn sort_activity_desc(mut entries: Vec) -> Vec { - entries.sort_by(|left, right| right.at.cmp(&left.at).then_with(|| right.id.cmp(&left.id))); - entries -} - -pub fn cap_activity(entries: Vec, limit: usize) -> Vec { - let mut entries = sort_activity_desc(entries); - entries.truncate(limit); - entries -} - -pub fn append_activity( - mut entries: Vec, - entry: ActivityEntry, - limit: usize, -) -> Vec { - entries.push(entry); - cap_activity(entries, limit) -} diff --git a/apps/windows-client/src-tauri/src/adapters/proxifyre.rs b/apps/windows-client/src-tauri/src/adapters/proxifyre.rs deleted file mode 100644 index 2839bbc..0000000 --- a/apps/windows-client/src-tauri/src/adapters/proxifyre.rs +++ /dev/null @@ -1,245 +0,0 @@ -#[cfg(not(test))] -use crate::adapters::proxy_router::{ - ProxyRouterAdapter, ProxyRouterError, ProxyRouterErrorKind, ProxyRouterGeneratedConfig, - ProxyRouterRequest, -}; -use crate::models::{ - ComponentId, ComponentState, ComponentStatus, Profile, ProfileItemType, Protocol, - ProxyProtocol, Target, -}; -#[cfg(test)] -use crate::proxy_router::{ - ProxyRouterAdapter, ProxyRouterError, ProxyRouterErrorKind, ProxyRouterGeneratedConfig, - ProxyRouterRequest, -}; -use serde::{Deserialize, Serialize}; - -pub const PROXIFYRE_ADAPTER_ID: &str = "proxifyre"; -pub const PROXIFYRE_OUTPUT_FILE: &str = "proxifyre-app-config.json"; - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct ProxiFyreAdapter { - log_level: String, - bypass_lan: bool, -} - -impl ProxiFyreAdapter { - pub fn new(log_level: impl Into, bypass_lan: bool) -> Self { - Self { - log_level: log_level.into(), - bypass_lan, - } - } - - pub fn generate_proxifyre_config( - &self, - request: ProxyRouterRequest<'_>, - ) -> Result { - let mut proxies = Vec::new(); - - for profile in request.profiles.iter().filter(|profile| profile.enabled) { - let target = find_target(profile, request.targets)?; - ensure_target_supported(profile, target, request.components)?; - - let app_names = app_names_for_profile(profile); - if app_names.is_empty() { - return Err(ProxyRouterError::new( - ProxyRouterErrorKind::EmptyProfileItems, - format!( - "В профиле '{}' нет приложений для маршрутизации", - profile.id - ), - )); - } - - proxies.push(ProxiFyreProxy { - app_names, - socks5_proxy_endpoint: format!("{}:{}", target.host, target.port), - supported_protocols: protocols_for_profile(profile), - }); - } - - Ok(ProxiFyreConfig { - log_level: self.log_level.clone(), - bypass_lan: self.bypass_lan, - proxies, - }) - } -} - -impl Default for ProxiFyreAdapter { - fn default() -> Self { - Self::new("Info", true) - } -} - -impl ProxyRouterAdapter for ProxiFyreAdapter { - fn id(&self) -> &'static str { - PROXIFYRE_ADAPTER_ID - } - - fn output_file_name(&self) -> &'static str { - PROXIFYRE_OUTPUT_FILE - } - - fn generate_config( - &self, - request: ProxyRouterRequest<'_>, - ) -> Result { - let config = self.generate_proxifyre_config(request)?; - let enabled_profiles = config.proxies.len(); - let routed_apps = config - .proxies - .iter() - .map(|proxy| proxy.app_names.len()) - .sum(); - let contents = serde_json::to_string_pretty(&config).map_err(|error| { - ProxyRouterError::new( - ProxyRouterErrorKind::Serialization, - format!("Не удалось сериализовать конфиг ProxiFyre: {error}"), - ) - })?; - - Ok(ProxyRouterGeneratedConfig { - adapter_id: self.id().to_string(), - output_file_name: self.output_file_name().to_string(), - contents, - enabled_profiles, - routed_apps, - }) - } -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct ProxiFyreConfig { - #[serde(rename = "logLevel")] - pub log_level: String, - #[serde(rename = "bypassLan")] - pub bypass_lan: bool, - pub proxies: Vec, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct ProxiFyreProxy { - #[serde(rename = "appNames")] - pub app_names: Vec, - #[serde(rename = "socks5ProxyEndpoint")] - pub socks5_proxy_endpoint: String, - #[serde(rename = "supportedProtocols")] - pub supported_protocols: Vec, -} - -fn find_target<'a>( - profile: &Profile, - targets: &'a [Target], -) -> Result<&'a Target, ProxyRouterError> { - targets - .iter() - .find(|target| target.id == profile.target_id) - .ok_or_else(|| { - ProxyRouterError::new( - ProxyRouterErrorKind::MissingTarget, - format!( - "Профиль '{}' ссылается на отсутствующую цель '{}'", - profile.id, profile.target_id - ), - ) - }) -} - -fn ensure_target_supported( - profile: &Profile, - target: &Target, - components: &[ComponentStatus], -) -> Result<(), ProxyRouterError> { - if target.protocol != ProxyProtocol::Socks5 { - return Err(ProxyRouterError::new( - ProxyRouterErrorKind::UnsupportedTargetProtocol, - format!( - "Цель '{}' использует HTTP, но ProxiFyre требует SOCKS5", - target.id - ), - )); - } - - if let Some(required_component) = &target.requires_component { - let Some(status) = components - .iter() - .find(|component| &component.id == required_component) - else { - return Err(ProxyRouterError::new( - ProxyRouterErrorKind::MissingRequiredComponent, - format!( - "Цель '{}' профиля '{}' требует отсутствующий компонент '{}'", - target.id, - profile.id, - component_id_label(required_component) - ), - )); - }; - - if !component_is_running(status) { - return Err(ProxyRouterError::new( - ProxyRouterErrorKind::RequiredComponentNotRunning, - format!( - "Цель '{}' профиля '{}' требует запущенный компонент '{}'", - target.id, - profile.id, - component_id_label(required_component) - ), - )); - } - } - - Ok(()) -} - -fn component_is_running(status: &ComponentStatus) -> bool { - status.installed && status.running && status.state == ComponentState::Running -} - -fn app_names_for_profile(profile: &Profile) -> Vec { - let mut names = Vec::new(); - - for item in &profile.items { - let value = item.value.trim(); - if value.is_empty() { - continue; - } - - let app_name = match item.item_type { - ProfileItemType::Process | ProfileItemType::Folder | ProfileItemType::Exe => value, - }; - - if !names.iter().any(|existing| existing == app_name) { - names.push(app_name.to_string()); - } - } - - names -} - -fn protocols_for_profile(profile: &Profile) -> Vec { - let mut protocols = Vec::new(); - - for protocol in &profile.protocols { - let value = match protocol { - Protocol::Tcp => "TCP", - Protocol::Udp => "UDP", - }; - - if !protocols.iter().any(|existing| existing == value) { - protocols.push(value.to_string()); - } - } - - protocols -} - -fn component_id_label(component_id: &ComponentId) -> &'static str { - match component_id { - ComponentId::ControlApp => "control-app", - ComponentId::Proxyfier => "proxyfier", - ComponentId::Singbox => "singbox", - } -} diff --git a/apps/windows-client/src-tauri/src/adapters/proxy_router.rs b/apps/windows-client/src-tauri/src/adapters/proxy_router.rs deleted file mode 100644 index f9e0162..0000000 --- a/apps/windows-client/src-tauri/src/adapters/proxy_router.rs +++ /dev/null @@ -1,67 +0,0 @@ -use crate::models::{ComponentStatus, Profile, Target}; - -#[derive(Debug, Clone, Copy)] -pub struct ProxyRouterRequest<'a> { - pub profiles: &'a [Profile], - pub targets: &'a [Target], - pub components: &'a [ComponentStatus], -} - -impl<'a> ProxyRouterRequest<'a> { - pub fn new( - profiles: &'a [Profile], - targets: &'a [Target], - components: &'a [ComponentStatus], - ) -> Self { - Self { - profiles, - targets, - components, - } - } -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct ProxyRouterGeneratedConfig { - pub adapter_id: String, - pub output_file_name: String, - pub contents: String, - pub enabled_profiles: usize, - pub routed_apps: usize, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct ProxyRouterError { - pub kind: ProxyRouterErrorKind, - pub message: String, -} - -impl ProxyRouterError { - pub fn new(kind: ProxyRouterErrorKind, message: impl Into) -> Self { - Self { - kind, - message: message.into(), - } - } -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum ProxyRouterErrorKind { - EmptyProfileItems, - MissingTarget, - MissingRequiredComponent, - RequiredComponentNotRunning, - UnsupportedTargetProtocol, - Serialization, -} - -pub trait ProxyRouterAdapter { - fn id(&self) -> &'static str; - - fn output_file_name(&self) -> &'static str; - - fn generate_config( - &self, - request: ProxyRouterRequest<'_>, - ) -> Result; -} diff --git a/apps/windows-client/src-tauri/src/adapters/singbox.rs b/apps/windows-client/src-tauri/src/adapters/singbox.rs deleted file mode 100644 index 117d3b0..0000000 --- a/apps/windows-client/src-tauri/src/adapters/singbox.rs +++ /dev/null @@ -1,338 +0,0 @@ -use crate::models::{LocalSingBoxConfig, SubscriptionCache}; -use serde::{Deserialize, Serialize}; -use serde_json::{json, Value}; -use std::{ - env, fs, - path::Path, - process::Command, - time::{SystemTime, UNIX_EPOCH}, -}; - -pub const SINGBOX_ADAPTER_ID: &str = "singbox"; -pub const SINGBOX_OUTPUT_FILE: &str = "sing-box-config.json"; -pub const DEFAULT_MIXED_INBOUND_TAG: &str = "vpn-proxy-mixed-in"; -pub const DEFAULT_VPN_OUTBOUND_TAG: &str = "vpn"; -pub const DEFAULT_DIRECT_OUTBOUND_TAG: &str = "direct"; -pub const DEFAULT_BLOCK_OUTBOUND_TAG: &str = "block"; - -const SUPPORTED_PROXY_TYPES: &[&str] = &["vless", "vmess", "trojan", "shadowsocks", "hysteria2"]; - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct SingBoxAdapter { - log_level: String, - inbound_tag: String, - vpn_outbound_tag: String, -} - -impl SingBoxAdapter { - pub fn new( - log_level: impl Into, - inbound_tag: impl Into, - vpn_outbound_tag: impl Into, - ) -> Self { - Self { - log_level: log_level.into(), - inbound_tag: inbound_tag.into(), - vpn_outbound_tag: vpn_outbound_tag.into(), - } - } - - pub fn generate_config( - &self, - request: SingBoxGenerationRequest<'_>, - checker: &C, - ) -> Result - where - C: SingBoxConfigChecker, - { - let selected_server_tag = request - .config - .selected_server_tag - .as_deref() - .map(str::trim) - .filter(|value| !value.is_empty()) - .ok_or_else(|| { - SingBoxConfigError::new( - SingBoxConfigErrorKind::MissingSelectedServer, - "Сервер Local sing-box не выбран", - ) - })?; - let vpn_outbound = selected_outbound( - &request.subscription_cache.config, - selected_server_tag, - &self.vpn_outbound_tag, - )?; - let generated_config = json!({ - "log": { - "disabled": false, - "level": self.log_level, - "timestamp": true - }, - "inbounds": [ - { - "type": "mixed", - "tag": self.inbound_tag, - "listen": request.config.listen_host, - "listen_port": request.config.listen_port, - "users": [], - "set_system_proxy": false - } - ], - "outbounds": [ - vpn_outbound, - { "type": "direct", "tag": DEFAULT_DIRECT_OUTBOUND_TAG }, - { "type": "block", "tag": DEFAULT_BLOCK_OUTBOUND_TAG } - ], - "route": { - "rules": [ - { "ip_is_private": true, "outbound": DEFAULT_DIRECT_OUTBOUND_TAG } - ], - "final": self.vpn_outbound_tag - } - }); - let contents = serde_json::to_string_pretty(&generated_config).map_err(|error| { - SingBoxConfigError::new( - SingBoxConfigErrorKind::Serialization, - format!("Не удалось сериализовать конфиг sing-box: {error}"), - ) - })?; - let check = match request.binary_path { - Some(binary_path) => Some(checker.check_config(binary_path, &contents)?), - None => None, - }; - - Ok(SingBoxGeneratedConfig { - adapter_id: SINGBOX_ADAPTER_ID.to_string(), - output_file_name: SINGBOX_OUTPUT_FILE.to_string(), - contents, - selected_server_tag: selected_server_tag.to_string(), - listen: request.config.listen_host.clone(), - listen_port: request.config.listen_port, - check, - }) - } -} - -impl Default for SingBoxAdapter { - fn default() -> Self { - Self::new("info", DEFAULT_MIXED_INBOUND_TAG, DEFAULT_VPN_OUTBOUND_TAG) - } -} - -#[derive(Debug, Clone, Copy)] -pub struct SingBoxGenerationRequest<'a> { - pub config: &'a LocalSingBoxConfig, - pub subscription_cache: &'a SubscriptionCache, - pub binary_path: Option<&'a Path>, -} - -impl<'a> SingBoxGenerationRequest<'a> { - pub fn new( - config: &'a LocalSingBoxConfig, - subscription_cache: &'a SubscriptionCache, - binary_path: Option<&'a Path>, - ) -> Self { - Self { - config, - subscription_cache, - binary_path, - } - } -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct SingBoxGeneratedConfig { - pub adapter_id: String, - pub output_file_name: String, - pub contents: String, - pub selected_server_tag: String, - pub listen: String, - pub listen_port: u16, - pub check: Option, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct SingBoxCheckResult { - pub checked: bool, - pub success: bool, - pub message: String, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct SingBoxConfigError { - pub kind: SingBoxConfigErrorKind, - pub message: String, -} - -impl SingBoxConfigError { - pub fn new(kind: SingBoxConfigErrorKind, message: impl Into) -> Self { - Self { - kind, - message: message.into(), - } - } -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum SingBoxConfigErrorKind { - MissingSelectedServer, - MissingSelectedOutbound, - UnsupportedSelectedOutbound, - Serialization, - CheckFailed, -} - -pub trait SingBoxConfigChecker { - fn check_config( - &self, - binary_path: &Path, - config_json: &str, - ) -> Result; -} - -#[derive(Debug, Clone, Copy, Default)] -pub struct SingBoxCommandChecker; - -impl SingBoxConfigChecker for SingBoxCommandChecker { - fn check_config( - &self, - binary_path: &Path, - config_json: &str, - ) -> Result { - let config_path = env::temp_dir().join(format!( - "vpn-proxy-sing-box-{}-{}.json", - std::process::id(), - now_millis() - )); - - fs::write(&config_path, config_json).map_err(|error| { - SingBoxConfigError::new( - SingBoxConfigErrorKind::CheckFailed, - format!( - "Не удалось записать временный конфиг sing-box '{}': {error}", - config_path.display() - ), - ) - })?; - - let output = Command::new(binary_path) - .arg("check") - .arg("-c") - .arg(&config_path) - .output() - .map_err(|error| { - let _ = fs::remove_file(&config_path); - SingBoxConfigError::new( - SingBoxConfigErrorKind::CheckFailed, - format!( - "Не удалось выполнить '{} check': {error}", - binary_path.display() - ), - ) - })?; - let _ = fs::remove_file(&config_path); - - let stdout = String::from_utf8_lossy(&output.stdout); - let stderr = String::from_utf8_lossy(&output.stderr); - let message = command_message(&stdout, &stderr); - - if !output.status.success() { - return Err(SingBoxConfigError::new( - SingBoxConfigErrorKind::CheckFailed, - format!("Проверка sing-box не прошла: {message}"), - )); - } - - Ok(SingBoxCheckResult { - checked: true, - success: true, - message: if message.is_empty() { - "Проверка sing-box прошла успешно".to_string() - } else { - message - }, - }) - } -} - -fn selected_outbound( - subscription_config: &Value, - selected_server_tag: &str, - vpn_outbound_tag: &str, -) -> Result { - let outbounds = subscription_config - .get("outbounds") - .and_then(Value::as_array) - .ok_or_else(|| { - SingBoxConfigError::new( - SingBoxConfigErrorKind::MissingSelectedOutbound, - "В cache подписки нет outbounds", - ) - })?; - let outbound = outbounds - .iter() - .find(|outbound| { - outbound - .get("tag") - .and_then(Value::as_str) - .is_some_and(|tag| tag.trim() == selected_server_tag) - }) - .ok_or_else(|| { - SingBoxConfigError::new( - SingBoxConfigErrorKind::MissingSelectedOutbound, - format!("Outbound не найден: {selected_server_tag}"), - ) - })?; - let outbound_type = outbound - .get("type") - .and_then(Value::as_str) - .unwrap_or_default(); - - if !SUPPORTED_PROXY_TYPES.contains(&outbound_type) { - return Err(SingBoxConfigError::new( - SingBoxConfigErrorKind::UnsupportedSelectedOutbound, - format!( - "Outbound '{selected_server_tag}' имеет неподдерживаемый тип '{outbound_type}'" - ), - )); - } - - let mut outbound = outbound.clone(); - let object = outbound.as_object_mut().ok_or_else(|| { - SingBoxConfigError::new( - SingBoxConfigErrorKind::UnsupportedSelectedOutbound, - format!("Outbound '{selected_server_tag}' должен быть JSON-объектом"), - ) - })?; - object.insert( - "tag".to_string(), - Value::String(vpn_outbound_tag.to_string()), - ); - if outbound_type == "vless" && !object.contains_key("packet_encoding") { - object.insert( - "packet_encoding".to_string(), - Value::String("xudp".to_string()), - ); - } - - Ok(outbound) -} - -fn now_millis() -> u128 { - SystemTime::now() - .duration_since(UNIX_EPOCH) - .map(|duration| duration.as_millis()) - .unwrap_or_default() -} - -fn command_message(stdout: &str, stderr: &str) -> String { - let stdout = stdout.trim(); - let stderr = stderr.trim(); - - match (stdout.is_empty(), stderr.is_empty()) { - (true, true) => String::new(), - (false, true) => stdout.to_string(), - (true, false) => stderr.to_string(), - (false, false) => format!("{stdout}\n{stderr}"), - } -} diff --git a/apps/windows-client/src-tauri/src/commands.rs b/apps/windows-client/src-tauri/src/commands.rs deleted file mode 100644 index cafabfd..0000000 --- a/apps/windows-client/src-tauri/src/commands.rs +++ /dev/null @@ -1,3530 +0,0 @@ -#[cfg(not(test))] -use crate::adapters::proxifyre::ProxiFyreAdapter; -#[cfg(not(test))] -use crate::adapters::proxy_router::{ - ProxyRouterAdapter, ProxyRouterError, ProxyRouterErrorKind, ProxyRouterGeneratedConfig, - ProxyRouterRequest, -}; -#[cfg(not(test))] -use crate::adapters::singbox::{ - SingBoxAdapter, SingBoxCheckResult, SingBoxCommandChecker, SingBoxConfigChecker, - SingBoxConfigError, SingBoxConfigErrorKind, SingBoxGeneratedConfig, SingBoxGenerationRequest, -}; -use crate::component_detection::{ - detect_proxyfier_install, detect_proxyfier_install_with_host, detect_singbox_install, - proxyfier_component_from_detection, singbox_component_from_detection, DetectedProxyfier, - DetectedSingBox, ProxyfierDetectionHost, SystemProxyfierDetectionHost, -}; -use crate::models::{ - ActivityEntry, ActivityLevel, ComponentId, ComponentState, ComponentStatus, LocalSingBoxConfig, - Profile, ProfileInput, ProfileItem, ProfileItemInput, ProfileItemType, Protocol, ProxyProtocol, - SubscriptionCache, SubscriptionServer, Target, TargetInput, TargetKind, -}; -#[cfg(test)] -use crate::proxifyre::ProxiFyreAdapter; -#[cfg(test)] -use crate::proxy_router::{ - ProxyRouterAdapter, ProxyRouterError, ProxyRouterErrorKind, ProxyRouterGeneratedConfig, - ProxyRouterRequest, -}; -#[cfg(test)] -use crate::singbox::{ - SingBoxAdapter, SingBoxCheckResult, SingBoxCommandChecker, SingBoxConfigChecker, - SingBoxConfigError, SingBoxConfigErrorKind, SingBoxGeneratedConfig, SingBoxGenerationRequest, -}; -use crate::singbox_service::{ - build_singbox_setup_status, ensure_safe_singbox_install_dir, - parse_service_command_output as parse_singbox_service_command_output, service_control_script, - ServiceCommandOutput as SingBoxServiceCommandOutput, SingBoxServiceAction, SingBoxSetupStatus, -}; -use crate::storage::{default_config_root, JsonStorage}; -use crate::subscription; -use crate::validation::{normalize_profile, normalize_target, ValidationError}; -use serde::{Deserialize, Serialize}; -use std::env; -use std::fs; -use std::net::{IpAddr, TcpStream, ToSocketAddrs, UdpSocket}; -use std::path::{Path, PathBuf}; -use std::process::{Command, Output}; -use std::time::{Duration, Instant}; -use std::time::{SystemTime, UNIX_EPOCH}; - -const PROXIFYRE_INSTALL_DIR: &str = r"C:\Tools\ProxiFyre"; -const PROXIFYRE_RELEASE_API_URL: &str = - "https://api.github.com/repos/wiresock/proxifyre/releases/latest"; -const NDISAPI_RELEASE_API_URL: &str = - "https://api.github.com/repos/wiresock/ndisapi/releases/latest"; -const VC_REDIST_X64_URL: &str = "https://aka.ms/vc14/vc_redist.x64.exe"; -const VC_REDIST_X86_URL: &str = "https://aka.ms/vc14/vc_redist.x86.exe"; - -#[derive(Debug, Clone)] -pub struct CommandState { - root: PathBuf, -} - -impl CommandState { - pub fn new(root: impl Into) -> Self { - Self { root: root.into() } - } - - pub fn storage(&self) -> JsonStorage { - JsonStorage::new(self.root.clone()) - } -} - -impl Default for CommandState { - fn default() -> Self { - Self::new(default_config_root()) - } -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct CommandError { - pub code: String, - pub message: String, - #[serde(default)] - pub details: Vec, -} - -impl CommandError { - fn new(code: impl Into, message: impl Into) -> Self { - Self { - code: code.into(), - message: message.into(), - details: Vec::new(), - } - } - - fn with_details( - code: impl Into, - message: impl Into, - details: Vec, - ) -> Self { - Self { - code: code.into(), - message: message.into(), - details, - } - } -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct ValidationIssue { - pub field: String, - pub message: String, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct StatusResponse { - pub route_line: String, - pub active_profile_count: usize, - pub routed_app_count: usize, - pub active_target: Option, - pub components: Vec, - pub recent_activity: Vec, - pub generated_config_path: String, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct SavedStateResponse { - pub profiles: Vec, - pub targets: Vec, - pub generated_config_path: String, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct ProxiFyreSetupStatusDto { - pub ready: bool, - pub missing_count: usize, - pub items: Vec, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct ProxiFyreSetupItemDto { - pub id: String, - pub name: String, - pub installed: bool, - pub version: Option, - pub details: String, -} - -pub type SingBoxSetupStatusDto = SingBoxSetupStatus; - -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct LocalSingBoxStatusResponse { - pub config: LocalSingBoxConfigDto, - pub cache: Option, - pub component: ComponentStatusDto, - pub generated_config_path: String, - pub lan_listen_host: Option, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct LocalSingBoxConfigDto { - pub subscription_display_url: Option, - pub has_subscription: bool, - pub selected_server_tag: Option, - pub listen_host: String, - pub listen_port: u16, - pub service_name: String, - pub install_root: String, - pub updated_at: Option, -} - -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct SubscriptionCacheDto { - pub servers: Vec, - pub user_info: serde_json::Map, - pub fetched_at: String, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct SubscriptionServerDto { - pub tag: String, - #[serde(rename = "type")] - pub server_type: String, - pub server: String, - pub server_port: u16, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct SaveSingBoxSubscriptionInputDto { - pub subscription_url: String, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct SelectSingBoxServerInputDto { - pub tag: String, - #[serde(default)] - pub server: Option, - #[serde(default)] - pub server_port: Option, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct PingSingBoxServerInputDto { - pub tag: String, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct PingProxyTargetInputDto { - pub host: String, - pub port: u16, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct PingServerResponse { - pub tag: String, - pub server: String, - pub server_port: u16, - pub ok: bool, - pub latency: Option, - pub error: Option, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct GenerateSingBoxConfigResponse { - pub success: bool, - pub message: String, - pub adapter_id: String, - pub generated_config_path: String, - pub selected_server_tag: String, - pub listen_host: String, - pub listen_port: u16, - pub check: Option, - pub activity: ActivityEntryDto, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct ProfileInputDto { - pub id: Option, - pub name: String, - #[serde(default)] - pub enabled: Option, - #[serde(default)] - pub target_id: Option, - #[serde(default)] - pub protocols: Option>, - #[serde(default)] - pub items: Option>, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct ProfileItemInputDto { - #[serde(rename = "type")] - pub item_type: String, - pub value: String, - #[serde(default)] - pub recursive: Option, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct TargetInputDto { - pub id: Option, - pub name: String, - #[serde(default)] - pub kind: Option, - #[serde(default)] - pub protocol: Option, - pub host: String, - pub port: u32, - #[serde(default)] - pub requires_component: Option, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct ProfileDto { - pub id: String, - pub name: String, - pub enabled: bool, - pub target_id: String, - pub protocols: Vec, - pub items: Vec, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct ProfileItemDto { - #[serde(rename = "type")] - pub item_type: ProfileItemType, - pub value: String, - pub recursive: bool, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct TargetDto { - pub id: String, - pub name: String, - pub kind: TargetKind, - pub protocol: ProxyProtocol, - pub host: String, - pub port: u16, - #[serde(skip_serializing_if = "Option::is_none")] - pub requires_component: Option, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct ComponentStatusDto { - pub id: ComponentId, - pub name: String, - pub state: ComponentState, - pub installed: bool, - pub running: bool, - #[serde(skip_serializing_if = "Option::is_none")] - pub version: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub path: Option, - pub problems: Vec, - pub actions: Vec, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct ActivityEntryDto { - pub id: String, - pub at: String, - pub level: ActivityLevel, - pub title: String, - pub message: String, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct ResolveProfilePreviewResponse { - pub profile_id: String, - pub apps: Vec, - pub warnings: Vec, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct ResolvedAppDto { - pub source_type: ProfileItemType, - pub source_value: String, - pub app_name: String, - pub notes: Vec, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct ApplyProfilesResponse { - pub success: bool, - pub changed: bool, - pub message: String, - pub adapter_id: String, - pub generated_config_path: String, - pub enabled_profiles: usize, - pub routed_apps: usize, - pub helper: HelperApplyResult, - pub activity: ActivityEntryDto, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct HelperApplyResult { - pub success: bool, - pub changed: bool, - pub action: String, - pub message: String, -} - -pub struct HelperApplyRequest<'a> { - pub adapter_id: &'a str, - pub config_path: &'a Path, - pub config_contents: &'a str, -} - -pub trait ProxyApplyHelper { - fn apply_proxy_config( - &self, - request: HelperApplyRequest<'_>, - ) -> Result; -} - -pub trait SubscriptionFetcher { - fn fetch_subscription( - &self, - url: &str, - ) -> Result; -} - -pub struct SystemSubscriptionFetcher; - -impl SubscriptionFetcher for SystemSubscriptionFetcher { - fn fetch_subscription( - &self, - url: &str, - ) -> Result { - subscription::fetch_subscription(url) - } -} - -pub trait Clock { - fn now(&self) -> String; -} - -pub struct SystemClock; - -impl Clock for SystemClock { - fn now(&self) -> String { - let seconds = SystemTime::now() - .duration_since(UNIX_EPOCH) - .map(|duration| duration.as_secs()) - .unwrap_or(0); - format!("unix:{seconds}") - } -} - -pub struct StagedApplyHelper; - -impl ProxyApplyHelper for StagedApplyHelper { - fn apply_proxy_config( - &self, - request: HelperApplyRequest<'_>, - ) -> Result { - Ok(HelperApplyResult { - success: true, - changed: true, - action: format!("{}.stage-generated-config", request.adapter_id), - message: format!( - "Сгенерированный конфиг подготовлен в {}; интеграция привилегированного помощника еще не подключена", - request.config_path.display() - ), - }) - } -} - -pub struct DetectedProxyApplyHelper { - host: H, -} - -impl DetectedProxyApplyHelper { - pub fn system() -> Self { - Self { - host: SystemProxyfierDetectionHost, - } - } -} - -impl DetectedProxyApplyHelper { - pub fn new(host: H) -> Self { - Self { host } - } -} - -impl ProxyApplyHelper for DetectedProxyApplyHelper -where - H: ProxyfierDetectionHost, -{ - fn apply_proxy_config( - &self, - request: HelperApplyRequest<'_>, - ) -> Result { - let Some(detected) = detect_proxyfier_install_with_host(&self.host) else { - return staged_apply_result(request); - }; - - apply_to_detected_proxyfier(request, &detected) - } -} - -#[tauri::command] -pub async fn get_status( - state: tauri::State<'_, CommandState>, -) -> Result { - let storage = state.storage(); - tauri::async_runtime::spawn_blocking(move || build_status(&storage)) - .await - .map_err(background_task_error)? -} - -#[tauri::command] -pub fn get_saved_state( - state: tauri::State<'_, CommandState>, -) -> Result { - read_saved_state(&state.storage()) -} - -#[tauri::command] -pub fn get_profiles( - state: tauri::State<'_, CommandState>, -) -> Result, CommandError> { - read_profiles(&state.storage()) -} - -#[tauri::command] -pub fn save_profile( - state: tauri::State<'_, CommandState>, - input: ProfileInputDto, -) -> Result { - save_profile_to_storage(&state.storage(), input) -} - -#[tauri::command] -pub fn get_targets(state: tauri::State<'_, CommandState>) -> Result, CommandError> { - read_targets(&state.storage()) -} - -#[tauri::command] -pub fn save_target( - state: tauri::State<'_, CommandState>, - input: TargetInputDto, -) -> Result { - save_target_to_storage(&state.storage(), input) -} - -#[tauri::command] -pub async fn get_components( - state: tauri::State<'_, CommandState>, -) -> Result, CommandError> { - let storage = state.storage(); - tauri::async_runtime::spawn_blocking(move || read_components(&storage)) - .await - .map_err(background_task_error)? -} - -#[tauri::command] -pub async fn get_proxifyre_setup_status() -> Result { - tauri::async_runtime::spawn_blocking(build_proxifyre_setup_status) - .await - .map_err(background_task_error) -} - -#[tauri::command] -pub async fn get_singbox_status( - state: tauri::State<'_, CommandState>, -) -> Result { - let storage = state.storage(); - tauri::async_runtime::spawn_blocking(move || read_singbox_status(&storage)) - .await - .map_err(background_task_error)? -} - -#[tauri::command] -pub async fn get_singbox_setup_status() -> Result { - tauri::async_runtime::spawn_blocking(|| { - build_singbox_setup_status(detect_singbox_install().as_ref()) - }) - .await - .map_err(background_task_error) -} - -#[tauri::command] -pub fn resolve_profile_preview( - input: ProfileInputDto, -) -> Result { - resolve_preview(input) -} - -#[tauri::command] -pub fn save_singbox_subscription( - state: tauri::State<'_, CommandState>, - input: SaveSingBoxSubscriptionInputDto, -) -> Result { - save_singbox_subscription_to_storage(&state.storage(), input, &SystemClock) -} - -#[tauri::command] -pub async fn fetch_singbox_subscription( - state: tauri::State<'_, CommandState>, -) -> Result { - let storage = state.storage(); - tauri::async_runtime::spawn_blocking(move || { - fetch_singbox_subscription_with_fetcher(&storage, &SystemSubscriptionFetcher, &SystemClock) - }) - .await - .map_err(background_task_error)? -} - -#[tauri::command] -pub fn forget_singbox_subscription( - state: tauri::State<'_, CommandState>, -) -> Result { - forget_singbox_subscription_in_storage(&state.storage(), &SystemClock) -} - -#[tauri::command] -pub fn select_singbox_server( - state: tauri::State<'_, CommandState>, - input: SelectSingBoxServerInputDto, -) -> Result { - select_singbox_server_in_storage(&state.storage(), input, &SystemClock) -} - -#[tauri::command] -pub fn ping_singbox_server( - state: tauri::State<'_, CommandState>, - input: PingSingBoxServerInputDto, -) -> Result { - ping_singbox_server_in_storage(&state.storage(), input) -} - -#[tauri::command] -pub fn ping_all_singbox_servers( - state: tauri::State<'_, CommandState>, -) -> Result, CommandError> { - ping_all_singbox_servers_in_storage(&state.storage()) -} - -#[tauri::command] -pub fn ping_proxy_target(input: PingProxyTargetInputDto) -> Result { - ping_proxy_target_endpoint(input) -} - -#[tauri::command] -pub fn generate_singbox_config( - state: tauri::State<'_, CommandState>, -) -> Result { - let detected = detect_singbox_install(); - let binary_path = detected - .as_ref() - .map(|detected| detected.executable_path.as_path()); - generate_singbox_config_with_services( - &state.storage(), - &SingBoxAdapter::default(), - &SingBoxCommandChecker, - &SystemClock, - binary_path, - ) -} - -#[tauri::command] -pub fn apply_profiles( - state: tauri::State<'_, CommandState>, -) -> Result { - let storage = state.storage(); - let adapter = ProxiFyreAdapter::default(); - let helper = DetectedProxyApplyHelper::system(); - let clock = SystemClock; - - apply_profiles_with_services(&storage, &adapter, &helper, &clock) -} - -#[tauri::command] -pub fn get_logs( - state: tauri::State<'_, CommandState>, -) -> Result, CommandError> { - read_activity(&state.storage()) -} - -#[tauri::command] -pub fn open_config_location(state: tauri::State<'_, CommandState>) -> Result { - let storage = state.storage(); - let generated_path = storage - .paths() - .generated_dir - .join("proxifyre-app-config.json"); - let config_path = detect_proxyfier_install() - .and_then(|detected| detected.config_path) - .filter(|path| path.exists()) - .or_else(|| generated_path.exists().then_some(generated_path.clone())); - - let Some(config_path) = config_path else { - return open_folder(&storage.paths().generated_dir); - }; - - open_file_or_select(&config_path)?; - Ok(config_path.display().to_string()) -} - -#[tauri::command] -pub async fn start_proxifyre_service() -> Result { - tauri::async_runtime::spawn_blocking(|| control_proxifyre_service(ServiceControlAction::Start)) - .await - .map_err(background_task_error)? -} - -#[tauri::command] -pub async fn stop_proxifyre_service() -> Result { - tauri::async_runtime::spawn_blocking(|| control_proxifyre_service(ServiceControlAction::Stop)) - .await - .map_err(background_task_error)? -} - -#[tauri::command] -pub async fn install_proxifyre( - state: tauri::State<'_, CommandState>, -) -> Result { - let storage = state.storage(); - tauri::async_runtime::spawn_blocking(move || install_proxifyre_component(&storage)) - .await - .map_err(background_task_error)? -} - -#[tauri::command] -pub async fn uninstall_proxifyre() -> Result { - tauri::async_runtime::spawn_blocking(uninstall_proxifyre_component) - .await - .map_err(background_task_error)? -} - -#[tauri::command] -pub async fn start_singbox_service( - state: tauri::State<'_, CommandState>, -) -> Result { - let storage = state.storage(); - tauri::async_runtime::spawn_blocking(move || { - let detected = detect_singbox_install(); - let binary_path = detected - .as_ref() - .map(|detected| detected.executable_path.as_path()); - let generated = generate_singbox_config_with_services( - &storage, - &SingBoxAdapter::default(), - &SingBoxCommandChecker, - &SystemClock, - binary_path, - )?; - let generated_path = PathBuf::from(generated.generated_config_path); - control_singbox_service(SingBoxServiceAction::Start, Some(generated_path.as_path())) - }) - .await - .map_err(background_task_error)? -} - -#[tauri::command] -pub async fn stop_singbox_service() -> Result { - tauri::async_runtime::spawn_blocking(|| control_singbox_service(SingBoxServiceAction::Stop, None)) - .await - .map_err(background_task_error)? -} - -#[tauri::command] -pub async fn install_singbox( - state: tauri::State<'_, CommandState>, -) -> Result { - let storage = state.storage(); - tauri::async_runtime::spawn_blocking(move || install_singbox_component(&storage)) - .await - .map_err(background_task_error)? -} - -#[tauri::command] -pub async fn uninstall_singbox() -> Result { - tauri::async_runtime::spawn_blocking(uninstall_singbox_component) - .await - .map_err(background_task_error)? -} - -pub fn build_status(storage: &JsonStorage) -> Result { - let profiles = storage.read_profiles().map_err(storage_error)?; - let targets = storage.read_targets().map_err(storage_error)?; - let components = components_or_defaults(storage)?; - let activity = storage.read_activity().map_err(storage_error)?; - let active_profile_count = profiles.iter().filter(|profile| profile.enabled).count(); - let routed_app_count = profiles - .iter() - .filter(|profile| profile.enabled) - .map(|profile| profile.items.len()) - .sum(); - let active_target = profiles - .iter() - .find(|profile| profile.enabled) - .and_then(|profile| targets.iter().find(|target| target.id == profile.target_id)); - let route_line = route_line(active_target); - - Ok(StatusResponse { - route_line, - active_profile_count, - routed_app_count, - active_target: active_target.map(TargetDto::from), - components: components.iter().map(ComponentStatusDto::from).collect(), - recent_activity: activity - .iter() - .take(10) - .map(ActivityEntryDto::from) - .collect(), - generated_config_path: storage - .paths() - .generated_dir - .join("proxifyre-app-config.json") - .display() - .to_string(), - }) -} - -pub fn read_profiles(storage: &JsonStorage) -> Result, CommandError> { - storage - .read_profiles() - .map_err(storage_error) - .map(|profiles| profiles.iter().map(ProfileDto::from).collect()) -} - -pub fn save_profile_to_storage( - storage: &JsonStorage, - input: ProfileInputDto, -) -> Result { - let profile = normalize_profile(input.into()).map_err(validation_error)?; - let mut profiles = storage.read_profiles().map_err(storage_error)?; - - match profiles - .iter() - .position(|existing| existing.id == profile.id) - { - Some(index) => profiles[index] = profile.clone(), - None => profiles.push(profile.clone()), - } - - storage.write_profiles(&profiles).map_err(storage_error)?; - Ok(ProfileDto::from(&profile)) -} - -pub fn read_targets(storage: &JsonStorage) -> Result, CommandError> { - storage - .read_targets() - .map_err(storage_error) - .map(|targets| targets.iter().map(TargetDto::from).collect()) -} - -pub fn save_target_to_storage( - storage: &JsonStorage, - input: TargetInputDto, -) -> Result { - let target = normalize_target(input.into()).map_err(validation_error)?; - let mut targets = storage.read_targets().map_err(storage_error)?; - - match targets.iter().position(|existing| existing.id == target.id) { - Some(index) => targets[index] = target.clone(), - None => targets.push(target.clone()), - } - - storage.write_targets(&targets).map_err(storage_error)?; - Ok(TargetDto::from(&target)) -} - -pub fn read_components(storage: &JsonStorage) -> Result, CommandError> { - components_or_defaults(storage).map(|components| { - components - .iter() - .map(ComponentStatusDto::from) - .collect::>() - }) -} - -pub fn read_activity(storage: &JsonStorage) -> Result, CommandError> { - storage - .read_activity() - .map_err(storage_error) - .map(|entries| entries.iter().map(ActivityEntryDto::from).collect()) -} - -pub fn read_saved_state(storage: &JsonStorage) -> Result { - Ok(SavedStateResponse { - profiles: read_profiles(storage)?, - targets: read_targets(storage)?, - generated_config_path: storage - .paths() - .generated_dir - .join("proxifyre-app-config.json") - .display() - .to_string(), - }) -} - -pub fn resolve_preview( - input: ProfileInputDto, -) -> Result { - let profile = normalize_profile(input.into()).map_err(validation_error)?; - let mut warnings = Vec::new(); - let apps = profile - .items - .iter() - .map(|item| resolved_app(item, &mut warnings)) - .collect(); - - Ok(ResolveProfilePreviewResponse { - profile_id: profile.id, - apps, - warnings, - }) -} - -pub fn apply_profiles_with_services( - storage: &JsonStorage, - adapter: &impl ProxyRouterAdapter, - helper: &impl ProxyApplyHelper, - clock: &impl Clock, -) -> Result { - let profiles = storage.read_profiles().map_err(storage_error)?; - let targets = storage.read_targets().map_err(storage_error)?; - let components = components_or_defaults(storage)?; - let generated = - match adapter.generate_config(ProxyRouterRequest::new(&profiles, &targets, &components)) { - Ok(generated) => generated, - Err(error) => { - let command_error = adapter_error(error); - let activity = activity_for_apply_error(clock, &command_error); - storage.append_activity(activity).map_err(storage_error)?; - return Err(command_error); - } - }; - - let generated_path = storage - .paths() - .generated_dir - .join(generated.output_file_name.as_str()); - write_generated_config(&generated_path, &generated.contents)?; - - let helper_result = helper.apply_proxy_config(HelperApplyRequest { - adapter_id: generated.adapter_id.as_str(), - config_path: &generated_path, - config_contents: generated.contents.as_str(), - })?; - - let activity = activity_for_apply(clock, &generated, &generated_path, &helper_result); - storage - .append_activity(activity.clone()) - .map_err(storage_error)?; - - Ok(ApplyProfilesResponse { - success: helper_result.success, - changed: helper_result.changed, - message: helper_result.message.clone(), - adapter_id: generated.adapter_id, - generated_config_path: generated_path.display().to_string(), - enabled_profiles: generated.enabled_profiles, - routed_apps: generated.routed_apps, - helper: helper_result, - activity: ActivityEntryDto::from(&activity), - }) -} - -pub fn read_singbox_status( - storage: &JsonStorage, -) -> Result { - let config = storage.read_local_singbox_config().map_err(storage_error)?; - let cache = storage - .read_singbox_subscription_cache() - .map_err(storage_error)?; - let detected = detect_singbox_install(); - let component = singbox_component_from_detection(detected.as_ref()); - - Ok(LocalSingBoxStatusResponse { - config: LocalSingBoxConfigDto::from(&config), - cache: cache.as_ref().map(SubscriptionCacheDto::from), - component: ComponentStatusDto::from(&component), - generated_config_path: storage - .paths() - .generated_dir - .join("sing-box-config.json") - .display() - .to_string(), - lan_listen_host: local_lan_ipv4(), - }) -} - -pub fn save_singbox_subscription_to_storage( - storage: &JsonStorage, - input: SaveSingBoxSubscriptionInputDto, - clock: &impl Clock, -) -> Result { - let subscription_url = input.subscription_url.trim().to_string(); - validate_subscription_url(&subscription_url)?; - - let mut config = storage.read_local_singbox_config().map_err(storage_error)?; - config.subscription_url = Some(subscription_url); - config.updated_at = Some(clock.now()); - storage - .write_local_singbox_config(&config) - .map_err(storage_error)?; - - read_singbox_status(storage) -} - -pub fn fetch_singbox_subscription_with_fetcher( - storage: &JsonStorage, - fetcher: &impl SubscriptionFetcher, - clock: &impl Clock, -) -> Result { - let mut config = storage.read_local_singbox_config().map_err(storage_error)?; - let subscription_url = config - .subscription_url - .as_deref() - .map(str::trim) - .filter(|value| !value.is_empty()) - .map(str::to_string) - .ok_or_else(|| { - CommandError::new( - "singbox_subscription_missing", - "Ссылка на подписку Local sing-box не сохранена.", - ) - })?; - - let cache = fetcher - .fetch_subscription(&subscription_url) - .map_err(|error| CommandError::new("singbox_subscription_fetch_failed", error.message))?; - let selected_tag = config - .selected_server_tag - .as_deref() - .filter(|tag| cache.servers.iter().any(|server| server.tag == *tag)) - .map(str::to_string) - .or_else(|| cache.servers.first().map(|server| server.tag.clone())); - - config.selected_server_tag = selected_tag; - config.updated_at = Some(clock.now()); - storage - .write_singbox_subscription_cache(&cache) - .map_err(storage_error)?; - storage - .write_local_singbox_config(&config) - .map_err(storage_error)?; - storage - .append_activity(ActivityEntry { - id: "singbox-subscription-fetched".to_string(), - at: clock.now(), - level: ActivityLevel::Success, - title: "Подписка Local sing-box обновлена".to_string(), - message: format!("Серверов найдено: {}", cache.servers.len()), - }) - .map_err(storage_error)?; - - read_singbox_status(storage) -} - -pub fn forget_singbox_subscription_in_storage( - storage: &JsonStorage, - clock: &impl Clock, -) -> Result { - let mut config = storage.read_local_singbox_config().map_err(storage_error)?; - config.subscription_url = None; - config.selected_server_tag = None; - config.updated_at = Some(clock.now()); - storage - .write_local_singbox_config(&config) - .map_err(storage_error)?; - storage - .remove_singbox_subscription_cache() - .map_err(storage_error)?; - - read_singbox_status(storage) -} - -pub fn select_singbox_server_in_storage( - storage: &JsonStorage, - input: SelectSingBoxServerInputDto, - clock: &impl Clock, -) -> Result { - let requested_tag = input.tag.trim().to_string(); - if requested_tag.is_empty() { - return Err(CommandError::new( - "singbox_server_tag_missing", - "Сервер Local sing-box не выбран.", - )); - } - - let cache = storage - .read_singbox_subscription_cache() - .map_err(storage_error)? - .ok_or_else(|| { - CommandError::new( - "singbox_subscription_cache_missing", - "Сначала нужно загрузить подписку Local sing-box.", - ) - })?; - let Some(server) = find_subscription_server( - &cache, - &requested_tag, - input.server.as_deref(), - input.server_port, - ) else { - return Err(CommandError::new( - "singbox_server_not_found", - format!("Сервер Local sing-box '{requested_tag}' не найден в текущей подписке."), - )); - }; - let selected_tag = server.tag.clone(); - - let mut config = storage.read_local_singbox_config().map_err(storage_error)?; - config.selected_server_tag = Some(selected_tag); - config.updated_at = Some(clock.now()); - storage - .write_local_singbox_config(&config) - .map_err(storage_error)?; - - read_singbox_status(storage) -} - -pub fn ping_singbox_server_in_storage( - storage: &JsonStorage, - input: PingSingBoxServerInputDto, -) -> Result { - let tag = input.tag.trim(); - let cache = read_required_singbox_cache(storage)?; - let server = find_subscription_server(&cache, tag, None, None).ok_or_else(|| { - CommandError::new( - "singbox_server_not_found", - format!("Сервер Local sing-box '{tag}' не найден в текущей подписке."), - ) - })?; - - Ok(ping_subscription_server(server)) -} - -pub fn ping_all_singbox_servers_in_storage( - storage: &JsonStorage, -) -> Result, CommandError> { - let cache = read_required_singbox_cache(storage)?; - Ok(cache.servers.iter().map(ping_subscription_server).collect()) -} - -pub fn ping_proxy_target_endpoint( - input: PingProxyTargetInputDto, -) -> Result { - let host = input.host.trim(); - if host.is_empty() { - return Err(CommandError::new( - "proxy_target_host_missing", - "Хост внешнего прокси не указан.", - )); - } - - Ok(ping_endpoint("external-proxy", host, input.port)) -} - -pub fn generate_singbox_config_with_services( - storage: &JsonStorage, - adapter: &SingBoxAdapter, - checker: &C, - clock: &impl Clock, - binary_path: Option<&Path>, -) -> Result -where - C: SingBoxConfigChecker, -{ - let config = storage.read_local_singbox_config().map_err(storage_error)?; - let cache = read_required_singbox_cache(storage)?; - let generated = adapter - .generate_config( - SingBoxGenerationRequest::new(&config, &cache, binary_path), - checker, - ) - .map_err(singbox_adapter_error)?; - let generated_path = storage - .paths() - .generated_dir - .join(generated.output_file_name.as_str()); - - write_generated_config(&generated_path, &generated.contents)?; - ensure_local_singbox_target(storage, &config)?; - - let activity = activity_for_singbox_generate(clock, &generated, &generated_path); - storage - .append_activity(activity.clone()) - .map_err(storage_error)?; - - Ok(GenerateSingBoxConfigResponse { - success: true, - message: "Конфиг Local sing-box создан".to_string(), - adapter_id: generated.adapter_id, - generated_config_path: generated_path.display().to_string(), - selected_server_tag: generated.selected_server_tag, - listen_host: generated.listen, - listen_port: generated.listen_port, - check: generated.check, - activity: ActivityEntryDto::from(&activity), - }) -} - -fn read_required_singbox_cache(storage: &JsonStorage) -> Result { - storage - .read_singbox_subscription_cache() - .map_err(storage_error)? - .ok_or_else(|| { - CommandError::new( - "singbox_subscription_cache_missing", - "Сначала нужно загрузить подписку Local sing-box.", - ) - }) -} - -fn validate_subscription_url(subscription_url: &str) -> Result<(), CommandError> { - if subscription_url.is_empty() { - return Err(CommandError::new( - "singbox_subscription_url_missing", - "Ссылка на подписку Local sing-box не указана.", - )); - } - - let parsed = url::Url::parse(subscription_url).map_err(|_| { - CommandError::new( - "singbox_subscription_url_invalid", - "Ссылка на подписку Local sing-box должна быть корректным URL.", - ) - })?; - if !matches!(parsed.scheme(), "http" | "https") { - return Err(CommandError::new( - "singbox_subscription_url_invalid", - "Ссылка на подписку Local sing-box должна начинаться с http:// или https://.", - )); - } - - Ok(()) -} - -fn ping_subscription_server(server: &SubscriptionServer) -> PingServerResponse { - ping_endpoint(&server.tag, &server.server, server.server_port) -} - -fn ping_endpoint(tag: &str, server: &str, server_port: u16) -> PingServerResponse { - let started = Instant::now(); - let addresses = match (server, server_port).to_socket_addrs() { - Ok(addresses) => addresses.collect::>(), - Err(error) => { - return PingServerResponse { - tag: tag.to_string(), - server: server.to_string(), - server_port, - ok: false, - latency: None, - error: Some(format!("DNS/адрес недоступен: {error}")), - }; - } - }; - - if addresses.is_empty() { - return PingServerResponse { - tag: tag.to_string(), - server: server.to_string(), - server_port, - ok: false, - latency: None, - error: Some("DNS не вернул адреса".to_string()), - }; - } - - let timeout = Duration::from_secs(2); - let mut last_error = None; - for address in addresses { - match TcpStream::connect_timeout(&address, timeout) { - Ok(_) => { - return PingServerResponse { - tag: tag.to_string(), - server: server.to_string(), - server_port, - ok: true, - latency: Some(started.elapsed().as_millis()), - error: None, - }; - } - Err(error) => last_error = Some(error.to_string()), - } - } - - PingServerResponse { - tag: tag.to_string(), - server: server.to_string(), - server_port, - ok: false, - latency: None, - error: last_error, - } -} - -fn local_lan_ipv4() -> Option { - let socket = UdpSocket::bind("0.0.0.0:0").ok()?; - socket.connect("8.8.8.8:80").ok()?; - let IpAddr::V4(address) = socket.local_addr().ok()?.ip() else { - return None; - }; - if address.is_loopback() || address.is_link_local() || address.is_unspecified() { - return None; - } - Some(address.to_string()) -} - -fn find_subscription_server<'a>( - cache: &'a SubscriptionCache, - requested_tag: &str, - requested_server: Option<&str>, - requested_port: Option, -) -> Option<&'a SubscriptionServer> { - cache - .servers - .iter() - .find(|server| server.tag == requested_tag) - .or_else(|| { - let requested = comparable_server_tag(requested_tag); - cache - .servers - .iter() - .find(|server| comparable_server_tag(&server.tag) == requested) - }) - .or_else(|| { - let server_name = requested_server?.trim(); - let server_port = requested_port?; - cache.servers.iter().find(|server| { - server.server.eq_ignore_ascii_case(server_name) && server.server_port == server_port - }) - }) -} - -fn comparable_server_tag(value: &str) -> String { - value - .chars() - .filter(|ch| !matches!(ch, '\u{fe0e}' | '\u{fe0f}' | '\u{200d}')) - .collect::() - .split_whitespace() - .collect::>() - .join(" ") -} - -fn ensure_local_singbox_target( - storage: &JsonStorage, - config: &LocalSingBoxConfig, -) -> Result<(), CommandError> { - let mut targets = storage.read_targets().map_err(storage_error)?; - let target = Target { - id: "local-singbox".to_string(), - name: "Локальный sing-box".to_string(), - kind: TargetKind::Local, - protocol: ProxyProtocol::Socks5, - host: config.listen_host.clone(), - port: config.listen_port, - requires_component: Some(ComponentId::Singbox), - }; - - match targets.iter().position(|existing| existing.id == target.id) { - Some(index) => targets[index] = target, - None => targets.push(target), - } - - storage.write_targets(&targets).map_err(storage_error) -} - -fn activity_for_singbox_generate( - clock: &impl Clock, - generated: &SingBoxGeneratedConfig, - generated_path: &Path, -) -> ActivityEntry { - ActivityEntry { - id: "singbox-config-generated".to_string(), - at: clock.now(), - level: ActivityLevel::Success, - title: "Конфиг Local sing-box создан".to_string(), - message: format!( - "Сервер: {}, listen: {}:{}, конфиг: {}", - generated.selected_server_tag, - generated.listen, - generated.listen_port, - generated_path.display() - ), - } -} - -fn singbox_adapter_error(error: SingBoxConfigError) -> CommandError { - let code = match error.kind { - SingBoxConfigErrorKind::MissingSelectedServer => "singbox_server_not_selected", - SingBoxConfigErrorKind::MissingSelectedOutbound => "singbox_selected_server_missing", - SingBoxConfigErrorKind::UnsupportedSelectedOutbound => { - "singbox_selected_server_unsupported" - } - SingBoxConfigErrorKind::Serialization => "serialization_error", - SingBoxConfigErrorKind::CheckFailed => "singbox_check_failed", - }; - - CommandError::new(code, error.message) -} - -fn control_singbox_service( - action: SingBoxServiceAction, - config_source: Option<&Path>, -) -> Result { - let Some(detected) = detect_singbox_install() else { - return Err(CommandError::new( - "singbox_not_found", - "Local sing-box не найден на компьютере.", - )); - }; - - let config_target = config_source.map(|_| detected.install_dir.join("config.json")); - let script = service_control_script( - action, - &detected.service_name, - config_source, - config_target.as_deref(), - ); - let output = Command::new("powershell") - .args([ - "-NoProfile", - "-NonInteractive", - "-ExecutionPolicy", - "Bypass", - "-Command", - script.as_str(), - ]) - .output() - .map_err(|error| { - CommandError::new( - singbox_service_error_code(action), - format!( - "Не удалось {} службу Local sing-box: {error}", - action.label() - ), - ) - })?; - let result = parse_singbox_service_command_output(&output.stdout).ok_or_else(|| { - CommandError::new( - singbox_service_error_code(action), - singbox_service_script_failed_message(action, output.status.code()), - ) - })?; - - if result.success { - let refreshed = detect_singbox_install(); - let component = singbox_component_from_detection(refreshed.as_ref()); - return Ok(ComponentStatusDto::from(&component)); - } - - if matches!( - result.code.as_str(), - "start_failed" | "stop_failed" | "config_sync_failed" - ) { - run_elevated_singbox_service_command( - action, - &detected.service_name, - config_source, - config_target.as_deref(), - &result, - )?; - let refreshed = detect_singbox_install(); - let component = singbox_component_from_detection(refreshed.as_ref()); - return Ok(ComponentStatusDto::from(&component)); - } - - Err(CommandError::new( - singbox_service_error_code(action), - singbox_service_command_failed_message(action, &result), - )) -} - -fn run_elevated_singbox_service_command( - action: SingBoxServiceAction, - service_name: &str, - config_source: Option<&Path>, - config_target: Option<&Path>, - direct_result: &SingBoxServiceCommandOutput, -) -> Result<(), CommandError> { - let script_path = - write_elevated_singbox_service_script(action, service_name, config_source, config_target)?; - let launch_script = format!( - "$p = Start-Process -FilePath 'powershell.exe' -Verb RunAs -Wait -PassThru -WindowStyle Hidden -ArgumentList @('-NoProfile','-ExecutionPolicy','Bypass','-File','{}'); exit $p.ExitCode", - escape_powershell_single(&script_path.display().to_string()) - ); - let output = Command::new("powershell") - .args([ - "-NoProfile", - "-NonInteractive", - "-ExecutionPolicy", - "Bypass", - "-Command", - launch_script.as_str(), - ]) - .output(); - - let _ = fs::remove_file(&script_path); - - match output { - Ok(output) if output.status.success() => Ok(()), - Ok(output) => Err(CommandError::new( - singbox_service_error_code(action), - elevated_singbox_service_failed_message(action, direct_result, output.status.code()), - )), - Err(error) => Err(CommandError::new( - singbox_service_error_code(action), - format!( - "Не удалось запросить права администратора, чтобы {} службу Local sing-box: {error}", - action.label() - ), - )), - } -} - -fn write_elevated_singbox_service_script( - action: SingBoxServiceAction, - service_name: &str, - config_source: Option<&Path>, - config_target: Option<&Path>, -) -> Result { - let nonce = SystemTime::now() - .duration_since(UNIX_EPOCH) - .map(|duration| duration.as_millis()) - .unwrap_or(0); - let script_path = env::temp_dir().join(format!("vpn-proxy-singbox-service-{nonce}.ps1")); - let script = elevated_singbox_service_script(action, service_name, config_source, config_target); - - write_powershell_script(&script_path, &script).map_err(|error| { - CommandError::new( - singbox_service_error_code(action), - format!( - "Не удалось подготовить временный скрипт для управления Local sing-box '{}': {error}", - script_path.display() - ), - ) - })?; - - Ok(script_path) -} - -fn elevated_singbox_service_script( - action: SingBoxServiceAction, - service_name: &str, - config_source: Option<&Path>, - config_target: Option<&Path>, -) -> String { - let action_name = action.action_name(); - let escaped_service_name = escape_powershell_single(service_name); - let escaped_config_source = config_source - .map(|path| escape_powershell_single(&path.display().to_string())) - .unwrap_or_default(); - let escaped_config_target = config_target - .map(|path| escape_powershell_single(&path.display().to_string())) - .unwrap_or_default(); - - format!( - r#" -$ErrorActionPreference = 'SilentlyContinue' -$serviceName = '{escaped_service_name}' -$action = '{action_name}' -$configSource = '{escaped_config_source}' -$configTarget = '{escaped_config_target}' - -if ($action -eq 'start') {{ - if (-not [string]::IsNullOrWhiteSpace($configSource)) {{ - if (-not (Test-Path -LiteralPath $configSource)) {{ exit 5 }} - if (-not [string]::IsNullOrWhiteSpace($configTarget)) {{ - try {{ - Copy-Item -LiteralPath $configSource -Destination $configTarget -Force -ErrorAction Stop - }} catch {{ - exit 6 - }} - }} - }} - - $service = Get-Service -Name $serviceName -ErrorAction SilentlyContinue - if ($null -eq $service) {{ exit 2 }} - if ($service.Status -eq 'Running') {{ exit 0 }} - - Start-Service -Name $serviceName -ErrorAction SilentlyContinue - $service = Get-Service -Name $serviceName -ErrorAction SilentlyContinue - if ($null -ne $service) {{ - try {{ $service.WaitForStatus('Running', [TimeSpan]::FromSeconds(15)) }} catch {{}} - if ($service.Status -eq 'Running') {{ exit 0 }} - }} - - exit 3 -}} - -$service = Get-Service -Name $serviceName -ErrorAction SilentlyContinue -if ($null -eq $service) {{ exit 2 }} -if ($service.Status -eq 'Stopped') {{ exit 0 }} - -Stop-Service -Name $serviceName -Force -ErrorAction SilentlyContinue -$service = Get-Service -Name $serviceName -ErrorAction SilentlyContinue -if ($null -ne $service) {{ - try {{ $service.WaitForStatus('Stopped', [TimeSpan]::FromSeconds(15)) }} catch {{}} - if ($service.Status -eq 'Stopped') {{ exit 0 }} -}} - -exit 4 -"# - ) -} - -fn install_singbox_component(storage: &JsonStorage) -> Result { - let generated_config_path = storage.paths().generated_dir.join("sing-box-config.json"); - run_elevated_singbox_package_script( - SingBoxPackageAction::Install, - include_str!("../../scripts/install-singbox.ps1"), - vec![ - "-ConfigSource".to_string(), - generated_config_path.display().to_string(), - ], - &storage.paths().state_dir, - )?; - - let refreshed = detect_singbox_install(); - let Some(detected) = refreshed.as_ref() else { - return Err(CommandError::new( - SingBoxPackageAction::Install.error_code(), - "Установка Local sing-box завершилась, но приложение не найдено после проверки.", - )); - }; - - Ok(ComponentStatusDto::from(&singbox_component_from_detection( - Some(detected), - ))) -} - -fn uninstall_singbox_component() -> Result { - let Some(detected) = detect_singbox_install() else { - let component = singbox_component_from_detection(None); - return Ok(ComponentStatusDto::from(&component)); - }; - - ensure_safe_singbox_install_dir(&detected.install_dir).map_err(|message| { - CommandError::new(SingBoxPackageAction::Uninstall.error_code(), message) - })?; - let artifact_dir = default_config_root().join("state"); - run_elevated_singbox_package_script( - SingBoxPackageAction::Uninstall, - include_str!("../../scripts/install-singbox.ps1"), - vec![ - "-InstallRoot".to_string(), - detected.install_dir.display().to_string(), - "-ServiceName".to_string(), - detected.service_name, - "-Uninstall".to_string(), - ], - &artifact_dir, - )?; - - let refreshed = detect_singbox_install(); - if refreshed.is_some() { - return Err(CommandError::new( - SingBoxPackageAction::Uninstall.error_code(), - "Удаление Local sing-box завершилось, но приложение все еще найдено на компьютере.", - )); - } - - let component = singbox_component_from_detection(None); - Ok(ComponentStatusDto::from(&component)) -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -enum SingBoxPackageAction { - Install, - Uninstall, -} - -impl SingBoxPackageAction { - fn error_code(self) -> &'static str { - match self { - SingBoxPackageAction::Install => "singbox_install_failed", - SingBoxPackageAction::Uninstall => "singbox_uninstall_failed", - } - } - - fn label(self) -> &'static str { - match self { - SingBoxPackageAction::Install => "установить", - SingBoxPackageAction::Uninstall => "удалить", - } - } - - fn file_label(self) -> &'static str { - match self { - SingBoxPackageAction::Install => "install", - SingBoxPackageAction::Uninstall => "uninstall", - } - } -} - -fn run_elevated_singbox_package_script( - action: SingBoxPackageAction, - installer_body: &str, - installer_args: Vec, - artifact_dir: &Path, -) -> Result<(), CommandError> { - let nonce = SystemTime::now() - .duration_since(UNIX_EPOCH) - .map(|duration| duration.as_millis()) - .unwrap_or(0); - fs::create_dir_all(artifact_dir).map_err(|error| { - CommandError::new( - action.error_code(), - format!( - "Не удалось создать папку для временных файлов Local sing-box '{}': {error}", - artifact_dir.display() - ), - ) - })?; - - let installer_path = artifact_dir.join(format!( - "vpn-proxy-singbox-{}-{nonce}.ps1", - action.file_label() - )); - let runner_path = artifact_dir.join(format!( - "vpn-proxy-singbox-{}-{nonce}.runner.ps1", - action.file_label() - )); - let result_path = artifact_dir.join(format!( - "vpn-proxy-singbox-{}-{nonce}.log", - action.file_label() - )); - - write_powershell_script(&installer_path, installer_body).map_err(|error| { - CommandError::new( - action.error_code(), - format!( - "Не удалось подготовить установщик Local sing-box '{}': {error}", - installer_path.display() - ), - ) - })?; - write_powershell_script( - &runner_path, - &singbox_installer_runner_script(&installer_path, &result_path, &installer_args), - ) - .map_err(|error| { - CommandError::new( - action.error_code(), - format!( - "Не удалось подготовить runner Local sing-box '{}': {error}", - runner_path.display() - ), - ) - })?; - - let launch_script = format!( - r#" -$ErrorActionPreference = 'Stop' -$resultPath = '{}' -try {{ - $p = Start-Process -FilePath 'powershell.exe' -Verb RunAs -Wait -PassThru -WindowStyle Hidden -ArgumentList @('-NoProfile','-ExecutionPolicy','Bypass','-File','{}') - if ($null -eq $p) {{ - Set-Content -LiteralPath $resultPath -Value 'Elevated PowerShell не был запущен.' -Encoding UTF8 - exit 1 - }} - exit $p.ExitCode -}} catch {{ - Set-Content -LiteralPath $resultPath -Value ($_ | Out-String) -Encoding UTF8 - exit 1 -}} -"#, - escape_powershell_single(&result_path.display().to_string()), - escape_powershell_single(&runner_path.display().to_string()) - ); - let output = Command::new("powershell") - .args([ - "-NoProfile", - "-NonInteractive", - "-ExecutionPolicy", - "Bypass", - "-Command", - launch_script.as_str(), - ]) - .output(); - - let _ = fs::remove_file(&installer_path); - let _ = fs::remove_file(&runner_path); - - match output { - Ok(output) if output.status.success() => { - let _ = fs::remove_file(&result_path); - Ok(()) - } - Ok(output) => { - let details = package_failure_details(&result_path, &output); - let _ = fs::remove_file(&result_path); - Err(CommandError::new( - action.error_code(), - format!( - "Не удалось {} Local sing-box. Код elevated-команды: {}. {details}", - action.label(), - output.status.code().unwrap_or(-1), - ), - )) - } - Err(error) => Err(CommandError::new( - action.error_code(), - format!( - "Не удалось запросить права администратора, чтобы {} Local sing-box: {error}", - action.label() - ), - )), - } -} - -pub(crate) fn singbox_installer_runner_script( - installer_path: &Path, - result_path: &Path, - installer_args: &[String], -) -> String { - let args = installer_args - .iter() - .map(|arg| format!("'{}'", escape_powershell_single(arg))) - .collect::>() - .join(", "); - - format!( - r#" -$ErrorActionPreference = 'Stop' -$installerPath = '{}' -$resultPath = '{}' -$stdoutPath = "$resultPath.stdout.log" -$stderrPath = "$resultPath.stderr.log" -$installerArgs = @({args}) -try {{ - $output = & powershell.exe -NoProfile -ExecutionPolicy Bypass -File $installerPath @installerArgs 2>&1 - $exitCode = $LASTEXITCODE - Set-Content -LiteralPath $stdoutPath -Value ($output | Out-String) -Encoding UTF8 - if ($exitCode -ne 0) {{ - $stdout = if (Test-Path -LiteralPath $stdoutPath) {{ Get-Content -LiteralPath $stdoutPath -Raw }} else {{ '' }} - $stderr = if (Test-Path -LiteralPath $stderrPath) {{ Get-Content -LiteralPath $stderrPath -Raw }} else {{ '' }} - throw "install-singbox.ps1 завершился с кодом $exitCode. stdout: $stdout stderr: $stderr" - }} - Set-Content -LiteralPath $resultPath -Value 'ok' -Encoding UTF8 - exit 0 -}} catch {{ - Set-Content -LiteralPath $resultPath -Value ($_ | Out-String) -Encoding UTF8 - exit 1 -}} finally {{ - Remove-Item -LiteralPath $stdoutPath, $stderrPath -Force -ErrorAction SilentlyContinue -}} -"#, - escape_powershell_single(&installer_path.display().to_string()), - escape_powershell_single(&result_path.display().to_string()) - ) -} - -fn singbox_service_error_code(action: SingBoxServiceAction) -> &'static str { - match action { - SingBoxServiceAction::Start => "singbox_service_start_failed", - SingBoxServiceAction::Stop => "singbox_service_stop_failed", - } -} - -fn singbox_service_script_failed_message( - action: SingBoxServiceAction, - exit_code: Option, -) -> String { - let exit_code = exit_code - .map(|code| format!(" Код выхода PowerShell: {code}.")) - .unwrap_or_default(); - - format!( - "Не удалось {} службу Local sing-box: команда управления службой не вернула корректный результат.{exit_code}", - action.label() - ) -} - -fn singbox_service_command_failed_message( - action: SingBoxServiceAction, - result: &SingBoxServiceCommandOutput, -) -> String { - let service_name = result - .service_name - .as_deref() - .filter(|value| !value.trim().is_empty()) - .unwrap_or("VpnProxySingBox"); - let status = result - .status - .as_deref() - .filter(|value| !value.trim().is_empty()) - .unwrap_or("неизвестен"); - let pid = result - .process_id - .filter(|value| *value > 0) - .map(|value| format!(", PID: {value}")) - .unwrap_or_default(); - - match result.code.as_str() { - "service_not_found" => "Служба Local sing-box не найдена.".to_string(), - "config_source_missing" => { - "Сгенерированный конфиг Local sing-box не найден перед запуском службы.".to_string() - } - "config_sync_failed" => { - "Не удалось обновить config.json службы Local sing-box перед запуском. Попробуй запустить приложение от имени администратора.".to_string() - } - "start_failed" => format!( - "Не удалось запустить службу {service_name}. Текущий статус: {status}{pid}. Попробуй запустить приложение от имени администратора." - ), - "stop_failed" => format!( - "Не удалось остановить службу {service_name}. Текущий статус: {status}{pid}. Запусти приложение от имени администратора или останови службу вручную в services.msc." - ), - _ => format!( - "Не удалось {} службу {service_name}. Текущий статус: {status}{pid}.", - action.label() - ), - } -} - -fn elevated_singbox_service_failed_message( - action: SingBoxServiceAction, - direct_result: &SingBoxServiceCommandOutput, - exit_code: Option, -) -> String { - let exit_code = exit_code - .map(|code| format!(" Код выхода elevated PowerShell: {code}.")) - .unwrap_or_default(); - format!( - "{} Попытка с правами администратора тоже не сработала.{exit_code}", - singbox_service_command_failed_message(action, direct_result) - ) -} - -fn components_or_defaults(storage: &JsonStorage) -> Result, CommandError> { - let components = storage.read_components().map_err(storage_error)?; - Ok(resolve_component_statuses( - components, - detect_proxyfier_install(), - detect_singbox_install(), - )) -} - -pub fn resolve_component_statuses( - stored_components: Vec, - detected_proxyfier: Option, - detected_singbox: Option, -) -> Vec { - let mut components = default_components(); - - for component in stored_components { - upsert_component(&mut components, component); - } - - if detected_proxyfier.is_some() { - upsert_component( - &mut components, - proxyfier_component_from_detection(detected_proxyfier.as_ref()), - ); - } - if detected_singbox.is_some() { - upsert_component( - &mut components, - singbox_component_from_detection(detected_singbox.as_ref()), - ); - } - - components -} - -fn default_components() -> Vec { - vec![ - ComponentStatus { - id: ComponentId::ControlApp, - name: "Приложение управления".to_string(), - state: ComponentState::Running, - installed: true, - running: true, - version: None, - path: None, - problems: Vec::new(), - actions: vec![ - "Открыть журнал".to_string(), - "Скопировать диагностику".to_string(), - ], - }, - ComponentStatus { - id: ComponentId::Proxyfier, - name: "ProxiFyre".to_string(), - state: ComponentState::Missing, - installed: false, - running: false, - version: None, - path: None, - problems: vec!["ProxiFyre нужен для маршрутизации выбранных приложений".to_string()], - actions: vec!["Установить ProxiFyre".to_string()], - }, - ComponentStatus { - id: ComponentId::Singbox, - name: "Локальный sing-box".to_string(), - state: ComponentState::Missing, - installed: false, - running: false, - version: None, - path: None, - problems: Vec::new(), - actions: vec!["Установить локальный sing-box".to_string()], - }, - ] -} - -fn upsert_component(components: &mut Vec, component: ComponentStatus) { - match components - .iter() - .position(|existing| existing.id == component.id) - { - Some(index) => components[index] = component, - None => components.push(component), - } -} - -fn route_line(active_target: Option<&Target>) -> String { - match active_target { - Some(target) if target.id == "local-singbox" => { - format!( - "Выбранные приложения -> ProxiFyre -> локальный sing-box {}:{} -> VPN", - target.host, target.port - ) - } - Some(target) => format!( - "Выбранные приложения -> ProxiFyre -> внешний прокси {}:{}", - target.host, target.port - ), - None => "Выбранные приложения -> ProxiFyre -> внешний прокси".to_string(), - } -} - -fn resolved_app(item: &ProfileItem, warnings: &mut Vec) -> ResolvedAppDto { - let mut notes = Vec::new(); - match item.item_type { - ProfileItemType::Process => notes.push("Имя процесса используется напрямую".to_string()), - ProfileItemType::Folder => { - let note = "Сканирование папок отложено; ProxiFyre получает путь к папке"; - notes.push(note.to_string()); - warnings.push(note.to_string()); - } - ProfileItemType::Exe => { - notes.push("Путь к EXE сохраняется для сопоставления в ProxiFyre".to_string()) - } - } - - ResolvedAppDto { - source_type: item.item_type.clone(), - source_value: item.value.clone(), - app_name: item.value.clone(), - notes, - } -} - -fn write_generated_config(path: &Path, contents: &str) -> Result<(), CommandError> { - if let Some(parent) = path.parent() { - fs::create_dir_all(parent).map_err(storage_error)?; - } - fs::write(path, contents).map_err(storage_error) -} - -fn open_file_or_select(path: &Path) -> Result<(), CommandError> { - let status = Command::new("notepad.exe") - .arg(path) - .spawn() - .map_err(|error| { - CommandError::new( - "open_config_failed", - format!("Не удалось открыть конфиг '{}': {error}", path.display()), - ) - })?; - - drop(status); - Ok(()) -} - -fn open_folder(path: &Path) -> Result { - fs::create_dir_all(path).map_err(storage_error)?; - Command::new("explorer.exe") - .arg(path) - .spawn() - .map_err(|error| { - CommandError::new( - "open_config_failed", - format!("Не удалось открыть папку '{}': {error}", path.display()), - ) - })?; - - Ok(path.display().to_string()) -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -enum ServiceControlAction { - Start, - Stop, -} - -impl ServiceControlAction { - fn error_code(self) -> &'static str { - match self { - ServiceControlAction::Start => "proxifyre_service_start_failed", - ServiceControlAction::Stop => "proxifyre_service_stop_failed", - } - } - - fn label(self) -> &'static str { - match self { - ServiceControlAction::Start => "запустить", - ServiceControlAction::Stop => "остановить", - } - } -} - -fn control_proxifyre_service( - action: ServiceControlAction, -) -> Result { - let Some(detected) = detect_proxyfier_install() else { - return Err(CommandError::new( - "proxifyre_not_found", - "ProxiFyre не найден на компьютере.", - )); - }; - - run_proxifyre_service_command(action, &service_name_candidates(&detected))?; - let refreshed = detect_proxyfier_install(); - let component = proxyfier_component_from_detection(refreshed.as_ref()); - - Ok(ComponentStatusDto::from(&component)) -} - -fn service_name_candidates(detected: &DetectedProxyfier) -> Vec { - let mut names = Vec::new(); - - if let Some(service_name) = &detected.service_name { - names.push(service_name.clone()); - } - for service_name in ["ProxiFyreService", "ProxiFyre"] { - if !names - .iter() - .any(|existing| existing.eq_ignore_ascii_case(service_name)) - { - names.push(service_name.to_string()); - } - } - - names -} - -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -struct ServiceCommandOutput { - success: bool, - code: String, - service_name: Option, - status: Option, - process_id: Option, -} - -fn run_proxifyre_service_command( - action: ServiceControlAction, - service_names: &[String], -) -> Result<(), CommandError> { - let names = service_names - .iter() - .map(|name| format!("'{}'", escape_powershell_single(name))) - .collect::>() - .join(", "); - let action_name = match action { - ServiceControlAction::Start => "start", - ServiceControlAction::Stop => "stop", - }; - let script = format!( - r#" -$ErrorActionPreference = 'Stop' -$names = @({names}) -$action = '{action_name}' -$service = $null - -function Find-ProxiFyreService {{ - foreach ($name in $names) {{ - $candidate = Get-Service -Name $name -ErrorAction SilentlyContinue - if ($null -ne $candidate) {{ return $candidate }} - }} - - return Get-Service | - Where-Object {{ $_.Name -match 'ProxiFyre|Proxifyre' -or $_.DisplayName -match 'ProxiFyre|Proxifyre' }} | - Select-Object -First 1 -}} - -function Get-ServiceProcessId([string]$name) {{ - $escapedName = $name.Replace("'", "''") - $record = Get-CimInstance Win32_Service -Filter "Name='$escapedName'" -ErrorAction SilentlyContinue - if ($null -eq $record) {{ return 0 }} - return [int]$record.ProcessId -}} - -function Get-ServiceStatus([string]$name) {{ - $current = Get-Service -Name $name -ErrorAction SilentlyContinue - if ($null -eq $current) {{ return $null }} - return $current.Status.ToString() -}} - -function Write-ServiceResult([bool]$success, [string]$code, [string]$status, [int]$processId) {{ - [PSCustomObject]@{{ - success = $success - code = $code - serviceName = if ($null -ne $service) {{ $service.Name }} else {{ $null }} - status = $status - processId = $processId - }} | ConvertTo-Json -Compress - exit 0 -}} - -$service = Find-ProxiFyreService -if ($null -eq $service) {{ - Write-ServiceResult $false 'service_not_found' $null 0 -}} - -$status = $service.Status.ToString() -$processId = Get-ServiceProcessId $service.Name - -if ($action -eq 'start') {{ - if ($status -eq 'Running') {{ - Write-ServiceResult $true 'already_running' $status $processId - }} - - try {{ - Start-Service -Name $service.Name -ErrorAction Stop - $service = Get-Service -Name $service.Name - $service.WaitForStatus('Running', [TimeSpan]::FromSeconds(15)) - }} catch {{ - Write-ServiceResult $false 'start_failed' (Get-ServiceStatus $service.Name) (Get-ServiceProcessId $service.Name) - }} - - Write-ServiceResult ($service.Status -eq 'Running') 'started' $service.Status.ToString() (Get-ServiceProcessId $service.Name) -}} - -if ($status -eq 'Stopped') {{ - Write-ServiceResult $true 'already_stopped' $status $processId -}} - -try {{ - if ($service.CanStop) {{ - Stop-Service -Name $service.Name -Force -ErrorAction Stop - }} -}} catch {{}} - -try {{ - $service = Get-Service -Name $service.Name -ErrorAction SilentlyContinue - if ($null -ne $service -and $service.Status -ne 'Stopped') {{ - $null = & sc.exe stop $service.Name 2>$null - }} -}} catch {{}} - -try {{ - $service = Get-Service -Name $service.Name -ErrorAction SilentlyContinue - if ($null -ne $service -and $service.Status -ne 'Stopped') {{ - $service.WaitForStatus('Stopped', [TimeSpan]::FromSeconds(8)) - }} -}} catch {{}} - -$status = Get-ServiceStatus $service.Name -$processId = Get-ServiceProcessId $service.Name -if ($status -ne 'Stopped' -and $processId -gt 0) {{ - try {{ - $null = & taskkill.exe /PID $processId /F 2>$null - Start-Sleep -Milliseconds 700 - $service = Get-Service -Name $service.Name -ErrorAction SilentlyContinue - if ($null -ne $service) {{ - $service.WaitForStatus('Stopped', [TimeSpan]::FromSeconds(8)) - }} - }} catch {{}} -}} - -$status = Get-ServiceStatus $service.Name -$processId = Get-ServiceProcessId $service.Name -if ($status -eq 'Stopped') {{ - Write-ServiceResult $true 'stopped' $status $processId -}} - -Write-ServiceResult $false 'stop_failed' $status $processId -"# - ); - - let output = Command::new("powershell") - .args([ - "-NoProfile", - "-NonInteractive", - "-ExecutionPolicy", - "Bypass", - "-Command", - script.as_str(), - ]) - .output() - .map_err(|error| { - CommandError::new( - action.error_code(), - format!("Не удалось {} службу ProxiFyre: {error}", action.label()), - ) - })?; - - let result = parse_service_command_output(&output.stdout).ok_or_else(|| { - CommandError::new( - action.error_code(), - service_script_failed_message(action, output.status.code()), - ) - })?; - - if result.success { - return Ok(()); - } - - if matches!(result.code.as_str(), "start_failed" | "stop_failed") { - run_elevated_proxifyre_service_command(action, service_names, &result)?; - return Ok(()); - } - - Err(CommandError::new( - action.error_code(), - service_command_failed_message(action, &result), - )) -} - -fn run_elevated_proxifyre_service_command( - action: ServiceControlAction, - service_names: &[String], - direct_result: &ServiceCommandOutput, -) -> Result<(), CommandError> { - let script_path = write_elevated_service_script(action, service_names)?; - let launch_script = format!( - "$p = Start-Process -FilePath 'powershell.exe' -Verb RunAs -Wait -PassThru -WindowStyle Hidden -ArgumentList @('-NoProfile','-ExecutionPolicy','Bypass','-File','{}'); exit $p.ExitCode", - escape_powershell_single(&script_path.display().to_string()) - ); - let output = Command::new("powershell") - .args([ - "-NoProfile", - "-NonInteractive", - "-ExecutionPolicy", - "Bypass", - "-Command", - launch_script.as_str(), - ]) - .output(); - - let _ = fs::remove_file(&script_path); - - match output { - Ok(output) if output.status.success() => Ok(()), - Ok(output) => Err(CommandError::new( - action.error_code(), - elevated_service_failed_message(action, direct_result, output.status.code()), - )), - Err(error) => Err(CommandError::new( - action.error_code(), - format!( - "Не удалось запросить права администратора, чтобы {} службу ProxiFyre: {error}", - action.label() - ), - )), - } -} - -fn write_elevated_service_script( - action: ServiceControlAction, - service_names: &[String], -) -> Result { - let nonce = SystemTime::now() - .duration_since(UNIX_EPOCH) - .map(|duration| duration.as_millis()) - .unwrap_or(0); - let script_path = env::temp_dir().join(format!("vpn-proxy-proxifyre-service-{nonce}.ps1")); - let script = elevated_service_script(action, service_names); - - write_powershell_script(&script_path, &script).map_err(|error| { - CommandError::new( - action.error_code(), - format!( - "Не удалось подготовить временный скрипт для управления ProxiFyre '{}': {error}", - script_path.display() - ), - ) - })?; - - Ok(script_path) -} - -fn elevated_service_script(action: ServiceControlAction, service_names: &[String]) -> String { - let names = service_names - .iter() - .map(|name| format!("'{}'", escape_powershell_single(name))) - .collect::>() - .join(", "); - let action_name = match action { - ServiceControlAction::Start => "start", - ServiceControlAction::Stop => "stop", - }; - - format!( - r#" -$ErrorActionPreference = 'SilentlyContinue' -$names = @({names}) -$action = '{action_name}' -$service = $null - -foreach ($name in $names) {{ - $service = Get-Service -Name $name -ErrorAction SilentlyContinue - if ($null -ne $service) {{ break }} -}} - -if ($null -eq $service) {{ - $service = Get-Service | - Where-Object {{ $_.Name -match 'ProxiFyre|Proxifyre' -or $_.DisplayName -match 'ProxiFyre|Proxifyre' }} | - Select-Object -First 1 -}} - -if ($null -eq $service) {{ exit 2 }} - -function Get-ServiceProcessId([string]$name) {{ - $escapedName = $name.Replace("'", "''") - $record = Get-CimInstance Win32_Service -Filter "Name='$escapedName'" -ErrorAction SilentlyContinue - if ($null -eq $record) {{ return 0 }} - return [int]$record.ProcessId -}} - -if ($action -eq 'start') {{ - if ($service.Status -eq 'Running') {{ exit 0 }} - Start-Service -Name $service.Name -ErrorAction SilentlyContinue - $service = Get-Service -Name $service.Name -ErrorAction SilentlyContinue - if ($null -ne $service) {{ - try {{ $service.WaitForStatus('Running', [TimeSpan]::FromSeconds(15)) }} catch {{}} - if ($service.Status -eq 'Running') {{ exit 0 }} - }} - exit 3 -}} - -if ($service.Status -eq 'Stopped') {{ exit 0 }} - -if ($service.CanStop) {{ - Stop-Service -Name $service.Name -Force -ErrorAction SilentlyContinue -}} - -$service = Get-Service -Name $service.Name -ErrorAction SilentlyContinue -if ($null -ne $service -and $service.Status -ne 'Stopped') {{ - $null = & sc.exe stop $service.Name 2>$null -}} - -$service = Get-Service -Name $service.Name -ErrorAction SilentlyContinue -if ($null -ne $service -and $service.Status -ne 'Stopped') {{ - try {{ $service.WaitForStatus('Stopped', [TimeSpan]::FromSeconds(8)) }} catch {{}} -}} - -$service = Get-Service -Name $service.Name -ErrorAction SilentlyContinue -if ($null -ne $service -and $service.Status -ne 'Stopped') {{ - $processId = Get-ServiceProcessId $service.Name - if ($processId -gt 0) {{ - $null = & taskkill.exe /PID $processId /F 2>$null - Start-Sleep -Milliseconds 700 - $service = Get-Service -Name $service.Name -ErrorAction SilentlyContinue - if ($null -ne $service) {{ - try {{ $service.WaitForStatus('Stopped', [TimeSpan]::FromSeconds(8)) }} catch {{}} - }} - }} -}} - -$service = Get-Service -Name $service.Name -ErrorAction SilentlyContinue -if ($null -eq $service -or $service.Status -eq 'Stopped') {{ exit 0 }} -exit 4 -"# - ) -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -enum ProxiFyrePackageAction { - Install, - Uninstall, -} - -impl ProxiFyrePackageAction { - fn error_code(self) -> &'static str { - match self { - ProxiFyrePackageAction::Install => "proxifyre_install_failed", - ProxiFyrePackageAction::Uninstall => "proxifyre_uninstall_failed", - } - } - - fn label(self) -> &'static str { - match self { - ProxiFyrePackageAction::Install => "установить", - ProxiFyrePackageAction::Uninstall => "удалить", - } - } - - fn file_label(self) -> &'static str { - match self { - ProxiFyrePackageAction::Install => "install", - ProxiFyrePackageAction::Uninstall => "uninstall", - } - } -} - -fn install_proxifyre_component(storage: &JsonStorage) -> Result { - let generated_config_path = storage - .paths() - .generated_dir - .join("proxifyre-app-config.json"); - let script = install_proxifyre_script(&generated_config_path); - - run_elevated_package_script( - ProxiFyrePackageAction::Install, - script, - &storage.paths().state_dir, - )?; - - let refreshed = detect_proxyfier_install(); - let Some(detected) = refreshed.as_ref() else { - return Err(CommandError::new( - ProxiFyrePackageAction::Install.error_code(), - "Установка ProxiFyre завершилась, но приложение не найдено после проверки.", - )); - }; - - Ok(ComponentStatusDto::from( - &proxyfier_component_from_detection(Some(detected)), - )) -} - -fn uninstall_proxifyre_component() -> Result { - let Some(detected) = detect_proxyfier_install() else { - let component = proxyfier_component_from_detection(None); - return Ok(ComponentStatusDto::from(&component)); - }; - - ensure_safe_proxifyre_install_dir(&detected.install_dir)?; - let script = uninstall_proxifyre_script(&detected.install_dir, &detected.executable_path); - - let artifact_dir = default_config_root().join("state"); - run_elevated_package_script(ProxiFyrePackageAction::Uninstall, script, &artifact_dir)?; - - let refreshed = detect_proxyfier_install(); - if refreshed.is_some() { - return Err(CommandError::new( - ProxiFyrePackageAction::Uninstall.error_code(), - "Удаление ProxiFyre завершилось, но приложение все еще найдено на компьютере.", - )); - } - - let component = proxyfier_component_from_detection(None); - Ok(ComponentStatusDto::from(&component)) -} - -fn build_proxifyre_setup_status() -> ProxiFyreSetupStatusDto { - let vc_runtime = detect_vc_runtime(); - let packet_filter = detect_windows_packet_filter(); - let proxifyre = detect_proxyfier_install(); - - let vc_runtime_item = setup_item_from_program( - "vc-runtime", - &format!("Microsoft Visual C++ Runtime ({})", runtime_arch_label()), - vc_runtime, - "Нужен для запуска ProxiFyre.exe. Установщик скачает официальный vc_redist от Microsoft.", - ); - let packet_filter_item = setup_item_from_program( - "packet-filter", - "Windows Packet Filter", - packet_filter, - "Сетевой драйвер NT Kernel/WireSock, через который ProxiFyre перехватывает трафик приложений.", - ); - let proxifyre_item = match proxifyre { - Some(detected) => ProxiFyreSetupItemDto { - id: "proxifyre".to_string(), - name: "ProxiFyre".to_string(), - installed: true, - version: Some(if detected.running { - "служба запущена".to_string() - } else { - "служба не запущена".to_string() - }), - details: detected.install_dir.display().to_string(), - }, - None => ProxiFyreSetupItemDto { - id: "proxifyre".to_string(), - name: "ProxiFyre".to_string(), - installed: false, - version: None, - details: format!( - "Будет скачан с GitHub и распакован в {}.", - PROXIFYRE_INSTALL_DIR - ), - }, - }; - - let items = vec![vc_runtime_item, packet_filter_item, proxifyre_item]; - let missing_count = items.iter().filter(|item| !item.installed).count(); - - ProxiFyreSetupStatusDto { - ready: missing_count == 0, - missing_count, - items, - } -} - -fn setup_item_from_program( - id: &str, - name: &str, - program: Option, - missing_details: &str, -) -> ProxiFyreSetupItemDto { - match program { - Some(program) => ProxiFyreSetupItemDto { - id: id.to_string(), - name: name.to_string(), - installed: true, - version: program.display_version, - details: program.display_name, - }, - None => ProxiFyreSetupItemDto { - id: id.to_string(), - name: name.to_string(), - installed: false, - version: None, - details: missing_details.to_string(), - }, - } -} - -#[derive(Debug, Clone)] -struct InstalledProgram { - display_name: String, - display_version: Option, -} - -#[derive(Debug, Deserialize)] -#[serde(rename_all = "PascalCase")] -struct InstalledProgramJson { - display_name: Option, - display_version: Option, -} - -fn detect_vc_runtime() -> Option { - installed_program(&vc_runtime_registry_pattern()) -} - -fn detect_windows_packet_filter() -> Option { - installed_program("Windows Packet Filter|WinpkFilter|NDISAPI") -} - -fn installed_program(pattern: &str) -> Option { - let script = format!( - r#" -$paths = @( - 'HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*', - 'HKLM:\Software\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*', - 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*' -) -$program = Get-ItemProperty -Path $paths -ErrorAction SilentlyContinue | - Where-Object {{ $_.DisplayName -match '{}' }} | - Select-Object -First 1 DisplayName, DisplayVersion -if ($null -ne $program) {{ - $program | ConvertTo-Json -Compress -}} -"#, - escape_powershell_single(pattern) - ); - - let output = Command::new("powershell") - .args([ - "-NoProfile", - "-NonInteractive", - "-ExecutionPolicy", - "Bypass", - "-Command", - script.as_str(), - ]) - .output() - .ok()?; - if !output.status.success() { - return None; - } - - let stdout = String::from_utf8_lossy(&output.stdout); - let payload = stdout.trim(); - if payload.is_empty() || payload.eq_ignore_ascii_case("null") { - return None; - } - - let parsed: InstalledProgramJson = serde_json::from_str(payload).ok()?; - let display_name = parsed.display_name?.trim().to_string(); - if display_name.is_empty() { - return None; - } - - Some(InstalledProgram { - display_name, - display_version: parsed - .display_version - .map(|version| version.trim().to_string()) - .filter(|version| !version.is_empty()), - }) -} - -fn vc_runtime_registry_pattern() -> String { - let arch = runtime_arch_label(); - if arch == "ARM64" { - return r"Microsoft Visual C\+\+.*Redistributable.*\((ARM64|x64)\)".to_string(); - } - - format!(r"Microsoft Visual C\+\+.*Redistributable.*\({arch}\)") -} - -fn runtime_arch_label() -> &'static str { - if cfg!(target_arch = "aarch64") { - "ARM64" - } else if cfg!(target_arch = "x86") { - "x86" - } else { - "x64" - } -} - -fn run_elevated_package_script( - action: ProxiFyrePackageAction, - body: String, - artifact_dir: &Path, -) -> Result<(), CommandError> { - let nonce = SystemTime::now() - .duration_since(UNIX_EPOCH) - .map(|duration| duration.as_millis()) - .unwrap_or(0); - fs::create_dir_all(artifact_dir).map_err(|error| { - CommandError::new( - action.error_code(), - format!( - "Не удалось создать папку для временных файлов ProxiFyre '{}': {error}", - artifact_dir.display() - ), - ) - })?; - let script_path = artifact_dir.join(format!( - "vpn-proxy-proxifyre-{}-{nonce}.ps1", - action.file_label() - )); - let result_path = artifact_dir.join(format!( - "vpn-proxy-proxifyre-{}-{nonce}.log", - action.file_label() - )); - let script = wrap_elevated_package_script(&body, &result_path); - - write_powershell_script(&script_path, &script).map_err(|error| { - CommandError::new( - action.error_code(), - format!( - "Не удалось подготовить временный скрипт, чтобы {} ProxiFyre '{}': {error}", - action.label(), - script_path.display() - ), - ) - })?; - - let launch_script = format!( - r#" -$ErrorActionPreference = 'Stop' -$resultPath = '{}' -try {{ - $p = Start-Process -FilePath 'powershell.exe' -Verb RunAs -Wait -PassThru -WindowStyle Hidden -ArgumentList @('-NoProfile','-ExecutionPolicy','Bypass','-File','{}') - if ($null -eq $p) {{ - Set-Content -LiteralPath $resultPath -Value 'Elevated PowerShell не был запущен.' -Encoding UTF8 - exit 1 - }} - exit $p.ExitCode -}} catch {{ - Set-Content -LiteralPath $resultPath -Value ($_ | Out-String) -Encoding UTF8 - exit 1 -}} -"#, - escape_powershell_single(&result_path.display().to_string()), - escape_powershell_single(&script_path.display().to_string()) - ); - let output = Command::new("powershell") - .args([ - "-NoProfile", - "-NonInteractive", - "-ExecutionPolicy", - "Bypass", - "-Command", - launch_script.as_str(), - ]) - .output(); - - let _ = fs::remove_file(&script_path); - - match output { - Ok(output) if output.status.success() => { - let _ = fs::remove_file(&result_path); - Ok(()) - } - Ok(output) => { - let details = package_failure_details(&result_path, &output); - let _ = fs::remove_file(&result_path); - Err(CommandError::new( - action.error_code(), - format!( - "Не удалось {} ProxiFyre. Код elevated-команды: {}. {details}", - action.label(), - output.status.code().unwrap_or(-1), - ), - )) - } - Err(error) => Err(CommandError::new( - action.error_code(), - format!( - "Не удалось запросить права администратора, чтобы {} ProxiFyre: {error}", - action.label() - ), - )), - } -} - -pub(crate) fn wrap_elevated_package_script(body: &str, result_path: &Path) -> String { - let mut script = String::new(); - script.push_str("$ErrorActionPreference = 'Stop'\n"); - script.push_str(&format!( - "$resultPath = '{}'\n", - escape_powershell_single(&result_path.display().to_string()) - )); - script.push_str("try {\n"); - script.push_str(body); - script.push_str( - r#" - Set-Content -LiteralPath $resultPath -Value 'ok' -Encoding UTF8 - exit 0 -} catch { - $message = ($_ | Out-String) - Set-Content -LiteralPath $resultPath -Value $message -Encoding UTF8 - exit 1 -} -"#, - ); - - script -} - -fn write_powershell_script(path: &Path, script: &str) -> std::io::Result<()> { - let mut bytes = Vec::with_capacity(script.len() + 3); - bytes.extend_from_slice(&[0xEF, 0xBB, 0xBF]); - bytes.extend_from_slice(script.as_bytes()); - fs::write(path, bytes) -} - -pub(crate) fn install_proxifyre_script(generated_config_path: &Path) -> String { - let mut script = String::new(); - script.push_str(&format!( - "$targetDir = '{}'\n", - escape_powershell_single(PROXIFYRE_INSTALL_DIR) - )); - script.push_str(&format!( - "$generatedConfigPath = '{}'\n", - escape_powershell_single(&generated_config_path.display().to_string()) - )); - script.push_str(&format!( - "$proxifyreReleaseApi = '{}'\n", - escape_powershell_single(PROXIFYRE_RELEASE_API_URL) - )); - script.push_str(&format!( - "$ndisapiReleaseApi = '{}'\n", - escape_powershell_single(NDISAPI_RELEASE_API_URL) - )); - script.push_str(&format!( - "$vcRedistX64Url = '{}'\n", - escape_powershell_single(VC_REDIST_X64_URL) - )); - script.push_str(&format!( - "$vcRedistX86Url = '{}'\n", - escape_powershell_single(VC_REDIST_X86_URL) - )); - script.push_str( - r#" - [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 - - 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 'x64' } - return 'x86' - } - - function Invoke-Download([string]$uri, [string]$path) { - Invoke-WebRequest -UseBasicParsing -Uri $uri -OutFile $path -Headers @{ 'User-Agent' = 'vpn-proxy-windows-client' } - } - - function Select-Asset($assets, [string]$pattern, [string]$label) { - $asset = $assets | Where-Object { $_.name -match $pattern } | Select-Object -First 1 - if ($null -eq $asset) { throw "Не найден подходящий asset для $label ($pattern)." } - return $asset - } - - function Verify-AssetHash([string]$path, $asset) { - if ($asset.digest -match '^sha256:(.+)$') { - $expected = $Matches[1].ToLowerInvariant() - $actual = (Get-FileHash -LiteralPath $path -Algorithm SHA256).Hash.ToLowerInvariant() - if ($actual -ne $expected) { - throw "SHA256 не совпал для $($asset.name). Ожидалось $expected, получилось $actual." - } - } - } - - function Assert-ExitCode($process, [string]$label) { - if ($process.ExitCode -ne 0 -and $process.ExitCode -ne 3010) { - throw "$label завершился с кодом $($process.ExitCode)." - } - } - - function Get-InstalledProgram([string]$pattern) { - $paths = @( - 'HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*', - 'HKLM:\Software\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*', - 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*' - ) - return Get-ItemProperty -Path $paths -ErrorAction SilentlyContinue | - Where-Object { $_.DisplayName -match $pattern } | - Select-Object -First 1 - } - - function Test-VcRuntime([string]$arch) { - $pattern = if ($arch -eq 'ARM64') { - 'Microsoft Visual C\+\+.*Redistributable.*\((ARM64|x64)\)' - } else { - "Microsoft Visual C\+\+.*Redistributable.*\($arch\)" - } - - return $null -ne (Get-InstalledProgram $pattern) - } - - function Test-WindowsPacketFilter { - return $null -ne (Get-InstalledProgram 'Windows Packet Filter|WinpkFilter|NDISAPI') - } - - function Get-LogTail([string]$path) { - if (-not (Test-Path -LiteralPath $path)) { return '' } - return (Get-Content -LiteralPath $path -Tail 40 -ErrorAction SilentlyContinue) -join ' ' - } - - $arch = Get-NativeArchitecture - $workDir = Join-Path ([IO.Path]::GetTempPath()) 'vpn-proxy-proxifyre-install' - $extractDir = Join-Path $workDir 'proxifyre' - Remove-Item -LiteralPath $workDir -Recurse -Force -ErrorAction SilentlyContinue - New-Item -ItemType Directory -Force -Path $workDir, $extractDir, $targetDir | Out-Null - - if (-not (Test-VcRuntime $arch)) { - $vcRedistPath = Join-Path $workDir 'vc_redist.exe' - $vcRedistUrl = if ($arch -eq 'x86') { $vcRedistX86Url } else { $vcRedistX64Url } - Invoke-Download $vcRedistUrl $vcRedistPath - $vcProcess = Start-Process -FilePath $vcRedistPath -ArgumentList @('/install', '/quiet', '/norestart') -Wait -PassThru -WindowStyle Hidden - if ($vcProcess.ExitCode -ne 0 -and $vcProcess.ExitCode -ne 3010 -and $vcProcess.ExitCode -ne 1638 -and -not (Test-VcRuntime $arch)) { - throw "Visual C++ Runtime завершился с кодом $($vcProcess.ExitCode)." - } - } - - if (-not (Test-WindowsPacketFilter)) { - $ndisRelease = Invoke-RestMethod -Uri $ndisapiReleaseApi -Headers @{ 'User-Agent' = 'vpn-proxy-windows-client' } - $ndisPattern = if ($arch -eq 'ARM64') { 'ARM64\.msi$' } elseif ($arch -eq 'x86') { 'x86\.msi$' } else { 'x64\.msi$' } - $ndisAsset = Select-Asset $ndisRelease.assets $ndisPattern 'Windows Packet Filter' - $ndisPath = Join-Path $workDir $ndisAsset.name - $ndisLogPath = Join-Path $workDir 'windows-packet-filter-install.log' - Invoke-Download $ndisAsset.browser_download_url $ndisPath - Verify-AssetHash $ndisPath $ndisAsset - $ndisProcess = Start-Process -FilePath 'msiexec.exe' -ArgumentList @('/i', $ndisPath, '/qn', '/norestart', '/L*v', $ndisLogPath) -Wait -PassThru -WindowStyle Hidden - if ($ndisProcess.ExitCode -ne 0 -and $ndisProcess.ExitCode -ne 3010 -and -not (Test-WindowsPacketFilter)) { - $ndisLogTail = Get-LogTail $ndisLogPath - throw "Windows Packet Filter завершился с кодом $($ndisProcess.ExitCode). MSI log: $ndisLogPath $ndisLogTail" - } - } - - $proxifyreRelease = Invoke-RestMethod -Uri $proxifyreReleaseApi -Headers @{ 'User-Agent' = 'vpn-proxy-windows-client' } - $proxifyrePattern = if ($arch -eq 'ARM64') { 'ARM64-signed\.zip$' } elseif ($arch -eq 'x86') { 'x86-signed\.zip$' } else { 'x64-signed\.zip$' } - $proxifyreAsset = Select-Asset $proxifyreRelease.assets $proxifyrePattern 'ProxiFyre' - $proxifyreZipPath = Join-Path $workDir $proxifyreAsset.name - Invoke-Download $proxifyreAsset.browser_download_url $proxifyreZipPath - Verify-AssetHash $proxifyreZipPath $proxifyreAsset - - Expand-Archive -LiteralPath $proxifyreZipPath -DestinationPath $extractDir -Force - $proxifyreExe = Get-ChildItem -LiteralPath $extractDir -Recurse -Filter 'ProxiFyre.exe' | Select-Object -First 1 - if ($null -eq $proxifyreExe) { throw 'В архиве ProxiFyre не найден ProxiFyre.exe.' } - - Copy-Item -Path (Join-Path $proxifyreExe.Directory.FullName '*') -Destination $targetDir -Recurse -Force - - $configTarget = Join-Path $targetDir 'app-config.json' - if (Test-Path -LiteralPath $generatedConfigPath) { - Copy-Item -LiteralPath $generatedConfigPath -Destination $configTarget -Force - } elseif (-not (Test-Path -LiteralPath $configTarget)) { - $emptyConfig = '{"logLevel":"Info","bypassLan":true,"proxies":[]}' - Set-Content -LiteralPath $configTarget -Value $emptyConfig -Encoding UTF8 - } - - Push-Location $targetDir - try { - & .\ProxiFyre.exe stop | Out-Null - & .\ProxiFyre.exe uninstall | Out-Null - & .\ProxiFyre.exe install - if ($LASTEXITCODE -ne 0) { throw "ProxiFyre.exe install завершился с кодом $LASTEXITCODE." } - & .\ProxiFyre.exe start - if ($LASTEXITCODE -ne 0) { - Start-Service -Name 'ProxiFyreService' -ErrorAction Stop - } - } finally { - Pop-Location - } -"#, - ); - - script -} - -fn uninstall_proxifyre_script(install_dir: &Path, executable_path: &Path) -> String { - let mut script = String::new(); - script.push_str(&format!( - "$installDir = '{}'\n", - escape_powershell_single(&install_dir.display().to_string()) - )); - script.push_str(&format!( - "$exePath = '{}'\n", - escape_powershell_single(&executable_path.display().to_string()) - )); - script.push_str( - r#" - function Find-ProxiFyreService { - foreach ($name in @('ProxiFyreService', 'ProxiFyre')) { - $candidate = Get-Service -Name $name -ErrorAction SilentlyContinue - if ($null -ne $candidate) { return $candidate } - } - - return Get-Service | - Where-Object { $_.Name -match 'ProxiFyre|Proxifyre' -or $_.DisplayName -match 'ProxiFyre|Proxifyre' } | - Select-Object -First 1 - } - - function Get-ServiceProcessId([string]$name) { - $escapedName = $name.Replace("'", "''") - $record = Get-CimInstance Win32_Service -Filter "Name='$escapedName'" -ErrorAction SilentlyContinue - if ($null -eq $record) { return 0 } - return [int]$record.ProcessId - } - - $service = Find-ProxiFyreService - if ($null -ne $service -and $service.Status -ne 'Stopped') { - try { - if ($service.CanStop) { Stop-Service -Name $service.Name -Force -ErrorAction SilentlyContinue } - $service = Get-Service -Name $service.Name -ErrorAction SilentlyContinue - if ($null -ne $service) { $service.WaitForStatus('Stopped', [TimeSpan]::FromSeconds(8)) } - } catch {} - } - - $service = Find-ProxiFyreService - if ($null -ne $service -and $service.Status -ne 'Stopped') { - $processId = Get-ServiceProcessId $service.Name - if ($processId -gt 0) { - taskkill.exe /PID $processId /F | Out-Null - Start-Sleep -Milliseconds 700 - } - } - - if (Test-Path -LiteralPath $exePath) { - Push-Location (Split-Path -Parent $exePath) - try { - & $exePath uninstall | Out-Null - } finally { - Pop-Location - } - } - - $service = Find-ProxiFyreService - if ($null -ne $service) { - sc.exe delete $service.Name | Out-Null - } - - Get-Process -Name 'ProxiFyre' -ErrorAction SilentlyContinue | Stop-Process -Force -ErrorAction SilentlyContinue - - if (Test-Path -LiteralPath $installDir) { - Remove-Item -LiteralPath $installDir -Recurse -Force - } -"#, - ); - - script -} - -fn ensure_safe_proxifyre_install_dir(path: &Path) -> Result<(), CommandError> { - let name = path - .file_name() - .and_then(|value| value.to_str()) - .map(|value| value.to_ascii_lowercase()) - .unwrap_or_default(); - - if path.parent().is_some() && name.contains("proxifyre") { - return Ok(()); - } - - Err(CommandError::new( - ProxiFyrePackageAction::Uninstall.error_code(), - format!( - "Отказываюсь рекурсивно удалять папку ProxiFyre с небезопасным путем: {}", - path.display() - ), - )) -} - -fn apply_to_detected_proxyfier( - request: HelperApplyRequest<'_>, - detected: &DetectedProxyfier, -) -> Result { - let Some(config_path) = &detected.config_path else { - return staged_apply_result(request); - }; - - if let Some(parent) = config_path.parent() { - fs::create_dir_all(parent).map_err(|error| { - CommandError::new( - "proxyfier_apply_failed", - format!( - "Не удалось создать папку конфига ProxiFyre '{}': {error}", - parent.display() - ), - ) - })?; - } - - if config_path.exists() { - let backup_path = config_path.with_file_name(format!( - "{}.bak", - config_path - .file_name() - .and_then(|value| value.to_str()) - .unwrap_or("app-config.json") - )); - fs::copy(config_path, backup_path).map_err(|error| { - CommandError::new( - "proxyfier_apply_failed", - format!( - "Не удалось создать backup текущего конфига ProxiFyre '{}': {error}", - config_path.display() - ), - ) - })?; - } - - fs::write(config_path, request.config_contents).map_err(|error| { - CommandError::new( - "proxyfier_apply_failed", - format!( - "Не удалось записать конфиг ProxiFyre '{}': {error}", - config_path.display() - ), - ) - })?; - - Ok(HelperApplyResult { - success: true, - changed: true, - action: "proxifyre.apply-detected-config".to_string(), - message: format!( - "Сгенерированный конфиг записан в найденную установку ProxiFyre: {}", - config_path.display() - ), - }) -} - -fn staged_apply_result(request: HelperApplyRequest<'_>) -> Result { - Ok(HelperApplyResult { - success: true, - changed: true, - action: format!("{}.stage-generated-config", request.adapter_id), - message: format!( - "Сгенерированный конфиг подготовлен в {}; совместимая установка ProxiFyre не найдена", - request.config_path.display() - ), - }) -} - -fn activity_for_apply( - clock: &impl Clock, - generated: &ProxyRouterGeneratedConfig, - generated_path: &Path, - helper_result: &HelperApplyResult, -) -> ActivityEntry { - let level = if helper_result.success { - ActivityLevel::Success - } else { - ActivityLevel::Error - }; - - ActivityEntry { - id: format!("apply-{}", generated.adapter_id), - at: clock.now(), - level, - title: "Конфиг ProxiFyre создан".to_string(), - message: format!( - "Профилей: {}, приложений: {}, конфиг: {}", - generated.enabled_profiles, - generated.routed_apps, - generated_path.display() - ), - } -} - -fn activity_for_apply_error(clock: &impl Clock, error: &CommandError) -> ActivityEntry { - ActivityEntry { - id: format!("apply-error-{}", error.code), - at: clock.now(), - level: ActivityLevel::Error, - title: "Применение ProxiFyre заблокировано".to_string(), - message: error.message.clone(), - } -} - -fn storage_error(error: std::io::Error) -> CommandError { - CommandError::new("storage_error", error.to_string()) -} - -fn background_task_error(error: impl std::fmt::Display) -> CommandError { - CommandError::new( - "background_task_failed", - format!("Фоновая проверка не завершилась: {error}"), - ) -} - -fn package_failure_details(result_path: &Path, output: &Output) -> String { - let mut parts = Vec::new(); - - if let Ok(contents) = fs::read_to_string(result_path) { - let details = compact_error_text(&contents); - if !details.is_empty() && !details.eq_ignore_ascii_case("ok") { - parts.push(details); - } - } - - let stdout = String::from_utf8_lossy(&output.stdout); - let stdout = compact_error_text(&stdout); - if !stdout.is_empty() { - parts.push(format!("stdout: {stdout}")); - } - - let stderr = String::from_utf8_lossy(&output.stderr); - let stderr = compact_error_text(&stderr); - if !stderr.is_empty() { - parts.push(format!("stderr: {stderr}")); - } - - if parts.is_empty() { - parts.push( - "Лог elevated-скрипта не создан. Обычно это значит, что окно UAC было отменено или Windows не дала запустить elevated PowerShell." - .to_string(), - ); - } - - parts.join(" ") -} - -fn compact_error_text(value: &str) -> String { - let text = value - .lines() - .map(str::trim) - .filter(|line| !line.is_empty()) - .collect::>() - .join(" "); - - let max_chars = 1400; - if text.chars().count() <= max_chars { - return text; - } - - let truncated = text.chars().take(max_chars).collect::(); - format!("{truncated}...") -} - -fn parse_service_command_output(stdout: &[u8]) -> Option { - let stdout = String::from_utf8_lossy(stdout); - let payload = stdout - .lines() - .rev() - .map(str::trim) - .find(|line| line.starts_with('{') && line.ends_with('}'))?; - - serde_json::from_str(payload).ok() -} - -fn service_script_failed_message(action: ServiceControlAction, exit_code: Option) -> String { - let exit_code = exit_code - .map(|code| format!(" Код выхода PowerShell: {code}.")) - .unwrap_or_default(); - - format!( - "Не удалось {} службу ProxiFyre: команда управления службой не вернула корректный результат.{exit_code}", - action.label() - ) -} - -fn service_command_failed_message( - action: ServiceControlAction, - result: &ServiceCommandOutput, -) -> String { - let service_name = result - .service_name - .as_deref() - .filter(|value| !value.trim().is_empty()) - .unwrap_or("ProxiFyre"); - let status = result - .status - .as_deref() - .filter(|value| !value.trim().is_empty()) - .unwrap_or("неизвестен"); - let pid = result - .process_id - .filter(|value| *value > 0) - .map(|value| format!(", PID: {value}")) - .unwrap_or_default(); - - match result.code.as_str() { - "service_not_found" => "Служба ProxiFyre не найдена.".to_string(), - "start_failed" => format!( - "Не удалось запустить службу {service_name}. Текущий статус: {status}{pid}. Попробуй запустить приложение от имени администратора." - ), - "stop_failed" => format!( - "Не удалось остановить службу {service_name} даже после принудительной попытки. Текущий статус: {status}{pid}. Запусти приложение от имени администратора или останови службу вручную в services.msc." - ), - _ => format!( - "Не удалось {} службу {service_name}. Текущий статус: {status}{pid}.", - action.label() - ), - } -} - -fn elevated_service_failed_message( - action: ServiceControlAction, - direct_result: &ServiceCommandOutput, - exit_code: Option, -) -> String { - let service_name = direct_result - .service_name - .as_deref() - .filter(|value| !value.trim().is_empty()) - .unwrap_or("ProxiFyre"); - let status = direct_result - .status - .as_deref() - .filter(|value| !value.trim().is_empty()) - .unwrap_or("неизвестен"); - let pid = direct_result - .process_id - .filter(|value| *value > 0) - .map(|value| format!(", PID: {value}")) - .unwrap_or_default(); - let exit_code = exit_code - .map(|code| format!(" Код elevated-команды: {code}.")) - .unwrap_or_default(); - - format!( - "Не удалось {} службу {service_name} даже после запроса прав администратора. До запроса UAC статус был: {status}{pid}.{exit_code} Если появлялось окно UAC, проверь, что оно было подтверждено.", - action.label() - ) -} - -fn escape_powershell_single(value: &str) -> String { - value.replace('\'', "''") -} - -fn validation_error(errors: Vec) -> CommandError { - CommandError::with_details( - "validation_error", - "Проверка введенных данных не прошла", - errors - .into_iter() - .map(|error| ValidationIssue { - field: error.field, - message: error.message, - }) - .collect(), - ) -} - -fn adapter_error(error: ProxyRouterError) -> CommandError { - let code = match error.kind { - ProxyRouterErrorKind::EmptyProfileItems => "empty_profile_items", - ProxyRouterErrorKind::MissingTarget => "missing_target", - ProxyRouterErrorKind::MissingRequiredComponent => "missing_required_component", - ProxyRouterErrorKind::RequiredComponentNotRunning => "required_component_not_running", - ProxyRouterErrorKind::UnsupportedTargetProtocol => "unsupported_target_protocol", - ProxyRouterErrorKind::Serialization => "serialization_error", - }; - - CommandError::new(code, error.message) -} - -impl From for ProfileInput { - fn from(input: ProfileInputDto) -> Self { - Self { - id: input.id, - name: input.name, - enabled: input.enabled.unwrap_or(true), - target_id: input - .target_id - .unwrap_or_else(|| "local-singbox".to_string()), - protocols: input - .protocols - .unwrap_or_else(|| vec!["TCP".to_string(), "UDP".to_string()]), - items: input - .items - .unwrap_or_default() - .into_iter() - .map(ProfileItemInput::from) - .collect(), - } - } -} - -impl From for ProfileItemInput { - fn from(input: ProfileItemInputDto) -> Self { - Self { - item_type: input.item_type, - value: input.value, - recursive: input.recursive, - } - } -} - -impl From for TargetInput { - fn from(input: TargetInputDto) -> Self { - Self { - id: input.id, - name: input.name, - kind: input.kind.unwrap_or_else(|| "external".to_string()), - protocol: input.protocol.unwrap_or_else(|| "socks5".to_string()), - host: input.host, - port: input.port, - requires_component: input.requires_component, - } - } -} - -impl From<&Profile> for ProfileDto { - fn from(profile: &Profile) -> Self { - Self { - id: profile.id.clone(), - name: profile.name.clone(), - enabled: profile.enabled, - target_id: profile.target_id.clone(), - protocols: profile.protocols.clone(), - items: profile.items.iter().map(ProfileItemDto::from).collect(), - } - } -} - -impl From<&ProfileItem> for ProfileItemDto { - fn from(item: &ProfileItem) -> Self { - Self { - item_type: item.item_type.clone(), - value: item.value.clone(), - recursive: item.recursive, - } - } -} - -impl From<&Target> for TargetDto { - fn from(target: &Target) -> Self { - Self { - id: target.id.clone(), - name: target.name.clone(), - kind: target.kind.clone(), - protocol: target.protocol.clone(), - host: target.host.clone(), - port: target.port, - requires_component: target.requires_component.clone(), - } - } -} - -impl From<&ComponentStatus> for ComponentStatusDto { - fn from(component: &ComponentStatus) -> Self { - Self { - id: component.id.clone(), - name: component.name.clone(), - state: component.state.clone(), - installed: component.installed, - running: component.running, - version: component.version.clone(), - path: component.path.clone(), - problems: component.problems.clone(), - actions: component.actions.clone(), - } - } -} - -impl From<&ActivityEntry> for ActivityEntryDto { - fn from(entry: &ActivityEntry) -> Self { - Self { - id: entry.id.clone(), - at: entry.at.clone(), - level: entry.level.clone(), - title: entry.title.clone(), - message: entry.message.clone(), - } - } -} - -impl From<&LocalSingBoxConfig> for LocalSingBoxConfigDto { - fn from(config: &LocalSingBoxConfig) -> Self { - Self { - subscription_display_url: config.subscription_display_url(), - has_subscription: config - .subscription_url - .as_deref() - .is_some_and(|value| !value.trim().is_empty()), - selected_server_tag: config.selected_server_tag.clone(), - listen_host: config.listen_host.clone(), - listen_port: config.listen_port, - service_name: config.service_name.clone(), - install_root: config.install_root.clone(), - updated_at: config.updated_at.clone(), - } - } -} - -impl From<&SubscriptionCache> for SubscriptionCacheDto { - fn from(cache: &SubscriptionCache) -> Self { - Self { - servers: cache - .servers - .iter() - .map(SubscriptionServerDto::from) - .collect(), - user_info: cache.user_info.clone(), - fetched_at: cache.fetched_at.clone(), - } - } -} - -impl From<&SubscriptionServer> for SubscriptionServerDto { - fn from(server: &SubscriptionServer) -> Self { - Self { - tag: server.tag.clone(), - server_type: server.server_type.clone(), - server: server.server.clone(), - server_port: server.server_port, - } - } -} diff --git a/apps/windows-client/src-tauri/src/component_detection.rs b/apps/windows-client/src-tauri/src/component_detection.rs deleted file mode 100644 index 878dbe5..0000000 --- a/apps/windows-client/src-tauri/src/component_detection.rs +++ /dev/null @@ -1,564 +0,0 @@ -use crate::models::{ - ComponentId, ComponentState, ComponentStatus, DEFAULT_LOCAL_SINGBOX_INSTALL_ROOT, - DEFAULT_LOCAL_SINGBOX_SERVICE_NAME, -}; -use serde::Deserialize; -use std::{ - env, - path::{Path, PathBuf}, - process::Command, -}; - -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum ProxyfierEngine { - ProxiFyre, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct DetectedProxyfier { - pub engine: ProxyfierEngine, - pub name: String, - pub install_dir: PathBuf, - pub executable_path: PathBuf, - pub config_path: Option, - pub running: bool, - pub service_name: Option, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct DetectedSingBox { - pub install_dir: PathBuf, - pub executable_path: PathBuf, - pub wrapper_path: PathBuf, - pub binary_exists: bool, - pub wrapper_exists: bool, - pub running: bool, - pub service_name: String, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct RegistryInstallEntry { - pub display_name: String, - pub install_location: Option, - pub display_icon: Option, -} - -pub trait ProxyfierDetectionHost { - fn env_var(&self, name: &str) -> Option; - - fn path_exists(&self, path: &Path) -> bool; - - fn process_running(&self, process_name: &str) -> bool; - - fn service_running(&self, service_name: &str) -> bool; - - fn registry_install_entries(&self) -> Vec; -} - -#[derive(Debug, Clone, Copy, Default)] -pub struct SystemProxyfierDetectionHost; - -impl ProxyfierDetectionHost for SystemProxyfierDetectionHost { - fn env_var(&self, name: &str) -> Option { - env::var(name).ok().filter(|value| !value.trim().is_empty()) - } - - fn path_exists(&self, path: &Path) -> bool { - path.exists() - } - - fn process_running(&self, process_name: &str) -> bool { - let process_name = process_name.trim_end_matches(".exe"); - let script = format!( - "if (Get-Process -Name '{}' -ErrorAction SilentlyContinue) {{ 'true' }} else {{ 'false' }}", - escape_powershell_single(process_name) - ); - - powershell_bool(&script) - } - - fn service_running(&self, service_name: &str) -> bool { - let script = format!( - "$s = Get-Service -Name '{}' -ErrorAction SilentlyContinue; if ($s -and $s.Status -eq 'Running') {{ 'true' }} else {{ 'false' }}", - escape_powershell_single(service_name) - ); - - powershell_bool(&script) - } - - fn registry_install_entries(&self) -> Vec { - read_registry_install_entries() - } -} - -pub fn detect_proxyfier_install() -> Option { - detect_proxyfier_install_with_host(&SystemProxyfierDetectionHost) -} - -pub fn detect_proxyfier_install_with_host( - host: &impl ProxyfierDetectionHost, -) -> Option { - let proxifyre_running = host.process_running("ProxiFyre.exe") - || host.service_running("ProxiFyreService") - || host.service_running("ProxiFyre"); - - proxyfier_candidates(host) - .into_iter() - .filter_map(|candidate| candidate.into_detected(host, proxifyre_running)) - .next() -} - -pub fn proxyfier_component_from_detection(detected: Option<&DetectedProxyfier>) -> ComponentStatus { - match detected { - Some(proxyfier) => detected_proxyfier_component(proxyfier), - None => missing_proxyfier_component(), - } -} - -pub fn detect_singbox_install() -> Option { - detect_singbox_install_with_host(&SystemProxyfierDetectionHost) -} - -pub fn detect_singbox_install_with_host( - host: &impl ProxyfierDetectionHost, -) -> Option { - let running = host.process_running("sing-box.exe") - || host.service_running(DEFAULT_LOCAL_SINGBOX_SERVICE_NAME); - - singbox_candidates(host) - .into_iter() - .filter_map(|install_dir| detected_singbox_from_dir(host, install_dir, running)) - .next() -} - -pub fn singbox_component_from_detection(detected: Option<&DetectedSingBox>) -> ComponentStatus { - match detected { - Some(singbox) => detected_singbox_component(singbox), - None => missing_singbox_component(), - } -} - -fn detected_proxyfier_component(proxyfier: &DetectedProxyfier) -> ComponentStatus { - let state = if proxyfier.running { - ComponentState::Running - } else { - ComponentState::Installed - }; - let actions = match proxyfier.engine { - ProxyfierEngine::ProxiFyre => { - if proxyfier.running { - vec![ - "Применить сгенерированный конфиг".to_string(), - "Открыть папку конфига".to_string(), - "Остановить".to_string(), - ] - } else { - vec![ - "Применить сгенерированный конфиг".to_string(), - "Открыть папку конфига".to_string(), - "Запустить".to_string(), - ] - } - } - }; - - ComponentStatus { - id: ComponentId::Proxyfier, - name: "ProxiFyre".to_string(), - state, - installed: true, - running: proxyfier.running, - version: Some(match proxyfier.engine { - ProxyfierEngine::ProxiFyre => "ProxiFyre найден".to_string(), - }), - path: Some(proxyfier.install_dir.display().to_string()), - problems: Vec::new(), - actions, - } -} - -fn missing_proxyfier_component() -> ComponentStatus { - ComponentStatus { - id: ComponentId::Proxyfier, - name: "ProxiFyre".to_string(), - state: ComponentState::Missing, - installed: false, - running: false, - version: None, - path: None, - problems: vec!["ProxiFyre нужен для маршрутизации выбранных приложений".to_string()], - actions: vec!["Установить ProxiFyre".to_string()], - } -} - -fn detected_singbox_component(singbox: &DetectedSingBox) -> ComponentStatus { - let state = if singbox.running { - ComponentState::Running - } else { - ComponentState::Stopped - }; - let actions = if singbox.running { - vec![ - "Сгенерировать конфиг".to_string(), - "Остановить".to_string(), - "Открыть папку".to_string(), - ] - } else { - vec![ - "Сгенерировать конфиг".to_string(), - "Запустить".to_string(), - "Открыть папку".to_string(), - ] - }; - let problems = if singbox.running { - Vec::new() - } else { - vec!["Служба Local sing-box остановлена".to_string()] - }; - - ComponentStatus { - id: ComponentId::Singbox, - name: "Local sing-box".to_string(), - state, - installed: true, - running: singbox.running, - version: Some("sing-box найден".to_string()), - path: Some(singbox.executable_path.display().to_string()), - problems, - actions, - } -} - -fn missing_singbox_component() -> ComponentStatus { - ComponentStatus { - id: ComponentId::Singbox, - name: "Local sing-box".to_string(), - state: ComponentState::Missing, - installed: false, - running: false, - version: None, - path: None, - problems: Vec::new(), - actions: vec!["Установить Local sing-box".to_string()], - } -} - -#[derive(Debug, Clone, PartialEq, Eq)] -struct ProxyfierCandidate { - engine: ProxyfierEngine, - name: String, - install_dir: PathBuf, -} - -impl ProxyfierCandidate { - fn into_detected( - self, - host: &impl ProxyfierDetectionHost, - proxifyre_running: bool, - ) -> Option { - let executable_path = self.install_dir.join(executable_name(&self.engine)); - let config_path = config_path(&self.engine, &self.install_dir); - let exists = host.path_exists(&self.install_dir) - || host.path_exists(&executable_path) - || config_path - .as_ref() - .is_some_and(|path| host.path_exists(path)); - - if !exists { - return None; - } - - Some(DetectedProxyfier { - service_name: service_name(&self.engine).map(str::to_string), - engine: self.engine, - name: self.name, - install_dir: self.install_dir, - executable_path, - config_path, - running: proxifyre_running, - }) - } -} - -fn proxyfier_candidates(host: &impl ProxyfierDetectionHost) -> Vec { - let mut candidates = Vec::new(); - - push_env_candidate( - &mut candidates, - host, - ProxyfierEngine::ProxiFyre, - "ProxiFyre", - "VPN_PROXY_PROXIFYRE_ROOT", - ); - for entry in host.registry_install_entries() { - if let Some(engine) = engine_from_name(&entry.display_name) { - let install_dir = entry - .install_location - .or_else(|| entry.display_icon.and_then(|path| executable_parent(&path))); - if let Some(install_dir) = install_dir { - push_candidate( - &mut candidates, - ProxyfierCandidate { - name: entry.display_name, - engine, - install_dir, - }, - ); - } - } - } - - for install_dir in common_install_dirs(host, "ProxiFyre") { - push_candidate( - &mut candidates, - ProxyfierCandidate { - engine: ProxyfierEngine::ProxiFyre, - name: "ProxiFyre".to_string(), - install_dir, - }, - ); - } - candidates -} - -fn push_env_candidate( - candidates: &mut Vec, - host: &impl ProxyfierDetectionHost, - engine: ProxyfierEngine, - name: &str, - env_name: &str, -) { - if let Some(path) = host.env_var(env_name) { - push_candidate( - candidates, - ProxyfierCandidate { - engine, - name: name.to_string(), - install_dir: PathBuf::from(path), - }, - ); - } -} - -fn push_candidate(candidates: &mut Vec, candidate: ProxyfierCandidate) { - if !candidates.iter().any(|existing| { - existing.engine == candidate.engine - && same_path(&existing.install_dir, &candidate.install_dir) - }) { - candidates.push(candidate); - } -} - -fn common_install_dirs(host: &impl ProxyfierDetectionHost, folder_name: &str) -> Vec { - let mut dirs = vec![PathBuf::from(format!(r"C:\Tools\{folder_name}"))]; - - for env_name in ["ProgramFiles", "ProgramFiles(x86)", "LOCALAPPDATA"] { - if let Some(root) = host.env_var(env_name) { - dirs.push(PathBuf::from(root).join(folder_name)); - } - } - - dirs -} - -fn singbox_candidates(host: &impl ProxyfierDetectionHost) -> Vec { - let mut candidates = Vec::new(); - - if let Some(path) = host.env_var("VPN_PROXY_SINGBOX_ROOT") { - push_path_candidate(&mut candidates, PathBuf::from(path)); - } - push_path_candidate( - &mut candidates, - PathBuf::from(DEFAULT_LOCAL_SINGBOX_INSTALL_ROOT), - ); - push_path_candidate( - &mut candidates, - PathBuf::from(r"C:\Tools\VpnProxy\sing-box"), - ); - for env_name in ["ProgramFiles", "ProgramFiles(x86)", "LOCALAPPDATA"] { - if let Some(root) = host.env_var(env_name) { - push_path_candidate( - &mut candidates, - PathBuf::from(&root).join("VpnProxy").join("sing-box"), - ); - push_path_candidate(&mut candidates, PathBuf::from(root).join("sing-box")); - } - } - - candidates -} - -fn push_path_candidate(candidates: &mut Vec, candidate: PathBuf) { - if !candidates - .iter() - .any(|existing| same_path(existing, &candidate)) - { - candidates.push(candidate); - } -} - -fn detected_singbox_from_dir( - host: &impl ProxyfierDetectionHost, - install_dir: PathBuf, - running: bool, -) -> Option { - let executable_path = install_dir.join("sing-box.exe"); - let wrapper_path = install_dir.join("VpnProxySingBox.exe"); - let binary_exists = host.path_exists(&executable_path); - let wrapper_exists = host.path_exists(&wrapper_path); - let exists = host.path_exists(&install_dir) || binary_exists || wrapper_exists; - - if !exists { - return None; - } - - Some(DetectedSingBox { - install_dir, - executable_path, - wrapper_path, - binary_exists, - wrapper_exists, - running, - service_name: DEFAULT_LOCAL_SINGBOX_SERVICE_NAME.to_string(), - }) -} - -fn executable_name(engine: &ProxyfierEngine) -> &'static str { - match engine { - ProxyfierEngine::ProxiFyre => "ProxiFyre.exe", - } -} - -fn config_path(engine: &ProxyfierEngine, install_dir: &Path) -> Option { - match engine { - ProxyfierEngine::ProxiFyre => Some(install_dir.join("app-config.json")), - } -} - -fn service_name(engine: &ProxyfierEngine) -> Option<&'static str> { - match engine { - ProxyfierEngine::ProxiFyre => Some("ProxiFyreService"), - } -} - -fn engine_from_name(name: &str) -> Option { - let normalized = name.to_ascii_lowercase(); - if normalized.contains("proxifyre") { - Some(ProxyfierEngine::ProxiFyre) - } else { - None - } -} - -fn executable_parent(path: &Path) -> Option { - path.parent().map(Path::to_path_buf) -} - -fn same_path(left: &Path, right: &Path) -> bool { - left.to_string_lossy() - .eq_ignore_ascii_case(&right.to_string_lossy()) -} - -fn powershell_bool(script: &str) -> bool { - Command::new("powershell") - .args(["-NoProfile", "-NonInteractive", "-Command", script]) - .output() - .ok() - .and_then(|output| String::from_utf8(output.stdout).ok()) - .is_some_and(|stdout| stdout.trim().eq_ignore_ascii_case("true")) -} - -#[derive(Debug, Deserialize)] -#[serde(rename_all = "PascalCase")] -struct RegistryInstallJson { - display_name: Option, - install_location: Option, - display_icon: Option, -} - -fn read_registry_install_entries() -> Vec { - let script = r#" -$paths = @( - 'HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*', - 'HKLM:\Software\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*', - 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*' -) -$items = foreach ($path in $paths) { - Get-ItemProperty -Path $path -ErrorAction SilentlyContinue -} -$items | - Where-Object { $_.DisplayName -match 'ProxiFyre' } | - Select-Object DisplayName,InstallLocation,DisplayIcon | - ConvertTo-Json -Compress -"#; - - let Ok(output) = Command::new("powershell") - .args(["-NoProfile", "-NonInteractive", "-Command", script]) - .output() - else { - return Vec::new(); - }; - - if !output.status.success() { - return Vec::new(); - } - - let Ok(stdout) = String::from_utf8(output.stdout) else { - return Vec::new(); - }; - let stdout = stdout.trim(); - if stdout.is_empty() { - return Vec::new(); - } - - parse_registry_json(stdout) -} - -fn parse_registry_json(json: &str) -> Vec { - let Ok(value) = serde_json::from_str::(json) else { - return Vec::new(); - }; - - match value { - serde_json::Value::Array(entries) => entries - .into_iter() - .filter_map(registry_entry_from_value) - .collect(), - entry => registry_entry_from_value(entry).into_iter().collect(), - } -} - -fn registry_entry_from_value(value: serde_json::Value) -> Option { - let parsed = serde_json::from_value::(value).ok()?; - let display_name = parsed.display_name?; - Some(RegistryInstallEntry { - display_name, - install_location: parsed - .install_location - .filter(|value| !value.trim().is_empty()) - .map(PathBuf::from), - display_icon: parsed - .display_icon - .and_then(|value| display_icon_path(&value)), - }) -} - -fn display_icon_path(value: &str) -> Option { - let trimmed = value.trim().trim_matches('"'); - if trimmed.is_empty() { - return None; - } - - let without_icon_index = trimmed - .split_once(',') - .map(|(path, _)| path) - .unwrap_or(trimmed) - .trim() - .trim_matches('"'); - - Some(PathBuf::from(without_icon_index)) -} - -fn escape_powershell_single(value: &str) -> String { - value.replace('\'', "''") -} diff --git a/apps/windows-client/src-tauri/src/helper.rs b/apps/windows-client/src-tauri/src/helper.rs deleted file mode 100644 index 1334c29..0000000 --- a/apps/windows-client/src-tauri/src/helper.rs +++ /dev/null @@ -1,184 +0,0 @@ -use crate::models::ComponentId; -use serde::{Deserialize, Serialize}; -use serde_json::{json, Value}; -use std::path::{Path, PathBuf}; - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub enum HelperAction { - #[serde(rename = "install-control-app")] - InstallControlApp, - #[serde(rename = "install-proxyfier")] - InstallProxyfier, - #[serde(rename = "install-singbox")] - InstallSingbox, - #[serde(rename = "proxyfier.apply")] - ProxyfierApply, - #[serde(rename = "service.status")] - ServiceStatus, - #[serde(rename = "service.start")] - ServiceStart, - #[serde(rename = "service.stop")] - ServiceStop, - #[serde(rename = "service.restart")] - ServiceRestart, -} - -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct HelperRequest { - pub action: HelperAction, - #[serde(skip_serializing_if = "Option::is_none")] - pub component: Option, - #[serde(default)] - pub payload: Value, -} - -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct HelperResponse { - pub success: bool, - pub action: HelperAction, - pub changed: bool, - pub message: String, - #[serde(default)] - pub details: Value, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct HelperCommandSpec { - pub program: PathBuf, - pub args: Vec, - pub stdin: String, - pub requires_elevation: bool, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct HelperCommandOutput { - pub status_code: i32, - pub stdout: String, - pub stderr: String, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct HelperError { - pub code: String, - pub message: String, -} - -impl HelperError { - pub fn new(code: impl Into, message: impl Into) -> Self { - Self { - code: code.into(), - message: message.into(), - } - } -} - -pub trait HelperCommandRunner { - fn run(&self, spec: &HelperCommandSpec) -> Result; -} - -#[derive(Debug, Clone)] -pub struct StructuredHelper { - helper_program: PathBuf, - runner: R, -} - -impl StructuredHelper -where - R: HelperCommandRunner, -{ - pub fn new(helper_program: impl Into, runner: R) -> Self { - Self { - helper_program: helper_program.into(), - runner, - } - } - - pub fn runner(&self) -> &R { - &self.runner - } - - pub fn execute(&self, request: &HelperRequest) -> Result { - let stdin = serde_json::to_string(request) - .map_err(|error| HelperError::new("helper_request_encode", error.to_string()))?; - let spec = HelperCommandSpec { - program: self.helper_program.clone(), - args: vec!["--json".to_string()], - stdin, - requires_elevation: helper_action_requires_elevation(&request.action), - }; - let output = self.runner.run(&spec)?; - - if output.status_code != 0 { - return Err(HelperError::new( - "helper_exit", - format!( - "Помощник завершился с кодом {}: {}", - output.status_code, output.stderr - ), - )); - } - - parse_helper_response(&output.stdout) - } -} - -pub fn parse_helper_response(stdout: &str) -> Result { - serde_json::from_str(stdout).map_err(|error| { - HelperError::new( - "helper_response_decode", - format!("Помощник вернул не JSON или некорректный JSON: {error}"), - ) - }) -} - -pub fn install_request(component: ComponentId) -> HelperRequest { - let action = match component { - ComponentId::ControlApp => HelperAction::InstallControlApp, - ComponentId::Proxyfier => HelperAction::InstallProxyfier, - ComponentId::Singbox => HelperAction::InstallSingbox, - }; - - HelperRequest { - action, - component: Some(component), - payload: json!({}), - } -} - -pub fn service_request(component: ComponentId, action: HelperAction) -> HelperRequest { - HelperRequest { - action, - component: Some(component), - payload: json!({}), - } -} - -pub fn proxifyre_apply_request( - config_path: impl AsRef, - service_name: impl Into, -) -> HelperRequest { - HelperRequest { - action: HelperAction::ProxyfierApply, - component: Some(ComponentId::Proxyfier), - payload: json!({ - "configPath": config_path.as_ref().display().to_string(), - "serviceName": service_name.into(), - }), - } -} - -pub fn helper_action_requires_elevation(action: &HelperAction) -> bool { - matches!( - action, - HelperAction::InstallControlApp - | HelperAction::InstallProxyfier - | HelperAction::InstallSingbox - | HelperAction::ProxyfierApply - | HelperAction::ServiceStart - | HelperAction::ServiceStop - | HelperAction::ServiceRestart - ) -} diff --git a/apps/windows-client/src-tauri/src/lib.rs b/apps/windows-client/src-tauri/src/lib.rs deleted file mode 100644 index 5504150..0000000 --- a/apps/windows-client/src-tauri/src/lib.rs +++ /dev/null @@ -1,5 +0,0 @@ -pub fn run() { - tauri::Builder::default() - .run(tauri::generate_context!()) - .expect("не удалось запустить клиент VPN Proxy для Windows"); -} diff --git a/apps/windows-client/src-tauri/src/main.rs b/apps/windows-client/src-tauri/src/main.rs deleted file mode 100644 index ae5dec6..0000000 --- a/apps/windows-client/src-tauri/src/main.rs +++ /dev/null @@ -1,71 +0,0 @@ -#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] - -mod activity; -mod commands; -mod component_detection; -mod models; -mod singbox_service; -mod storage; -mod subscription; -mod validation; - -mod adapters { - pub mod proxifyre; - pub mod proxy_router; - pub mod singbox; -} - -#[cfg(test)] -pub(crate) mod proxifyre { - pub use crate::adapters::proxifyre::*; -} - -#[cfg(test)] -pub(crate) mod proxy_router { - pub use crate::adapters::proxy_router::*; -} - -#[cfg(test)] -pub(crate) mod singbox { - pub use crate::adapters::singbox::*; -} - -fn main() { - tauri::Builder::default() - .plugin(tauri_plugin_dialog::init()) - .manage(commands::CommandState::default()) - .invoke_handler(tauri::generate_handler![ - commands::get_status, - commands::get_profiles, - commands::get_saved_state, - commands::save_profile, - commands::get_targets, - commands::save_target, - commands::get_components, - commands::get_proxifyre_setup_status, - commands::get_singbox_status, - commands::get_singbox_setup_status, - commands::resolve_profile_preview, - commands::save_singbox_subscription, - commands::fetch_singbox_subscription, - commands::forget_singbox_subscription, - commands::select_singbox_server, - commands::ping_singbox_server, - commands::ping_all_singbox_servers, - commands::ping_proxy_target, - commands::generate_singbox_config, - commands::apply_profiles, - commands::get_logs, - commands::open_config_location, - commands::start_proxifyre_service, - commands::stop_proxifyre_service, - commands::install_proxifyre, - commands::uninstall_proxifyre, - commands::start_singbox_service, - commands::stop_singbox_service, - commands::install_singbox, - commands::uninstall_singbox - ]) - .run(tauri::generate_context!()) - .expect("не удалось запустить клиент VPN Proxy для Windows"); -} diff --git a/apps/windows-client/src-tauri/src/models.rs b/apps/windows-client/src-tauri/src/models.rs deleted file mode 100644 index 2ff7211..0000000 --- a/apps/windows-client/src-tauri/src/models.rs +++ /dev/null @@ -1,273 +0,0 @@ -use serde::{Deserialize, Serialize}; - -pub const DEFAULT_LOCAL_SINGBOX_LISTEN_HOST: &str = "127.0.0.1"; -pub const DEFAULT_LOCAL_SINGBOX_LISTEN_PORT: u16 = 1080; -pub const DEFAULT_LOCAL_SINGBOX_SERVICE_NAME: &str = "VpnProxySingBox"; -pub const DEFAULT_LOCAL_SINGBOX_INSTALL_ROOT: &str = r"C:\Program Files\VpnProxy\sing-box"; - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "SCREAMING_SNAKE_CASE")] -pub enum Protocol { - Tcp, - Udp, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub enum ProfileItemType { - Process, - Folder, - Exe, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub enum TargetKind { - Local, - External, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "lowercase")] -pub enum ProxyProtocol { - Socks5, - Http, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "kebab-case")] -pub enum ComponentId { - ControlApp, - Proxyfier, - Singbox, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub enum ComponentState { - Installed, - Missing, - Stopped, - Running, - Error, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct ProfileItemInput { - #[serde(rename = "type")] - pub item_type: String, - pub value: String, - #[serde(default)] - pub recursive: Option, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct ProfileInput { - pub id: Option, - pub name: String, - #[serde(default = "default_enabled")] - pub enabled: bool, - #[serde(default = "default_target_id")] - pub target_id: String, - #[serde(default = "default_protocols")] - pub protocols: Vec, - #[serde(default)] - pub items: Vec, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct ProfileItem { - #[serde(rename = "type")] - pub item_type: ProfileItemType, - pub value: String, - pub recursive: bool, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct Profile { - pub id: String, - pub name: String, - pub enabled: bool, - pub target_id: String, - pub protocols: Vec, - pub items: Vec, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct TargetInput { - pub id: Option, - pub name: String, - #[serde(default = "default_target_kind")] - pub kind: String, - #[serde(default = "default_proxy_protocol")] - pub protocol: String, - pub host: String, - pub port: u32, - #[serde(default)] - pub requires_component: Option, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct Target { - pub id: String, - pub name: String, - pub kind: TargetKind, - pub protocol: ProxyProtocol, - pub host: String, - pub port: u16, - pub requires_component: Option, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct ComponentStatus { - pub id: ComponentId, - pub name: String, - pub state: ComponentState, - pub installed: bool, - pub running: bool, - pub version: Option, - pub path: Option, - #[serde(default)] - pub problems: Vec, - #[serde(default)] - pub actions: Vec, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct LocalSingBoxConfig { - #[serde(default)] - pub subscription_url: Option, - #[serde(default)] - pub selected_server_tag: Option, - #[serde(default = "default_local_singbox_listen_host")] - pub listen_host: String, - #[serde(default = "default_local_singbox_listen_port")] - pub listen_port: u16, - #[serde(default = "default_local_singbox_service_name")] - pub service_name: String, - #[serde(default = "default_local_singbox_install_root")] - pub install_root: String, - #[serde(default)] - pub updated_at: Option, -} - -impl LocalSingBoxConfig { - pub fn subscription_display_url(&self) -> Option { - self.subscription_url - .as_deref() - .map(redact_subscription_url) - } -} - -impl Default for LocalSingBoxConfig { - fn default() -> Self { - Self { - subscription_url: None, - selected_server_tag: None, - listen_host: default_local_singbox_listen_host(), - listen_port: default_local_singbox_listen_port(), - service_name: default_local_singbox_service_name(), - install_root: default_local_singbox_install_root(), - updated_at: None, - } - } -} - -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub struct SubscriptionCache { - pub config: serde_json::Value, - #[serde(default)] - pub servers: Vec, - #[serde(default)] - pub user_info: serde_json::Map, - pub fetched_at: String, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct SubscriptionServer { - pub tag: String, - #[serde(rename = "type")] - pub server_type: String, - pub server: String, - pub server_port: u16, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct ActivityEntry { - pub id: String, - pub at: String, - pub level: ActivityLevel, - pub title: String, - pub message: String, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "lowercase")] -pub enum ActivityLevel { - Info, - Warning, - Error, - Success, -} - -fn default_enabled() -> bool { - true -} - -fn default_target_id() -> String { - "local-singbox".to_string() -} - -fn default_protocols() -> Vec { - vec!["TCP".to_string(), "UDP".to_string()] -} - -fn default_target_kind() -> String { - "external".to_string() -} - -fn default_proxy_protocol() -> String { - "socks5".to_string() -} - -fn default_local_singbox_listen_host() -> String { - DEFAULT_LOCAL_SINGBOX_LISTEN_HOST.to_string() -} - -fn default_local_singbox_listen_port() -> u16 { - DEFAULT_LOCAL_SINGBOX_LISTEN_PORT -} - -fn default_local_singbox_service_name() -> String { - DEFAULT_LOCAL_SINGBOX_SERVICE_NAME.to_string() -} - -fn default_local_singbox_install_root() -> String { - DEFAULT_LOCAL_SINGBOX_INSTALL_ROOT.to_string() -} - -pub fn redact_subscription_url(raw_url: &str) -> String { - let trimmed = raw_url.trim(); - if trimmed.is_empty() { - return String::new(); - } - - match trimmed.split_once("://") { - Some((scheme, rest)) => { - let host = rest - .split(['/', '?', '#']) - .next() - .filter(|value| !value.is_empty()) - .unwrap_or("subscription"); - format!("{scheme}://{host}/...") - } - None => { - let visible = trimmed.chars().take(18).collect::(); - if trimmed.chars().count() <= 18 { - "***".to_string() - } else { - format!("{visible}...") - } - } - } -} diff --git a/apps/windows-client/src-tauri/src/singbox_service.rs b/apps/windows-client/src-tauri/src/singbox_service.rs deleted file mode 100644 index d5a42c8..0000000 --- a/apps/windows-client/src-tauri/src/singbox_service.rs +++ /dev/null @@ -1,273 +0,0 @@ -use crate::component_detection::DetectedSingBox; -use crate::models::{DEFAULT_LOCAL_SINGBOX_INSTALL_ROOT, DEFAULT_LOCAL_SINGBOX_SERVICE_NAME}; -use serde::{Deserialize, Serialize}; -use std::path::Path; - -pub const SINGBOX_RELEASE_API_URL: &str = - "https://api.github.com/repos/SagerNet/sing-box/releases/latest"; -pub const WINSW_RELEASE_API_URL: &str = "https://api.github.com/repos/winsw/winsw/releases/latest"; -pub const WINSW_WRAPPER_FILE: &str = "VpnProxySingBox.exe"; -pub const SINGBOX_BINARY_FILE: &str = "sing-box.exe"; - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum SingBoxServiceAction { - Start, - Stop, -} - -impl SingBoxServiceAction { - pub fn action_name(self) -> &'static str { - match self { - SingBoxServiceAction::Start => "start", - SingBoxServiceAction::Stop => "stop", - } - } - - pub fn label(self) -> &'static str { - match self { - SingBoxServiceAction::Start => "запустить", - SingBoxServiceAction::Stop => "остановить", - } - } -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct SingBoxSetupStatus { - pub ready: bool, - pub missing_count: usize, - pub items: Vec, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct SingBoxSetupItem { - pub id: String, - pub name: String, - pub installed: bool, - pub version: Option, - pub details: String, -} - -#[derive(Debug, Clone, PartialEq, Eq, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct ServiceCommandOutput { - pub success: bool, - pub code: String, - pub service_name: Option, - pub status: Option, - pub process_id: Option, -} - -pub fn build_singbox_setup_status(detected: Option<&DetectedSingBox>) -> SingBoxSetupStatus { - let install_root = detected - .map(|singbox| singbox.install_dir.display().to_string()) - .unwrap_or_else(|| DEFAULT_LOCAL_SINGBOX_INSTALL_ROOT.to_string()); - let binary_item = match detected { - Some(singbox) if singbox.binary_exists => SingBoxSetupItem { - id: "sing-box-binary".to_string(), - name: "sing-box".to_string(), - installed: true, - version: Some("binary найден".to_string()), - details: singbox.executable_path.display().to_string(), - }, - _ => SingBoxSetupItem { - id: "sing-box-binary".to_string(), - name: "sing-box".to_string(), - installed: false, - version: None, - details: format!( - "Будет скачан из GitHub releases SagerNet/sing-box и установлен в {install_root}." - ), - }, - }; - let wrapper_item = match detected { - Some(singbox) if singbox.wrapper_exists => SingBoxSetupItem { - id: "winsw-wrapper".to_string(), - name: "WinSW service wrapper".to_string(), - installed: true, - version: Some("wrapper найден".to_string()), - details: singbox.wrapper_path.display().to_string(), - }, - _ => SingBoxSetupItem { - id: "winsw-wrapper".to_string(), - name: "WinSW service wrapper".to_string(), - installed: false, - version: None, - details: format!( - "Будет скачан из GitHub releases winsw/winsw как {WINSW_WRAPPER_FILE}." - ), - }, - }; - let service_item = match detected { - Some(singbox) if singbox.running => SingBoxSetupItem { - id: "windows-service".to_string(), - name: DEFAULT_LOCAL_SINGBOX_SERVICE_NAME.to_string(), - installed: true, - version: Some("служба запущена".to_string()), - details: format!("Служба {}", singbox.service_name), - }, - Some(singbox) => SingBoxSetupItem { - id: "windows-service".to_string(), - name: DEFAULT_LOCAL_SINGBOX_SERVICE_NAME.to_string(), - installed: true, - version: Some("служба остановлена".to_string()), - details: format!("Служба {}", singbox.service_name), - }, - None => SingBoxSetupItem { - id: "windows-service".to_string(), - name: DEFAULT_LOCAL_SINGBOX_SERVICE_NAME.to_string(), - installed: false, - version: None, - details: "Будет создана Windows-служба Local sing-box.".to_string(), - }, - }; - - let items = vec![binary_item, wrapper_item, service_item]; - let missing_count = items.iter().filter(|item| !item.installed).count(); - - SingBoxSetupStatus { - ready: missing_count == 0, - missing_count, - items, - } -} - -pub fn parse_service_command_output(stdout: &[u8]) -> Option { - let stdout = String::from_utf8_lossy(stdout); - let payload = stdout - .lines() - .rev() - .map(str::trim) - .find(|line| line.starts_with('{') && line.ends_with('}'))?; - - serde_json::from_str(payload).ok() -} - -pub fn ensure_safe_singbox_install_dir(path: &Path) -> Result<(), String> { - let normalized = path - .display() - .to_string() - .replace('/', "\\") - .to_ascii_lowercase(); - let file_name = path - .file_name() - .and_then(|value| value.to_str()) - .unwrap_or_default() - .to_ascii_lowercase(); - - if file_name == "sing-box" - && (normalized.contains("\\vpnproxy\\") || normalized.contains("\\vpn-proxy\\")) - { - return Ok(()); - } - - Err(format!( - "Отказываюсь рекурсивно удалять Local sing-box с небезопасным путем: {}", - path.display() - )) -} - -pub fn service_control_script( - action: SingBoxServiceAction, - service_name: &str, - config_source: Option<&Path>, - config_target: Option<&Path>, -) -> String { - let action_name = action.action_name(); - let escaped_service_name = escape_powershell_single(service_name); - let escaped_config_source = config_source - .map(|path| escape_powershell_single(&path.display().to_string())) - .unwrap_or_default(); - let escaped_config_target = config_target - .map(|path| escape_powershell_single(&path.display().to_string())) - .unwrap_or_default(); - format!( - r#" -$ErrorActionPreference = 'Stop' -$serviceName = '{escaped_service_name}' -$action = '{action_name}' -$configSource = '{escaped_config_source}' -$configTarget = '{escaped_config_target}' - -function Get-ServiceProcessId([string]$name) {{ - $escapedName = $name.Replace("'", "''") - $record = Get-CimInstance Win32_Service -Filter "Name='$escapedName'" -ErrorAction SilentlyContinue - if ($null -eq $record) {{ return 0 }} - return [int]$record.ProcessId -}} - -function Get-ServiceStatus([string]$name) {{ - $current = Get-Service -Name $name -ErrorAction SilentlyContinue - if ($null -eq $current) {{ return $null }} - return $current.Status.ToString() -}} - -function Write-ServiceResult([bool]$success, [string]$code, [string]$status, [int]$processId) {{ - [PSCustomObject]@{{ - success = $success - code = $code - serviceName = $serviceName - status = $status - processId = $processId - }} | ConvertTo-Json -Compress - exit 0 -}} - -function Sync-ServiceConfig {{ - if ($action -ne 'start' -or [string]::IsNullOrWhiteSpace($configSource)) {{ return }} - if (-not (Test-Path -LiteralPath $configSource)) {{ - Write-ServiceResult $false 'config_source_missing' (Get-ServiceStatus $serviceName) (Get-ServiceProcessId $serviceName) - }} - if ([string]::IsNullOrWhiteSpace($configTarget)) {{ return }} - - try {{ - Copy-Item -LiteralPath $configSource -Destination $configTarget -Force -ErrorAction Stop - }} catch {{ - Write-ServiceResult $false 'config_sync_failed' (Get-ServiceStatus $serviceName) (Get-ServiceProcessId $serviceName) - }} -}} - -$service = Get-Service -Name $serviceName -ErrorAction SilentlyContinue -if ($null -eq $service) {{ - Write-ServiceResult $false 'service_not_found' $null 0 -}} - -if ($action -eq 'start') {{ - Sync-ServiceConfig - - if ($service.Status -eq 'Running') {{ - Write-ServiceResult $true 'already_running' $service.Status.ToString() (Get-ServiceProcessId $serviceName) - }} - - try {{ - Start-Service -Name $serviceName -ErrorAction Stop - $service = Get-Service -Name $serviceName -ErrorAction Stop - $service.WaitForStatus('Running', [TimeSpan]::FromSeconds(15)) - }} catch {{ - Write-ServiceResult $false 'start_failed' (Get-ServiceStatus $serviceName) (Get-ServiceProcessId $serviceName) - }} - - Write-ServiceResult ($service.Status -eq 'Running') 'started' $service.Status.ToString() (Get-ServiceProcessId $serviceName) -}} - -if ($service.Status -eq 'Stopped') {{ - Write-ServiceResult $true 'already_stopped' $service.Status.ToString() (Get-ServiceProcessId $serviceName) -}} - -try {{ - Stop-Service -Name $serviceName -Force -ErrorAction Stop - $service = Get-Service -Name $serviceName -ErrorAction Stop - $service.WaitForStatus('Stopped', [TimeSpan]::FromSeconds(15)) -}} catch {{ - Write-ServiceResult $false 'stop_failed' (Get-ServiceStatus $serviceName) (Get-ServiceProcessId $serviceName) -}} - -Write-ServiceResult ($service.Status -eq 'Stopped') 'stopped' $service.Status.ToString() (Get-ServiceProcessId $serviceName) -"# - ) -} - -fn escape_powershell_single(value: &str) -> String { - value.replace('\'', "''") -} diff --git a/apps/windows-client/src-tauri/src/storage.rs b/apps/windows-client/src-tauri/src/storage.rs deleted file mode 100644 index d5b902c..0000000 --- a/apps/windows-client/src-tauri/src/storage.rs +++ /dev/null @@ -1,228 +0,0 @@ -use crate::activity::{append_activity, cap_activity, DEFAULT_ACTIVITY_LIMIT}; -use crate::models::{ - ActivityEntry, ComponentStatus, LocalSingBoxConfig, Profile, SubscriptionCache, Target, -}; -use serde::{de::DeserializeOwned, Serialize}; -use std::fs; -use std::io::{self, ErrorKind}; -use std::path::{Path, PathBuf}; - -pub fn default_config_root() -> PathBuf { - PathBuf::from(r"C:\ProgramData\VpnProxy") -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct StoragePaths { - pub root: PathBuf, - pub config_dir: PathBuf, - pub state_dir: PathBuf, - pub generated_dir: PathBuf, - pub profiles_file: PathBuf, - pub targets_file: PathBuf, - pub components_file: PathBuf, - pub local_singbox_file: PathBuf, - pub singbox_subscription_cache_file: PathBuf, - pub activity_file: PathBuf, -} - -impl StoragePaths { - pub fn new(root: impl Into) -> Self { - let root = root.into(); - let config_dir = root.join("config"); - let state_dir = root.join("state"); - let generated_dir = root.join("generated"); - - Self { - root, - profiles_file: config_dir.join("profiles.json"), - targets_file: config_dir.join("targets.json"), - components_file: config_dir.join("components.json"), - local_singbox_file: config_dir.join("local-singbox.json"), - singbox_subscription_cache_file: state_dir.join("singbox-subscription-cache.json"), - activity_file: state_dir.join("activity.json"), - config_dir, - state_dir, - generated_dir, - } - } -} - -impl Default for StoragePaths { - fn default() -> Self { - Self::new(default_config_root()) - } -} - -#[derive(Debug, Clone)] -pub struct JsonStorage { - paths: StoragePaths, - activity_limit: usize, -} - -impl JsonStorage { - pub fn new(root: impl Into) -> Self { - Self::with_activity_limit(root, DEFAULT_ACTIVITY_LIMIT) - } - - pub fn with_activity_limit(root: impl Into, activity_limit: usize) -> Self { - Self { - paths: StoragePaths::new(root), - activity_limit, - } - } - - pub fn paths(&self) -> &StoragePaths { - &self.paths - } - - pub fn ensure_dirs(&self) -> io::Result<()> { - fs::create_dir_all(&self.paths.config_dir)?; - fs::create_dir_all(&self.paths.state_dir)?; - fs::create_dir_all(&self.paths.generated_dir)?; - Ok(()) - } - - pub fn read_profiles(&self) -> io::Result> { - self.read_json_or_default(&self.paths.profiles_file) - } - - pub fn write_profiles(&self, profiles: &[Profile]) -> io::Result<()> { - self.write_json(&self.paths.profiles_file, profiles) - } - - pub fn read_targets(&self) -> io::Result> { - self.read_json_or_default(&self.paths.targets_file) - } - - pub fn write_targets(&self, targets: &[Target]) -> io::Result<()> { - self.write_json(&self.paths.targets_file, targets) - } - - pub fn read_components(&self) -> io::Result> { - self.read_json_or_default(&self.paths.components_file) - } - - pub fn write_components(&self, components: &[ComponentStatus]) -> io::Result<()> { - self.write_json(&self.paths.components_file, components) - } - - pub fn read_local_singbox_config(&self) -> io::Result { - self.read_json_or_default(&self.paths.local_singbox_file) - } - - pub fn write_local_singbox_config(&self, config: &LocalSingBoxConfig) -> io::Result<()> { - self.write_json(&self.paths.local_singbox_file, config) - } - - pub fn read_singbox_subscription_cache(&self) -> io::Result> { - self.read_optional_json(&self.paths.singbox_subscription_cache_file) - } - - pub fn write_singbox_subscription_cache(&self, cache: &SubscriptionCache) -> io::Result<()> { - self.write_json(&self.paths.singbox_subscription_cache_file, cache) - } - - pub fn remove_singbox_subscription_cache(&self) -> io::Result<()> { - match fs::remove_file(&self.paths.singbox_subscription_cache_file) { - Ok(()) => Ok(()), - Err(error) if error.kind() == ErrorKind::NotFound => Ok(()), - Err(error) => Err(error), - } - } - - pub fn read_activity(&self) -> io::Result> { - let entries = self.read_json_or_default(&self.paths.activity_file)?; - Ok(cap_activity(entries, self.activity_limit)) - } - - pub fn write_activity(&self, entries: &[ActivityEntry]) -> io::Result<()> { - let entries = cap_activity(entries.to_vec(), self.activity_limit); - self.write_json(&self.paths.activity_file, &entries) - } - - pub fn append_activity(&self, entry: ActivityEntry) -> io::Result> { - let entries = self.read_activity()?; - let entries = append_activity(entries, entry, self.activity_limit); - self.write_json(&self.paths.activity_file, &entries)?; - Ok(entries) - } - - fn read_json_or_default(&self, path: &Path) -> io::Result - where - T: DeserializeOwned + Default, - { - match fs::read_to_string(path) { - Ok(contents) => match serde_json::from_str(&contents) { - Ok(value) => Ok(value), - Err(_) => Ok(T::default()), - }, - Err(error) if error.kind() == ErrorKind::NotFound => Ok(T::default()), - Err(error) => Err(error), - } - } - - fn write_json(&self, path: &Path, value: &T) -> io::Result<()> - where - T: Serialize + ?Sized, - { - let contents = serde_json::to_vec_pretty(value) - .map_err(|error| io::Error::new(ErrorKind::InvalidData, error))?; - write_atomic(path, &contents) - } - - fn read_optional_json(&self, path: &Path) -> io::Result> - where - T: DeserializeOwned, - { - match fs::read_to_string(path) { - Ok(contents) => Ok(serde_json::from_str(&contents).ok()), - Err(error) if error.kind() == ErrorKind::NotFound => Ok(None), - Err(error) => Err(error), - } - } -} - -impl Default for JsonStorage { - fn default() -> Self { - Self::new(default_config_root()) - } -} - -pub fn backup_path(path: &Path) -> PathBuf { - sibling_with_suffix(path, "bak") -} - -fn temp_path(path: &Path) -> PathBuf { - sibling_with_suffix(path, "tmp") -} - -fn sibling_with_suffix(path: &Path, suffix: &str) -> PathBuf { - let file_name = path - .file_name() - .and_then(|value| value.to_str()) - .unwrap_or("storage.json"); - - path.with_file_name(format!("{file_name}.{suffix}")) -} - -fn write_atomic(path: &Path, contents: &[u8]) -> io::Result<()> { - if let Some(parent) = path.parent() { - fs::create_dir_all(parent)?; - } - - let temp_path = temp_path(path); - fs::write(&temp_path, contents)?; - - if path.exists() { - fs::copy(path, backup_path(path))?; - fs::remove_file(path)?; - } - - match fs::rename(&temp_path, path) { - Ok(()) => Ok(()), - Err(error) => { - let _ = fs::remove_file(&temp_path); - Err(error) - } - } -} diff --git a/apps/windows-client/src-tauri/src/subscription.rs b/apps/windows-client/src-tauri/src/subscription.rs deleted file mode 100644 index 98fa70a..0000000 --- a/apps/windows-client/src-tauri/src/subscription.rs +++ /dev/null @@ -1,269 +0,0 @@ -use crate::models::{SubscriptionCache, SubscriptionServer}; -use base64::{engine::general_purpose, Engine}; -use serde_json::{json, Map, Value}; -use std::time::{SystemTime, UNIX_EPOCH}; -use url::Url; - -const SUPPORTED_PROXY_TYPES: &[&str] = &["vless", "vmess", "trojan", "shadowsocks", "hysteria2"]; - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct SubscriptionError { - pub message: String, -} - -impl SubscriptionError { - fn new(message: impl Into) -> Self { - Self { - message: message.into(), - } - } -} - -impl std::fmt::Display for SubscriptionError { - fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - formatter.write_str(&self.message) - } -} - -impl std::error::Error for SubscriptionError {} - -#[derive(Debug, Clone, PartialEq)] -pub struct ParsedSubscription { - pub config: Value, - pub servers: Vec, -} - -pub fn parse_subscription_body(body: &str) -> Result { - let config = match serde_json::from_str::(body) { - Ok(value) => value, - Err(_) => parse_link_subscription(body)?, - }; - let servers = servers_from_config(&config)?; - - Ok(ParsedSubscription { config, servers }) -} - -pub fn parse_user_info(header_value: Option<&str>) -> Map { - let mut result = Map::new(); - let Some(header_value) = header_value else { - return result; - }; - - for part in header_value.split(';') { - let Some((key, value)) = part.trim().split_once('=') else { - continue; - }; - let key = key.trim(); - if key.is_empty() { - continue; - } - if let Ok(parsed) = value.trim().parse::() { - result.insert(key.to_string(), Value::Number(parsed.into())); - } - } - - result -} - -pub fn fetch_subscription(url: &str) -> Result { - let parsed_url = - Url::parse(url).map_err(|_| SubscriptionError::new("Invalid subscription URL"))?; - if !matches!(parsed_url.scheme(), "http" | "https") { - return Err(SubscriptionError::new( - "Subscription URL must use http or https", - )); - } - - let response = reqwest::blocking::Client::new() - .get(parsed_url) - .header("user-agent", "singbox") - .header("x-device-os", std::env::consts::OS) - .header("x-device-model", "vpn-proxy-windows-client") - .send() - .map_err(|error| SubscriptionError::new(format!("Subscription request failed: {error}")))?; - - let status = response.status(); - if !status.is_success() { - return Err(SubscriptionError::new(format!( - "Subscription request failed: HTTP {}", - status.as_u16() - ))); - } - - let user_info = parse_user_info( - response - .headers() - .get("subscription-userinfo") - .and_then(|value| value.to_str().ok()), - ); - let body = response.text().map_err(|error| { - SubscriptionError::new(format!("Subscription body read failed: {error}")) - })?; - let parsed = parse_subscription_body(&body)?; - - Ok(SubscriptionCache { - config: parsed.config, - servers: parsed.servers, - user_info, - fetched_at: now_timestamp(), - }) -} - -fn parse_link_subscription(body: &str) -> Result { - let decoded = maybe_decode_base64(body); - let links = decoded - .lines() - .map(str::trim) - .filter(|line| line.starts_with("vless://")) - .collect::>(); - - if links.is_empty() { - return Err(SubscriptionError::new( - "Subscription does not contain JSON config or VLESS links", - )); - } - - let outbounds = links - .into_iter() - .map(parse_vless_url) - .collect::, _>>()?; - - Ok(json!({ "outbounds": outbounds })) -} - -fn parse_vless_url(raw_url: &str) -> Result { - if !raw_url.starts_with("vless://") { - return Err(SubscriptionError::new("VLESS URL must start with vless://")); - } - - let parsed = Url::parse(raw_url).map_err(|_| SubscriptionError::new("Invalid VLESS URL"))?; - let tag = parsed.fragment().unwrap_or("vless-out").to_string(); - let uuid = parsed.username().trim().to_string(); - let server = parsed.host_str().map(str::to_string).unwrap_or_default(); - let server_port = parsed.port_or_known_default().unwrap_or(443); - let public_key = query_value(&parsed, "pbk").unwrap_or_default(); - let short_id = query_value(&parsed, "sid").unwrap_or_default(); - let server_name = query_value(&parsed, "sni").unwrap_or_else(|| server.clone()); - let fingerprint = query_value(&parsed, "fp").unwrap_or_else(|| "chrome".to_string()); - let flow = query_value(&parsed, "flow").unwrap_or_default(); - - if uuid.is_empty() || server.is_empty() { - return Err(SubscriptionError::new( - "VLESS URL misses uuid, host or port", - )); - } - - if public_key.is_empty() || short_id.is_empty() { - return Err(SubscriptionError::new( - "VLESS REALITY parameters pbk and sid are required", - )); - } - - Ok(json!({ - "type": "vless", - "tag": tag, - "server": server, - "server_port": server_port, - "uuid": uuid, - "flow": flow, - "tls": { - "enabled": true, - "server_name": server_name, - "utls": { - "enabled": true, - "fingerprint": fingerprint - }, - "reality": { - "enabled": true, - "public_key": public_key, - "short_id": short_id - } - }, - "packet_encoding": "xudp" - })) -} - -fn servers_from_config(config: &Value) -> Result, SubscriptionError> { - let servers = config - .get("outbounds") - .and_then(Value::as_array) - .into_iter() - .flatten() - .filter_map(server_from_outbound) - .collect::>(); - - if servers.is_empty() { - return Err(SubscriptionError::new( - "No supported proxy outbounds found in subscription", - )); - } - - Ok(servers) -} - -fn server_from_outbound(outbound: &Value) -> Option { - let server_type = outbound.get("type")?.as_str()?.to_string(); - if !SUPPORTED_PROXY_TYPES.contains(&server_type.as_str()) { - return None; - } - - let server = outbound - .get("server") - .and_then(Value::as_str) - .unwrap_or("unknown") - .to_string(); - let server_port = outbound - .get("server_port") - .and_then(Value::as_u64) - .and_then(|value| u16::try_from(value).ok()) - .unwrap_or(443); - let tag = outbound - .get("tag") - .and_then(Value::as_str) - .map(str::to_string) - .unwrap_or_else(|| format!("{server_type}-{server}")); - - Some(SubscriptionServer { - tag, - server_type, - server, - server_port, - }) -} - -fn maybe_decode_base64(content: &str) -> String { - let compact = content.split_whitespace().collect::(); - if compact.is_empty() - || !compact - .chars() - .all(|ch| ch.is_ascii_alphanumeric() || matches!(ch, '+' | '/' | '=' | '-' | '_')) - { - return content.to_string(); - } - - for engine in [general_purpose::STANDARD, general_purpose::URL_SAFE] { - if let Ok(decoded) = engine.decode(compact.as_bytes()) { - if let Ok(decoded) = String::from_utf8(decoded) { - if decoded.contains("vless://") || decoded.contains('{') { - return decoded; - } - } - } - } - - content.to_string() -} - -fn query_value(url: &Url, key: &str) -> Option { - url.query_pairs() - .find(|(name, _)| name == key) - .map(|(_, value)| value.into_owned()) -} - -fn now_timestamp() -> String { - let seconds = SystemTime::now() - .duration_since(UNIX_EPOCH) - .map(|duration| duration.as_secs()) - .unwrap_or(0); - format!("unix:{seconds}") -} diff --git a/apps/windows-client/src-tauri/src/validation.rs b/apps/windows-client/src-tauri/src/validation.rs deleted file mode 100644 index 854fa48..0000000 --- a/apps/windows-client/src-tauri/src/validation.rs +++ /dev/null @@ -1,223 +0,0 @@ -use crate::models::{ - ComponentId, Profile, ProfileInput, ProfileItem, ProfileItemType, Protocol, ProxyProtocol, - Target, TargetInput, TargetKind, -}; - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct ValidationError { - pub field: String, - pub message: String, -} - -pub type ValidationResult = Result>; - -fn error(field: impl Into, message: impl Into) -> ValidationError { - ValidationError { - field: field.into(), - message: message.into(), - } -} - -fn clean(value: &str) -> String { - value.trim().to_string() -} - -fn slug(value: &str, fallback: &str) -> String { - let mut output = String::new(); - let mut previous_dash = false; - - for ch in value.trim().to_lowercase().chars() { - if ch.is_ascii_alphanumeric() { - output.push(ch); - previous_dash = false; - } else if !previous_dash { - output.push('-'); - previous_dash = true; - } - } - - let output = output.trim_matches('-').to_string(); - if output.is_empty() { - fallback.to_string() - } else { - output - } -} - -fn process_name(value: &str) -> String { - let base = value.trim().rsplit(['\\', '/']).next().unwrap_or("").trim(); - base.strip_suffix(".exe") - .or_else(|| base.strip_suffix(".EXE")) - .unwrap_or(base) - .trim() - .to_string() -} - -pub fn parse_protocol(value: &str) -> Result { - match value.trim().to_ascii_uppercase().as_str() { - "TCP" => Ok(Protocol::Tcp), - "UDP" => Ok(Protocol::Udp), - _ => Err(error( - "protocols", - format!("Неподдерживаемый протокол: {value}"), - )), - } -} - -pub fn parse_profile_item_type(value: &str) -> Result { - match value.trim().to_ascii_lowercase().as_str() { - "process" => Ok(ProfileItemType::Process), - "folder" => Ok(ProfileItemType::Folder), - "exe" => Ok(ProfileItemType::Exe), - _ => Err(error( - "items.type", - format!("Неподдерживаемый тип элемента: {value}"), - )), - } -} - -pub fn parse_target_kind(value: &str) -> Result { - match value.trim().to_ascii_lowercase().as_str() { - "local" => Ok(TargetKind::Local), - "external" => Ok(TargetKind::External), - _ => Err(error("kind", format!("Неподдерживаемый тип цели: {value}"))), - } -} - -pub fn parse_proxy_protocol(value: &str) -> Result { - match value.trim().to_ascii_lowercase().as_str() { - "socks5" => Ok(ProxyProtocol::Socks5), - "http" => Ok(ProxyProtocol::Http), - _ => Err(error( - "protocol", - format!("Неподдерживаемый протокол прокси: {value}"), - )), - } -} - -pub fn parse_component_id(value: &str) -> Result { - match value.trim().to_ascii_lowercase().as_str() { - "control-app" | "controlapp" => Ok(ComponentId::ControlApp), - "proxyfier" => Ok(ComponentId::Proxyfier), - "singbox" | "sing-box" => Ok(ComponentId::Singbox), - _ => Err(error( - "requires_component", - format!("Неподдерживаемый компонент: {value}"), - )), - } -} - -pub fn normalize_profile(input: ProfileInput) -> ValidationResult { - let mut errors = Vec::new(); - let name = clean(&input.name); - if name.is_empty() { - errors.push(error("name", "Укажите название профиля")); - } - - let target_id = clean(&input.target_id); - if target_id.is_empty() { - errors.push(error("target_id", "Укажите цель профиля")); - } - - let mut protocols = Vec::new(); - for value in input.protocols { - match parse_protocol(&value) { - Ok(protocol) if !protocols.contains(&protocol) => protocols.push(protocol), - Ok(_) => {} - Err(err) => errors.push(err), - } - } - if protocols.is_empty() { - errors.push(error("protocols", "Выберите хотя бы один протокол")); - } - - let mut items = Vec::new(); - for raw_item in input.items { - let item_type = match parse_profile_item_type(&raw_item.item_type) { - Ok(item_type) => item_type, - Err(err) => { - errors.push(err); - continue; - } - }; - - let value = match item_type { - ProfileItemType::Process => process_name(&raw_item.value), - ProfileItemType::Folder | ProfileItemType::Exe => clean(&raw_item.value), - }; - if value.is_empty() { - errors.push(error("items.value", "Укажите значение элемента профиля")); - continue; - } - - let recursive = - matches!(item_type, ProfileItemType::Folder) && raw_item.recursive.unwrap_or(true); - items.push(ProfileItem { - item_type, - value, - recursive, - }); - } - - if !errors.is_empty() { - return Err(errors); - } - - Ok(Profile { - id: slug(input.id.as_deref().unwrap_or(&name), "profile"), - name, - enabled: input.enabled, - target_id, - protocols, - items, - }) -} - -pub fn normalize_target(input: TargetInput) -> ValidationResult { - let mut errors = Vec::new(); - let name = clean(&input.name); - let host = clean(&input.host); - - if name.is_empty() { - errors.push(error("name", "Укажите название цели")); - } - if host.is_empty() { - errors.push(error("host", "Укажите хост цели")); - } - if input.port == 0 || input.port > u16::MAX as u32 { - errors.push(error("port", "Порт цели должен быть от 1 до 65535")); - } - - let kind = parse_target_kind(&input.kind).unwrap_or_else(|err| { - errors.push(err); - TargetKind::External - }); - let protocol = parse_proxy_protocol(&input.protocol).unwrap_or_else(|err| { - errors.push(err); - ProxyProtocol::Socks5 - }); - let requires_component = match input.requires_component { - Some(value) if !value.trim().is_empty() => match parse_component_id(&value) { - Ok(component) => Some(component), - Err(err) => { - errors.push(err); - None - } - }, - _ => None, - }; - - if !errors.is_empty() { - return Err(errors); - } - - Ok(Target { - id: slug(input.id.as_deref().unwrap_or(&name), "target"), - name, - kind, - protocol, - host, - port: input.port as u16, - requires_component, - }) -} diff --git a/apps/windows-client/src-tauri/tauri.conf.json b/apps/windows-client/src-tauri/tauri.conf.json deleted file mode 100644 index 77ccfed..0000000 --- a/apps/windows-client/src-tauri/tauri.conf.json +++ /dev/null @@ -1,37 +0,0 @@ -{ - "$schema": "https://schema.tauri.app/config/2", - "productName": "VPN Proxy для Windows", - "version": "0.1.0", - "identifier": "ru.dokops.vpn-proxy.windows", - "build": { - "beforeDevCommand": "npm run dev", - "beforeBuildCommand": "npm run build", - "devUrl": "http://127.0.0.1:5173", - "frontendDist": "../dist" - }, - "app": { - "windows": [ - { - "title": "VPN Proxy для Windows", - "width": 1120, - "height": 760, - "minWidth": 760, - "minHeight": 560, - "resizable": true - } - ], - "security": { - "csp": null - } - }, - "bundle": { - "active": true, - "targets": "all", - "icon": [ - "icons/32x32.png", - "icons/128x128.png", - "icons/128x128@2x.png", - "icons/icon.ico" - ] - } -} diff --git a/apps/windows-client/src-tauri/tests/command_tests.rs b/apps/windows-client/src-tauri/tests/command_tests.rs deleted file mode 100644 index 0362c6a..0000000 --- a/apps/windows-client/src-tauri/tests/command_tests.rs +++ /dev/null @@ -1,528 +0,0 @@ -#[path = "../src/activity.rs"] -mod activity; -#[path = "../src/commands.rs"] -mod commands; -#[path = "../src/component_detection.rs"] -mod component_detection; -#[path = "../src/models.rs"] -mod models; -#[path = "../src/adapters/proxifyre.rs"] -mod proxifyre; -#[path = "../src/adapters/proxy_router.rs"] -mod proxy_router; -#[path = "../src/adapters/singbox.rs"] -mod singbox; -#[path = "../src/singbox_service.rs"] -mod singbox_service; -#[path = "../src/storage.rs"] -mod storage; -#[path = "../src/subscription.rs"] -mod subscription; -#[path = "../src/validation.rs"] -mod validation; - -use commands::{ - apply_profiles_with_services, build_status, resolve_component_statuses, resolve_preview, - save_profile_to_storage, save_target_to_storage, Clock, CommandError, DetectedProxyApplyHelper, - HelperApplyRequest, HelperApplyResult, ProfileInputDto, ProfileItemInputDto, ProxyApplyHelper, - TargetInputDto, -}; -use component_detection::{ - DetectedProxyfier, ProxyfierDetectionHost, ProxyfierEngine, RegistryInstallEntry, -}; -use models::{ - ComponentId, ComponentState, ComponentStatus, Profile, ProfileItem, ProfileItemType, Protocol, - ProxyProtocol, Target, TargetKind, -}; -use proxifyre::ProxiFyreAdapter; -use std::collections::HashSet; -use std::fs; -use std::net::TcpListener; -use std::path::{Path, PathBuf}; -#[cfg(windows)] -use std::process::Command as ProcessCommand; -use std::time::{SystemTime, UNIX_EPOCH}; -use storage::JsonStorage; - -#[test] -fn save_commands_normalize_and_persist_profile_and_target() { - let root = test_root("save"); - let storage = JsonStorage::new(root.clone()); - - let target = save_target_to_storage( - &storage, - TargetInputDto { - id: Some("Home Gateway".to_string()), - name: " Home Gateway ".to_string(), - kind: Some("external".to_string()), - protocol: Some("socks5".to_string()), - host: " 192.168.50.111 ".to_string(), - port: 8080, - requires_component: None, - }, - ) - .expect("target command should normalize"); - let profile = save_profile_to_storage( - &storage, - ProfileInputDto { - id: Some("Discord".to_string()), - name: " Discord ".to_string(), - enabled: Some(true), - target_id: Some("home-gateway".to_string()), - protocols: Some(vec!["tcp".to_string(), "UDP".to_string()]), - items: Some(vec![ProfileItemInputDto { - item_type: "process".to_string(), - value: "Discord.exe".to_string(), - recursive: None, - }]), - }, - ) - .expect("profile command should normalize"); - - assert_eq!(target.id, "home-gateway"); - assert_eq!(profile.id, "discord"); - assert_eq!(profile.target_id, "home-gateway"); - assert_eq!(profile.items[0].value, "Discord"); - - let status = build_status(&storage).expect("status command should read stored state"); - assert_eq!( - status.route_line, - "Выбранные приложения -> ProxiFyre -> внешний прокси 192.168.50.111:8080" - ); - assert_eq!(status.active_profile_count, 1); - assert_eq!(status.routed_app_count, 1); - - cleanup(&root); -} - -#[test] -fn resolve_preview_returns_structured_apps_without_filesystem_scan() { - let preview = resolve_preview(ProfileInputDto { - id: Some("Game".to_string()), - name: "Game".to_string(), - enabled: Some(true), - target_id: Some("home-gateway".to_string()), - protocols: Some(vec!["TCP".to_string()]), - items: Some(vec![ - ProfileItemInputDto { - item_type: "process".to_string(), - value: "Discord.exe".to_string(), - recursive: None, - }, - ProfileItemInputDto { - item_type: "folder".to_string(), - value: r"C:\Games\Launcher".to_string(), - recursive: Some(true), - }, - ]), - }) - .expect("preview should normalize profile input"); - - assert_eq!(preview.profile_id, "game"); - assert_eq!(preview.apps.len(), 2); - assert_eq!(preview.apps[0].app_name, "Discord"); - assert_eq!(preview.apps[1].source_type, ProfileItemType::Folder); - assert!(preview - .warnings - .iter() - .any(|warning| warning.contains("Сканирование папок отложено"))); -} - -#[test] -fn ping_proxy_target_reports_open_tcp_endpoint() { - let listener = TcpListener::bind("127.0.0.1:0").expect("bind local listener"); - let port = listener.local_addr().expect("read local addr").port(); - - let result = commands::ping_proxy_target_endpoint(commands::PingProxyTargetInputDto { - host: "127.0.0.1".to_string(), - port, - }) - .expect("ping should return response"); - - assert_eq!(result.tag, "external-proxy"); - assert_eq!(result.server, "127.0.0.1"); - assert_eq!(result.server_port, port); - assert!(result.ok); - assert!(result.latency.is_some()); -} - -#[test] -#[cfg(windows)] -fn proxifyre_install_script_parses_as_powershell() { - let root = test_root("proxifyre-install-script"); - fs::create_dir_all(&root).expect("test root should be created"); - - let script = commands::wrap_elevated_package_script( - &commands::install_proxifyre_script(&root.join("proxifyre-app-config.json")), - &root.join("install.log"), - ); - let script_path = root.join("install.ps1"); - let mut script_bytes = vec![0xEF, 0xBB, 0xBF]; - script_bytes.extend_from_slice(script.as_bytes()); - fs::write(&script_path, script_bytes).expect("script should be written"); - - let escaped_path = script_path.display().to_string().replace('\'', "''"); - let parser = format!( - "$tokens = $null; $errors = $null; [System.Management.Automation.Language.Parser]::ParseFile('{escaped_path}', [ref]$tokens, [ref]$errors) | Out-Null; if ($errors.Count -gt 0) {{ $errors | ForEach-Object {{ $_.Message }}; exit 1 }}" - ); - let output = ProcessCommand::new("powershell") - .args(["-NoProfile", "-NonInteractive", "-Command", &parser]) - .output() - .expect("powershell parser should run"); - - assert!( - output.status.success(), - "install script should parse\nstdout:\n{}\nstderr:\n{}", - String::from_utf8_lossy(&output.stdout), - String::from_utf8_lossy(&output.stderr), - ); - - cleanup(&root); -} - -#[test] -fn singbox_runner_preserves_installer_args_with_spaces() { - let script = commands::singbox_installer_runner_script( - Path::new(r"C:\ProgramData\VpnProxy\state\install-singbox.ps1"), - Path::new(r"C:\ProgramData\VpnProxy\state\install.log"), - &[ - "-InstallRoot".to_string(), - r"C:\Program Files\VpnProxy\sing-box".to_string(), - "-ServiceName".to_string(), - "VpnProxySingBox".to_string(), - "-Uninstall".to_string(), - ], - ); - - assert!(script.contains("$installerArgs = @('-InstallRoot', 'C:\\Program Files\\VpnProxy\\sing-box'")); - assert!(script.contains("& powershell.exe -NoProfile -ExecutionPolicy Bypass -File $installerPath @installerArgs")); - assert!(!script.contains("Start-Process -FilePath 'powershell.exe' -ArgumentList $argumentList")); -} - -#[test] -fn apply_generates_derived_config_and_records_activity_with_mock_helper() { - let root = test_root("apply"); - let storage = JsonStorage::new(root.clone()); - storage - .write_profiles(&[discord_profile("home-gateway")]) - .expect("write profiles"); - storage - .write_targets(&[external_socks5_target()]) - .expect("write targets"); - storage - .write_components(&[proxyfier_running(), singbox_missing()]) - .expect("write components"); - - let response = apply_profiles_with_services( - &storage, - &ProxiFyreAdapter::default(), - &MockApplyHelper, - &FixedClock, - ) - .expect("apply command should generate config and call helper"); - - let generated_path = PathBuf::from(&response.generated_config_path); - let generated_contents = fs::read_to_string(&generated_path).expect("read generated config"); - let activity = storage.read_activity().expect("read activity"); - - assert!(response.success); - assert!(response.changed); - assert_eq!(response.adapter_id, "proxifyre"); - assert_eq!(response.enabled_profiles, 1); - assert_eq!(response.routed_apps, 1); - assert_eq!(response.helper.action, "proxyfier.apply.mock"); - assert!(generated_contents.contains("\"appNames\"")); - assert!(generated_contents.contains("Discord")); - assert!(generated_path.ends_with("proxifyre-app-config.json")); - assert_eq!(activity.len(), 1); - assert_eq!(activity[0].at, "2026-07-03T00:00:00Z"); - assert_eq!(activity[0].title, "Конфиг ProxiFyre создан"); - - cleanup(&root); -} - -#[test] -fn apply_blocks_local_singbox_target_when_component_is_missing() { - let root = test_root("missing-singbox"); - let storage = JsonStorage::new(root.clone()); - storage - .write_profiles(&[discord_profile("local-singbox")]) - .expect("write profiles"); - storage - .write_targets(&[local_singbox_target()]) - .expect("write targets"); - storage - .write_components(&[singbox_missing()]) - .expect("write components"); - - let error = apply_profiles_with_services( - &storage, - &ProxiFyreAdapter::default(), - &MockApplyHelper, - &FixedClock, - ) - .expect_err("missing sing-box should block local target apply"); - let activity = storage.read_activity().expect("read blocked activity"); - - assert_eq!(error.code, "required_component_not_running"); - assert_eq!(activity.len(), 1); - assert_eq!(activity[0].level, models::ActivityLevel::Error); - assert_eq!(activity[0].title, "Применение ProxiFyre заблокировано"); - - cleanup(&root); -} - -#[test] -fn component_status_merges_detected_existing_proxifyre() { - let components = resolve_component_statuses( - Vec::new(), - Some(DetectedProxyfier { - engine: ProxyfierEngine::ProxiFyre, - name: "ProxiFyre".to_string(), - install_dir: PathBuf::from(r"C:\Tools\ProxiFyre"), - executable_path: PathBuf::from(r"C:\Tools\ProxiFyre\ProxiFyre.exe"), - config_path: Some(PathBuf::from(r"C:\Tools\ProxiFyre\app-config.json")), - running: true, - service_name: Some("ProxiFyreService".to_string()), - }), - None, - ); - let proxyfier = components - .iter() - .find(|component| component.id == ComponentId::Proxyfier) - .expect("proxyfier component"); - - assert_eq!(proxyfier.state, ComponentState::Running); - assert!(proxyfier.installed); - assert!(proxyfier.running); - assert_eq!(proxyfier.path, Some(r"C:\Tools\ProxiFyre".to_string())); - assert!(proxyfier.problems.is_empty()); -} - -#[test] -fn detected_proxy_apply_helper_writes_proxifyre_app_config() { - let root = test_root("detected-proxifyre"); - let install_dir = root.join("ProxiFyre"); - fs::create_dir_all(&install_dir).expect("install dir"); - fs::write(install_dir.join("ProxiFyre.exe"), "mock exe").expect("mock exe"); - fs::write(install_dir.join("app-config.json"), "{}").expect("existing config"); - let generated_config = root.join("generated").join("proxifyre-app-config.json"); - let host = DetectionHost::new() - .with_registry("ProxiFyre", &install_dir) - .with_path(&install_dir) - .with_path(&install_dir.join("ProxiFyre.exe")); - let helper = DetectedProxyApplyHelper::new(host); - - let result = helper - .apply_proxy_config(HelperApplyRequest { - adapter_id: "proxifyre", - config_path: &generated_config, - config_contents: r#"{"proxies":[]}"#, - }) - .expect("detected helper should apply"); - - let applied = - fs::read_to_string(install_dir.join("app-config.json")).expect("read applied app-config"); - - assert!(result.success); - assert!(result.changed); - assert_eq!(result.action, "proxifyre.apply-detected-config"); - assert_eq!(applied, r#"{"proxies":[]}"#); - assert!(install_dir.join("app-config.json.bak").exists()); - - cleanup(&root); -} - -#[test] -fn detected_proxy_apply_helper_ignores_plain_proxifier_install() { - let root = test_root("detected-proxifier"); - let install_dir = root.join("Proxifier"); - fs::create_dir_all(&install_dir).expect("install dir"); - fs::write(install_dir.join("Proxifier.exe"), "mock exe").expect("mock exe"); - let generated_config = root.join("generated").join("proxifyre-app-config.json"); - let host = DetectionHost::new() - .with_registry("Proxifier", &install_dir) - .with_path(&install_dir) - .with_path(&install_dir.join("Proxifier.exe")); - let helper = DetectedProxyApplyHelper::new(host); - - let result = helper - .apply_proxy_config(HelperApplyRequest { - adapter_id: "proxifyre", - config_path: &generated_config, - config_contents: r#"{"proxies":[]}"#, - }) - .expect("plain Proxifier should be ignored and config should be staged"); - - assert!(result.success); - assert!(result.changed); - assert_eq!(result.action, "proxifyre.stage-generated-config"); - assert!(result - .message - .contains("совместимая установка ProxiFyre не найдена")); - - cleanup(&root); -} - -struct MockApplyHelper; - -impl ProxyApplyHelper for MockApplyHelper { - fn apply_proxy_config( - &self, - request: HelperApplyRequest<'_>, - ) -> Result { - assert_eq!(request.adapter_id, "proxifyre"); - assert!(request.config_contents.contains("Discord")); - assert!(request.config_path.ends_with("proxifyre-app-config.json")); - - Ok(HelperApplyResult { - success: true, - changed: true, - action: "proxyfier.apply.mock".to_string(), - message: "Mock helper accepted generated ProxiFyre config".to_string(), - }) - } -} - -struct FixedClock; - -impl Clock for FixedClock { - fn now(&self) -> String { - "2026-07-03T00:00:00Z".to_string() - } -} - -#[derive(Default)] -struct DetectionHost { - paths: HashSet, - registry: Vec, -} - -impl DetectionHost { - fn new() -> Self { - Self::default() - } - - fn with_path(mut self, path: &Path) -> Self { - self.paths.insert(normalize_path(path)); - self - } - - fn with_registry(mut self, display_name: &str, install_location: &Path) -> Self { - self.registry.push(RegistryInstallEntry { - display_name: display_name.to_string(), - install_location: Some(install_location.to_path_buf()), - display_icon: None, - }); - self - } -} - -impl ProxyfierDetectionHost for DetectionHost { - fn env_var(&self, _name: &str) -> Option { - None - } - - fn path_exists(&self, path: &Path) -> bool { - self.paths.contains(&normalize_path(path)) - } - - fn process_running(&self, _process_name: &str) -> bool { - false - } - - fn service_running(&self, _service_name: &str) -> bool { - false - } - - fn registry_install_entries(&self) -> Vec { - self.registry.clone() - } -} - -fn normalize_path(path: &Path) -> String { - path.display() - .to_string() - .replace('/', "\\") - .to_ascii_lowercase() -} - -fn test_root(name: &str) -> PathBuf { - let timestamp = SystemTime::now() - .duration_since(UNIX_EPOCH) - .expect("system clock before unix epoch") - .as_nanos(); - - std::env::temp_dir().join(format!("vpn-proxy-commands-{name}-{timestamp}")) -} - -fn cleanup(root: &Path) { - let _ = fs::remove_dir_all(root); -} - -fn discord_profile(target_id: &str) -> Profile { - Profile { - id: "discord".to_string(), - name: "Discord".to_string(), - enabled: true, - target_id: target_id.to_string(), - protocols: vec![Protocol::Tcp, Protocol::Udp], - items: vec![ProfileItem { - item_type: ProfileItemType::Process, - value: "Discord".to_string(), - recursive: false, - }], - } -} - -fn external_socks5_target() -> Target { - Target { - id: "home-gateway".to_string(), - name: "Домашний шлюз".to_string(), - kind: TargetKind::External, - protocol: ProxyProtocol::Socks5, - host: "192.168.50.111".to_string(), - port: 8080, - requires_component: None, - } -} - -fn local_singbox_target() -> Target { - Target { - id: "local-singbox".to_string(), - name: "Локальный sing-box".to_string(), - kind: TargetKind::Local, - protocol: ProxyProtocol::Socks5, - host: "127.0.0.1".to_string(), - port: 1080, - requires_component: Some(ComponentId::Singbox), - } -} - -fn proxyfier_running() -> ComponentStatus { - ComponentStatus { - id: ComponentId::Proxyfier, - name: "ProxiFyre".to_string(), - state: ComponentState::Running, - installed: true, - running: true, - version: Some("2.2.1".to_string()), - path: Some(r"C:\Tools\ProxiFyre".to_string()), - problems: Vec::new(), - actions: vec!["Restart".to_string()], - } -} - -fn singbox_missing() -> ComponentStatus { - ComponentStatus { - id: ComponentId::Singbox, - name: "Локальный sing-box".to_string(), - state: ComponentState::Missing, - installed: false, - running: false, - version: None, - path: None, - problems: vec!["Локальный sing-box не установлен".to_string()], - actions: vec!["Установить локальный sing-box".to_string()], - } -} diff --git a/apps/windows-client/src-tauri/tests/component_detection_tests.rs b/apps/windows-client/src-tauri/tests/component_detection_tests.rs deleted file mode 100644 index f4c6abb..0000000 --- a/apps/windows-client/src-tauri/tests/component_detection_tests.rs +++ /dev/null @@ -1,207 +0,0 @@ -#[path = "../src/component_detection.rs"] -mod component_detection; -#[path = "../src/models.rs"] -mod models; - -use component_detection::{ - detect_proxyfier_install_with_host, detect_singbox_install_with_host, - proxyfier_component_from_detection, singbox_component_from_detection, ProxyfierDetectionHost, - ProxyfierEngine, RegistryInstallEntry, -}; -use models::ComponentState; -use std::{ - collections::{HashMap, HashSet}, - path::{Path, PathBuf}, -}; - -#[test] -fn detects_existing_proxifyre_from_registry_install_location() { - let host = MockHost::new() - .with_registry("ProxiFyre", r"C:\Tools\ProxiFyre") - .with_path(r"C:\Tools\ProxiFyre") - .with_service("ProxiFyreService"); - - let detected = detect_proxyfier_install_with_host(&host) - .expect("existing ProxiFyre install should be detected"); - - assert_eq!(detected.engine, ProxyfierEngine::ProxiFyre); - assert_eq!(detected.install_dir, PathBuf::from(r"C:\Tools\ProxiFyre")); - assert_eq!( - detected.config_path, - Some(PathBuf::from(r"C:\Tools\ProxiFyre\app-config.json")) - ); - assert!(detected.running); - - let component = proxyfier_component_from_detection(Some(&detected)); - assert_eq!(component.state, ComponentState::Running); - assert!(component.installed); - assert!(component.running); - assert_eq!(component.path, Some(r"C:\Tools\ProxiFyre".to_string())); - assert!(component.problems.is_empty()); -} - -#[test] -fn ignores_plain_proxifier_install() { - let host = MockHost::new() - .with_registry("Proxifier", r"C:\Program Files\Proxifier") - .with_path(r"C:\Program Files\Proxifier") - .with_process("Proxifier.exe"); - - assert!(detect_proxyfier_install_with_host(&host).is_none()); -} - -#[test] -fn env_override_can_point_to_portable_proxifyre_install() { - let host = MockHost::new() - .with_env("VPN_PROXY_PROXIFYRE_ROOT", r"D:\Portable\ProxiFyre") - .with_path(r"D:\Portable\ProxiFyre\ProxiFyre.exe"); - - let detected = detect_proxyfier_install_with_host(&host) - .expect("env override should be checked before common paths"); - - assert_eq!(detected.engine, ProxyfierEngine::ProxiFyre); - assert_eq!( - detected.executable_path, - PathBuf::from(r"D:\Portable\ProxiFyre\ProxiFyre.exe") - ); -} - -#[test] -fn missing_proxyfier_returns_install_action_status() { - let component = proxyfier_component_from_detection(None); - - assert_eq!(component.state, ComponentState::Missing); - assert!(!component.installed); - assert_eq!(component.actions, vec!["Установить ProxiFyre"]); -} - -#[test] -fn detects_running_local_singbox_from_default_install_root_and_service() { - let host = MockHost::new() - .with_path(r"C:\Program Files\VpnProxy\sing-box\sing-box.exe") - .with_service("VpnProxySingBox"); - - let detected = - detect_singbox_install_with_host(&host).expect("existing sing-box should be detected"); - - assert_eq!( - detected.executable_path, - PathBuf::from(r"C:\Program Files\VpnProxy\sing-box\sing-box.exe") - ); - assert_eq!(detected.service_name, "VpnProxySingBox"); - assert!(detected.running); - - let component = singbox_component_from_detection(Some(&detected)); - assert_eq!(component.state, ComponentState::Running); - assert!(component.installed); - assert!(component.running); - assert_eq!( - component.path, - Some(r"C:\Program Files\VpnProxy\sing-box\sing-box.exe".to_string()) - ); - assert!(component.problems.is_empty()); -} - -#[test] -fn detects_stopped_local_singbox_from_env_override() { - let host = MockHost::new() - .with_env("VPN_PROXY_SINGBOX_ROOT", r"D:\Portable\sing-box") - .with_path(r"D:\Portable\sing-box\sing-box.exe"); - - let detected = detect_singbox_install_with_host(&host).expect("env override should be checked"); - let component = singbox_component_from_detection(Some(&detected)); - - assert_eq!( - detected.executable_path, - PathBuf::from(r"D:\Portable\sing-box\sing-box.exe") - ); - assert_eq!(component.state, ComponentState::Stopped); - assert!(component.installed); - assert!(!component.running); - assert!(component - .problems - .iter() - .any(|problem| problem.contains("остановлена"))); -} - -#[test] -fn missing_local_singbox_returns_optional_install_action_status() { - let component = singbox_component_from_detection(None); - - assert_eq!(component.state, ComponentState::Missing); - assert!(!component.installed); - assert!(!component.running); - assert_eq!(component.actions, vec!["Установить Local sing-box"]); - assert!(component.problems.is_empty()); -} - -#[derive(Default)] -struct MockHost { - env: HashMap, - paths: HashSet, - processes: HashSet, - services: HashSet, - registry: Vec, -} - -impl MockHost { - fn new() -> Self { - Self::default() - } - - fn with_env(mut self, name: &str, value: &str) -> Self { - self.env.insert(name.to_string(), value.to_string()); - self - } - - fn with_path(mut self, path: &str) -> Self { - self.paths.insert(normalize_path(path)); - self - } - - fn with_process(mut self, process: &str) -> Self { - self.processes.insert(process.to_ascii_lowercase()); - self - } - - fn with_service(mut self, service: &str) -> Self { - self.services.insert(service.to_ascii_lowercase()); - self - } - - fn with_registry(mut self, display_name: &str, install_location: &str) -> Self { - self.registry.push(RegistryInstallEntry { - display_name: display_name.to_string(), - install_location: Some(PathBuf::from(install_location)), - display_icon: None, - }); - self - } -} - -impl ProxyfierDetectionHost for MockHost { - fn env_var(&self, name: &str) -> Option { - self.env.get(name).cloned() - } - - fn path_exists(&self, path: &Path) -> bool { - self.paths - .contains(&normalize_path(&path.display().to_string())) - } - - fn process_running(&self, process_name: &str) -> bool { - self.processes.contains(&process_name.to_ascii_lowercase()) - } - - fn service_running(&self, service_name: &str) -> bool { - self.services.contains(&service_name.to_ascii_lowercase()) - } - - fn registry_install_entries(&self) -> Vec { - self.registry.clone() - } -} - -fn normalize_path(path: &str) -> String { - path.replace('/', "\\").to_ascii_lowercase() -} diff --git a/apps/windows-client/src-tauri/tests/domain_tests.rs b/apps/windows-client/src-tauri/tests/domain_tests.rs deleted file mode 100644 index a06428f..0000000 --- a/apps/windows-client/src-tauri/tests/domain_tests.rs +++ /dev/null @@ -1,128 +0,0 @@ -#[path = "../src/models.rs"] -mod models; -#[path = "../src/validation.rs"] -mod validation; - -use models::{ - ComponentId, ProfileInput, ProfileItemInput, ProfileItemType, Protocol, ProxyProtocol, - TargetInput, TargetKind, -}; -use validation::{normalize_profile, normalize_target}; - -#[test] -fn normalizes_profile_source_items() { - let profile = normalize_profile(ProfileInput { - id: Some("Discord + Vesktop".to_string()), - name: " Discord + Vesktop ".to_string(), - enabled: true, - target_id: " home-gateway ".to_string(), - protocols: vec!["tcp".to_string(), "UDP".to_string(), "TCP".to_string()], - items: vec![ - ProfileItemInput { - item_type: "process".to_string(), - value: "Discord.exe".to_string(), - recursive: None, - }, - ProfileItemInput { - item_type: "folder".to_string(), - value: "%LOCALAPPDATA%\\Vesktop".to_string(), - recursive: Some(true), - }, - ProfileItemInput { - item_type: "exe".to_string(), - value: "C:\\Games\\Game\\game.exe".to_string(), - recursive: Some(true), - }, - ], - }) - .expect("profile should normalize"); - - assert_eq!(profile.id, "discord-vesktop"); - assert_eq!(profile.name, "Discord + Vesktop"); - assert_eq!(profile.target_id, "home-gateway"); - assert_eq!(profile.protocols, vec![Protocol::Tcp, Protocol::Udp]); - assert_eq!(profile.items[0].item_type, ProfileItemType::Process); - assert_eq!(profile.items[0].value, "Discord"); - assert!(!profile.items[0].recursive); - assert_eq!(profile.items[1].item_type, ProfileItemType::Folder); - assert!(profile.items[1].recursive); - assert_eq!(profile.items[2].item_type, ProfileItemType::Exe); - assert!(!profile.items[2].recursive); -} - -#[test] -fn rejects_unsupported_profile_protocols() { - let error = normalize_profile(ProfileInput { - id: None, - name: "Bad protocol".to_string(), - enabled: true, - target_id: "home-gateway".to_string(), - protocols: vec!["icmp".to_string()], - items: vec![ProfileItemInput { - item_type: "process".to_string(), - value: "Discord".to_string(), - recursive: None, - }], - }) - .expect_err("unsupported protocol should fail"); - - assert!(error.iter().any(|item| item.field == "protocols")); -} - -#[test] -fn normalizes_external_target_without_local_singbox() { - let target = normalize_target(TargetInput { - id: Some("Home Gateway".to_string()), - name: " Home Gateway ".to_string(), - kind: "external".to_string(), - protocol: "socks5".to_string(), - host: " 192.168.50.111 ".to_string(), - port: 8080, - requires_component: None, - }) - .expect("external target should normalize"); - - assert_eq!(target.id, "home-gateway"); - assert_eq!(target.kind, TargetKind::External); - assert_eq!(target.protocol, ProxyProtocol::Socks5); - assert_eq!(target.host, "192.168.50.111"); - assert_eq!(target.port, 8080); - assert_eq!(target.requires_component, None); -} - -#[test] -fn local_singbox_target_can_exist_before_component_is_installed() { - let target = normalize_target(TargetInput { - id: Some("local-singbox".to_string()), - name: "Local sing-box".to_string(), - kind: "local".to_string(), - protocol: "socks5".to_string(), - host: "127.0.0.1".to_string(), - port: 1080, - requires_component: Some("singbox".to_string()), - }) - .expect("local target definition should not require installed component"); - - assert_eq!(target.kind, TargetKind::Local); - assert_eq!(target.requires_component, Some(ComponentId::Singbox)); -} - -#[test] -fn rejects_malformed_target_fields() { - let error = normalize_target(TargetInput { - id: None, - name: "".to_string(), - kind: "external".to_string(), - protocol: "ftp".to_string(), - host: "".to_string(), - port: 70_000, - requires_component: Some("unknown".to_string()), - }) - .expect_err("invalid target should fail"); - - assert!(error.iter().any(|item| item.field == "name")); - assert!(error.iter().any(|item| item.field == "host")); - assert!(error.iter().any(|item| item.field == "port")); - assert!(error.iter().any(|item| item.field == "protocol")); - assert!(error.iter().any(|item| item.field == "requires_component")); -} diff --git a/apps/windows-client/src-tauri/tests/helper_tests.rs b/apps/windows-client/src-tauri/tests/helper_tests.rs deleted file mode 100644 index 9ec7aea..0000000 --- a/apps/windows-client/src-tauri/tests/helper_tests.rs +++ /dev/null @@ -1,139 +0,0 @@ -#[path = "../src/helper.rs"] -mod helper; -#[path = "../src/models.rs"] -mod models; - -use helper::{ - helper_action_requires_elevation, install_request, parse_helper_response, - proxifyre_apply_request, service_request, HelperAction, HelperCommandOutput, - HelperCommandRunner, HelperCommandSpec, HelperError, HelperResponse, StructuredHelper, -}; -use models::ComponentId; -use serde_json::json; -use std::cell::RefCell; -use std::path::PathBuf; - -#[test] -fn structured_helper_serializes_request_and_parses_json_response() { - let runner = MockRunner { - output: HelperCommandOutput { - status_code: 0, - stdout: serde_json::to_string(&HelperResponse { - success: true, - action: HelperAction::ProxyfierApply, - changed: true, - message: "Applied".to_string(), - details: json!({ "serviceName": "ProxiFyreService" }), - }) - .expect("response json"), - stderr: String::new(), - }, - seen: RefCell::new(Vec::new()), - }; - let helper = StructuredHelper::new("vpn-proxy-helper.exe", runner); - - let response = helper - .execute(&proxifyre_apply_request( - r"C:\ProgramData\VpnProxy\generated\proxifyre-app-config.json", - "ProxiFyreService", - )) - .expect("helper response"); - - assert!(response.success); - assert_eq!(response.action, HelperAction::ProxyfierApply); - assert_eq!(response.details["serviceName"], "ProxiFyreService"); -} - -#[test] -fn helper_runner_receives_json_stdin_and_elevation_flag() { - let runner = MockRunner { - output: HelperCommandOutput { - status_code: 0, - stdout: r#"{"success":true,"action":"service.restart","changed":true,"message":"Restarted","details":{}}"#.to_string(), - stderr: String::new(), - }, - seen: RefCell::new(Vec::new()), - }; - let helper = StructuredHelper::new("vpn-proxy-helper.exe", runner); - let request = service_request(ComponentId::Proxyfier, HelperAction::ServiceRestart); - - let _ = helper.execute(&request).expect("helper response"); - let seen = helper.runner().seen.borrow(); - let spec = seen.first().expect("runner should be called"); - let stdin: serde_json::Value = serde_json::from_str(&spec.stdin).expect("stdin json"); - - assert_eq!(spec.program, PathBuf::from("vpn-proxy-helper.exe")); - assert_eq!(spec.args, vec!["--json"]); - assert!(spec.requires_elevation); - assert_eq!(stdin["action"], "service.restart"); - assert_eq!(stdin["component"], "proxyfier"); -} - -#[test] -fn install_requests_are_explicit_component_actions() { - let control = install_request(ComponentId::ControlApp); - let proxyfier = install_request(ComponentId::Proxyfier); - let singbox = install_request(ComponentId::Singbox); - - assert_eq!(control.action, HelperAction::InstallControlApp); - assert_eq!(proxyfier.action, HelperAction::InstallProxyfier); - assert_eq!(singbox.action, HelperAction::InstallSingbox); - assert!(helper_action_requires_elevation(&proxyfier.action)); -} - -#[test] -fn apply_request_does_not_encode_installer_action() { - let request = proxifyre_apply_request( - r"C:\ProgramData\VpnProxy\generated\proxifyre-app-config.json", - "ProxiFyreService", - ); - - assert_eq!(request.action, HelperAction::ProxyfierApply); - assert_eq!(request.component, Some(ComponentId::Proxyfier)); - assert_eq!( - request.payload["configPath"], - r"C:\ProgramData\VpnProxy\generated\proxifyre-app-config.json" - ); -} - -#[test] -fn non_json_helper_stdout_is_rejected() { - let error = parse_helper_response("Proxyfier restarted successfully") - .expect_err("raw stdout should not be accepted"); - - assert_eq!(error.code, "helper_response_decode"); -} - -#[test] -fn failed_helper_exit_is_structured_error() { - let runner = MockRunner { - output: HelperCommandOutput { - status_code: 5, - stdout: String::new(), - stderr: "Access denied".to_string(), - }, - seen: RefCell::new(Vec::new()), - }; - let helper = StructuredHelper::new("vpn-proxy-helper.exe", runner); - let error = helper - .execute(&service_request( - ComponentId::Proxyfier, - HelperAction::ServiceRestart, - )) - .expect_err("failed exit should become helper error"); - - assert_eq!(error.code, "helper_exit"); - assert!(error.message.contains("Access denied")); -} - -struct MockRunner { - output: HelperCommandOutput, - seen: RefCell>, -} - -impl HelperCommandRunner for MockRunner { - fn run(&self, spec: &HelperCommandSpec) -> Result { - self.seen.borrow_mut().push(spec.clone()); - Ok(self.output.clone()) - } -} diff --git a/apps/windows-client/src-tauri/tests/proxifyre_adapter_tests.rs b/apps/windows-client/src-tauri/tests/proxifyre_adapter_tests.rs deleted file mode 100644 index 1f67eaf..0000000 --- a/apps/windows-client/src-tauri/tests/proxifyre_adapter_tests.rs +++ /dev/null @@ -1,207 +0,0 @@ -#[path = "../src/models.rs"] -mod models; -#[path = "../src/adapters/proxifyre.rs"] -mod proxifyre; -#[path = "../src/adapters/proxy_router.rs"] -mod proxy_router; - -use models::{ - ComponentId, ComponentState, ComponentStatus, Profile, ProfileItem, ProfileItemType, Protocol, - ProxyProtocol, Target, TargetKind, -}; -use proxifyre::{ProxiFyreAdapter, ProxiFyreConfig, PROXIFYRE_OUTPUT_FILE}; -use proxy_router::{ProxyRouterAdapter, ProxyRouterErrorKind, ProxyRouterRequest}; - -#[test] -fn generates_proxifyre_config_for_discord_external_socks5_target() { - let adapter = ProxiFyreAdapter::default(); - let profiles = vec![discord_profile("home-gateway")]; - let targets = vec![external_socks5_target()]; - let components = vec![missing_singbox_component()]; - - let generated = adapter - .generate_config(ProxyRouterRequest::new(&profiles, &targets, &components)) - .expect("external socks5 target should not require sing-box"); - let config: ProxiFyreConfig = - serde_json::from_str(&generated.contents).expect("generated config json"); - - assert_eq!(generated.adapter_id, "proxifyre"); - assert_eq!(generated.output_file_name, PROXIFYRE_OUTPUT_FILE); - assert_eq!(generated.enabled_profiles, 1); - assert_eq!(generated.routed_apps, 1); - assert_eq!(config.log_level, "Info"); - assert!(config.bypass_lan); - assert_eq!(config.proxies.len(), 1); - assert_eq!(config.proxies[0].app_names, vec!["Discord"]); - assert_eq!( - config.proxies[0].socks5_proxy_endpoint, - "192.168.50.111:8080" - ); - assert_eq!(config.proxies[0].supported_protocols, vec!["TCP", "UDP"]); -} - -#[test] -fn skips_disabled_profiles_when_generating_proxifyre_config() { - let adapter = ProxiFyreAdapter::default(); - let mut disabled = discord_profile("home-gateway"); - disabled.enabled = false; - let profiles = vec![disabled]; - let targets = vec![external_socks5_target()]; - let components = Vec::new(); - - let generated = adapter - .generate_config(ProxyRouterRequest::new(&profiles, &targets, &components)) - .expect("disabled profiles should produce empty config"); - let config: ProxiFyreConfig = - serde_json::from_str(&generated.contents).expect("generated config json"); - - assert_eq!(generated.enabled_profiles, 0); - assert_eq!(generated.routed_apps, 0); - assert!(config.proxies.is_empty()); -} - -#[test] -fn includes_folder_paths_when_generating_proxifyre_config() { - let adapter = ProxiFyreAdapter::default(); - let mut profile = discord_profile("home-gateway"); - profile.items.push(ProfileItem { - item_type: ProfileItemType::Folder, - value: r"C:\Games\MyGame".to_string(), - recursive: true, - }); - let profiles = vec![profile]; - let targets = vec![external_socks5_target()]; - let components = Vec::new(); - - let generated = adapter - .generate_config(ProxyRouterRequest::new(&profiles, &targets, &components)) - .expect("folder paths should be accepted by ProxiFyre config generation"); - let config: ProxiFyreConfig = - serde_json::from_str(&generated.contents).expect("generated config json"); - - assert_eq!(generated.routed_apps, 2); - assert_eq!( - config.proxies[0].app_names, - vec!["Discord", r"C:\Games\MyGame"] - ); -} - -#[test] -fn blocks_local_singbox_target_when_required_component_is_missing() { - let adapter = ProxiFyreAdapter::default(); - let profiles = vec![discord_profile("local-singbox")]; - let targets = vec![local_singbox_target()]; - let components = Vec::new(); - - let error = adapter - .generate_config(ProxyRouterRequest::new(&profiles, &targets, &components)) - .expect_err("local sing-box target should require installed running sing-box"); - - assert_eq!(error.kind, ProxyRouterErrorKind::MissingRequiredComponent); -} - -#[test] -fn local_singbox_target_generates_when_required_component_is_running() { - let adapter = ProxiFyreAdapter::default(); - let profiles = vec![discord_profile("local-singbox")]; - let targets = vec![local_singbox_target()]; - let components = vec![running_singbox_component()]; - - let generated = adapter - .generate_config(ProxyRouterRequest::new(&profiles, &targets, &components)) - .expect("running sing-box should satisfy local target dependency"); - let config: ProxiFyreConfig = - serde_json::from_str(&generated.contents).expect("generated config json"); - - assert_eq!(config.proxies.len(), 1); - assert_eq!(config.proxies[0].socks5_proxy_endpoint, "127.0.0.1:1080"); -} - -#[test] -fn rejects_http_target_because_proxifyre_adapter_is_socks5_only() { - let adapter = ProxiFyreAdapter::default(); - let profiles = vec![discord_profile("office-http")]; - let targets = vec![Target { - id: "office-http".to_string(), - name: "Office HTTP".to_string(), - kind: TargetKind::External, - protocol: ProxyProtocol::Http, - host: "192.168.50.111".to_string(), - port: 3128, - requires_component: None, - }]; - let components = Vec::new(); - - let error = adapter - .generate_config(ProxyRouterRequest::new(&profiles, &targets, &components)) - .expect_err("ProxiFyre should reject HTTP targets"); - - assert_eq!(error.kind, ProxyRouterErrorKind::UnsupportedTargetProtocol); -} - -fn discord_profile(target_id: &str) -> Profile { - Profile { - id: "discord".to_string(), - name: "Discord".to_string(), - enabled: true, - target_id: target_id.to_string(), - protocols: vec![Protocol::Tcp, Protocol::Udp], - items: vec![ProfileItem { - item_type: ProfileItemType::Process, - value: "Discord".to_string(), - recursive: false, - }], - } -} - -fn external_socks5_target() -> Target { - Target { - id: "home-gateway".to_string(), - name: "Home Gateway".to_string(), - kind: TargetKind::External, - protocol: ProxyProtocol::Socks5, - host: "192.168.50.111".to_string(), - port: 8080, - requires_component: None, - } -} - -fn local_singbox_target() -> Target { - Target { - id: "local-singbox".to_string(), - name: "Local sing-box".to_string(), - kind: TargetKind::Local, - protocol: ProxyProtocol::Socks5, - host: "127.0.0.1".to_string(), - port: 1080, - requires_component: Some(ComponentId::Singbox), - } -} - -fn missing_singbox_component() -> ComponentStatus { - ComponentStatus { - id: ComponentId::Singbox, - name: "Local sing-box".to_string(), - state: ComponentState::Missing, - installed: false, - running: false, - version: None, - path: None, - problems: vec!["Local sing-box is not installed".to_string()], - actions: vec!["Install Local sing-box".to_string()], - } -} - -fn running_singbox_component() -> ComponentStatus { - ComponentStatus { - id: ComponentId::Singbox, - name: "Local sing-box".to_string(), - state: ComponentState::Running, - installed: true, - running: true, - version: Some("1.11.0".to_string()), - path: Some(r"C:\Tools\VpnProxy\sing-box\sing-box.exe".to_string()), - problems: Vec::new(), - actions: vec!["Restart".to_string(), "Stop".to_string()], - } -} diff --git a/apps/windows-client/src-tauri/tests/singbox_adapter_tests.rs b/apps/windows-client/src-tauri/tests/singbox_adapter_tests.rs deleted file mode 100644 index 9020d62..0000000 --- a/apps/windows-client/src-tauri/tests/singbox_adapter_tests.rs +++ /dev/null @@ -1,294 +0,0 @@ -#[path = "../src/models.rs"] -mod models; -#[path = "../src/adapters/proxifyre.rs"] -mod proxifyre; -#[path = "../src/adapters/proxy_router.rs"] -mod proxy_router; -#[path = "../src/adapters/singbox.rs"] -mod singbox; - -use models::{ - ComponentId, ComponentState, ComponentStatus, LocalSingBoxConfig, Profile, ProfileItem, - ProfileItemType, Protocol, ProxyProtocol, SubscriptionCache, SubscriptionServer, Target, - TargetKind, -}; -use proxifyre::{ProxiFyreAdapter, ProxiFyreConfig}; -use proxy_router::{ProxyRouterAdapter, ProxyRouterRequest}; -use singbox::{ - SingBoxAdapter, SingBoxCheckResult, SingBoxConfigChecker, SingBoxConfigError, - SingBoxConfigErrorKind, SingBoxGenerationRequest, DEFAULT_VPN_OUTBOUND_TAG, - SINGBOX_OUTPUT_FILE, -}; -use std::{ - cell::RefCell, - path::{Path, PathBuf}, -}; - -#[test] -fn generates_selected_outbound_config_and_runs_check_when_binary_path_is_supplied() { - let adapter = SingBoxAdapter::default(); - let config = local_singbox_config("nl-1"); - let cache = subscription_cache(); - let checker = RecordingChecker::ok("configuration OK"); - let binary_path = Path::new(r"C:\Tools\VpnProxy\sing-box\sing-box.exe"); - - let generated = adapter - .generate_config( - SingBoxGenerationRequest::new(&config, &cache, Some(binary_path)), - &checker, - ) - .expect("selected outbound should generate config"); - let generated_config: serde_json::Value = - serde_json::from_str(&generated.contents).expect("generated sing-box json"); - - assert_eq!(generated.adapter_id, "singbox"); - assert_eq!(generated.output_file_name, SINGBOX_OUTPUT_FILE); - assert_eq!(generated.selected_server_tag, "nl-1"); - assert_eq!(generated.listen, "127.0.0.1"); - assert_eq!(generated.listen_port, 1080); - assert_eq!( - generated.check, - Some(SingBoxCheckResult { - checked: true, - success: true, - message: "configuration OK".to_string(), - }) - ); - assert_eq!(generated_config["log"]["level"], "info"); - assert_eq!(generated_config["inbounds"][0]["type"], "mixed"); - assert_eq!(generated_config["inbounds"][0]["listen"], "127.0.0.1"); - assert_eq!(generated_config["inbounds"][0]["listen_port"], 1080); - assert_eq!(generated_config["inbounds"][0]["set_system_proxy"], false); - assert_eq!(generated_config["outbounds"][0]["type"], "vless"); - assert_eq!( - generated_config["outbounds"][0]["tag"], - DEFAULT_VPN_OUTBOUND_TAG - ); - assert_eq!( - generated_config["outbounds"][0]["server"], - "nl.example.test" - ); - assert_eq!(generated_config["outbounds"][0]["packet_encoding"], "xudp"); - assert_eq!(generated_config["route"]["final"], DEFAULT_VPN_OUTBOUND_TAG); - let calls = checker.calls.borrow(); - assert_eq!(calls.len(), 1); - assert_eq!(calls[0].0.as_path(), binary_path); - assert!(calls[0].1.contains(r#""type": "mixed""#)); - assert!(calls[0].1.contains(r#""tag": "vpn""#)); -} - -#[test] -fn skips_singbox_check_when_binary_path_is_not_supplied() { - let adapter = SingBoxAdapter::default(); - let config = local_singbox_config("nl-1"); - let cache = subscription_cache(); - let checker = RecordingChecker::ok("should not run"); - - let generated = adapter - .generate_config( - SingBoxGenerationRequest::new(&config, &cache, None), - &checker, - ) - .expect("binary path is optional"); - - assert_eq!(generated.check, None); - assert!(checker.calls.borrow().is_empty()); -} - -#[test] -fn blocks_config_when_server_is_not_selected() { - let adapter = SingBoxAdapter::default(); - let mut config = local_singbox_config("nl-1"); - config.selected_server_tag = None; - let cache = subscription_cache(); - let checker = RecordingChecker::ok("should not run"); - - let error = adapter - .generate_config( - SingBoxGenerationRequest::new(&config, &cache, None), - &checker, - ) - .expect_err("missing selected server should block config"); - - assert_eq!(error.kind, SingBoxConfigErrorKind::MissingSelectedServer); - assert!(checker.calls.borrow().is_empty()); -} - -#[test] -fn blocks_config_when_selected_outbound_is_missing() { - let adapter = SingBoxAdapter::default(); - let config = local_singbox_config("missing-server"); - let cache = subscription_cache(); - let checker = RecordingChecker::ok("should not run"); - - let error = adapter - .generate_config( - SingBoxGenerationRequest::new(&config, &cache, None), - &checker, - ) - .expect_err("missing outbound should block config"); - - assert_eq!(error.kind, SingBoxConfigErrorKind::MissingSelectedOutbound); - assert!(error.message.contains("missing-server")); - assert!(checker.calls.borrow().is_empty()); -} - -#[test] -fn propagates_failed_singbox_check_as_structured_error() { - let adapter = SingBoxAdapter::default(); - let config = local_singbox_config("nl-1"); - let cache = subscription_cache(); - let checker = RecordingChecker::err("invalid config"); - - let error = adapter - .generate_config( - SingBoxGenerationRequest::new(&config, &cache, Some(Path::new("sing-box.exe"))), - &checker, - ) - .expect_err("failed sing-box check should block generated config"); - - assert_eq!(error.kind, SingBoxConfigErrorKind::CheckFailed); - assert!(error.message.contains("invalid config")); -} - -#[test] -fn external_proxifyre_apply_does_not_require_singbox_component() { - let adapter = ProxiFyreAdapter::default(); - let profiles = vec![discord_profile("home-gateway")]; - let targets = vec![external_socks5_target()]; - let components = vec![missing_singbox_component()]; - - let generated = adapter - .generate_config(ProxyRouterRequest::new(&profiles, &targets, &components)) - .expect("external SOCKS5 target should not require local sing-box"); - let config: ProxiFyreConfig = - serde_json::from_str(&generated.contents).expect("generated proxifyre json"); - - assert_eq!(config.proxies.len(), 1); - assert_eq!( - config.proxies[0].socks5_proxy_endpoint, - "192.168.50.111:8080" - ); -} - -struct RecordingChecker { - calls: RefCell>, - result: Result, -} - -impl RecordingChecker { - fn ok(message: &str) -> Self { - Self { - calls: RefCell::new(Vec::new()), - result: Ok(SingBoxCheckResult { - checked: true, - success: true, - message: message.to_string(), - }), - } - } - - fn err(message: &str) -> Self { - Self { - calls: RefCell::new(Vec::new()), - result: Err(SingBoxConfigError::new( - SingBoxConfigErrorKind::CheckFailed, - message, - )), - } - } -} - -impl SingBoxConfigChecker for RecordingChecker { - fn check_config( - &self, - binary_path: &Path, - config_json: &str, - ) -> Result { - self.calls - .borrow_mut() - .push((binary_path.to_path_buf(), config_json.to_string())); - self.result.clone() - } -} - -fn local_singbox_config(selected_server_tag: &str) -> LocalSingBoxConfig { - LocalSingBoxConfig { - subscription_url: Some("https://sub.example.test/list".to_string()), - selected_server_tag: Some(selected_server_tag.to_string()), - listen_host: "127.0.0.1".to_string(), - listen_port: 1080, - service_name: "VpnProxySingBox".to_string(), - install_root: r"C:\Program Files\VpnProxy\sing-box".to_string(), - updated_at: Some("2026-07-07T10:00:00Z".to_string()), - } -} - -fn subscription_cache() -> SubscriptionCache { - SubscriptionCache { - config: serde_json::json!({ - "outbounds": [ - { - "type": "vless", - "tag": "nl-1", - "server": "nl.example.test", - "server_port": 443, - "uuid": "11111111-1111-1111-1111-111111111111" - }, - { - "type": "direct", - "tag": "direct" - } - ] - }), - servers: vec![SubscriptionServer { - tag: "nl-1".to_string(), - server_type: "vless".to_string(), - server: "nl.example.test".to_string(), - server_port: 443, - }], - user_info: serde_json::Map::new(), - fetched_at: "2026-07-07T10:00:00Z".to_string(), - } -} - -fn discord_profile(target_id: &str) -> Profile { - Profile { - id: "discord".to_string(), - name: "Discord".to_string(), - enabled: true, - target_id: target_id.to_string(), - protocols: vec![Protocol::Tcp, Protocol::Udp], - items: vec![ProfileItem { - item_type: ProfileItemType::Process, - value: "Discord".to_string(), - recursive: false, - }], - } -} - -fn external_socks5_target() -> Target { - Target { - id: "home-gateway".to_string(), - name: "Home Gateway".to_string(), - kind: TargetKind::External, - protocol: ProxyProtocol::Socks5, - host: "192.168.50.111".to_string(), - port: 8080, - requires_component: None, - } -} - -fn missing_singbox_component() -> ComponentStatus { - ComponentStatus { - id: ComponentId::Singbox, - name: "Local sing-box".to_string(), - state: ComponentState::Missing, - installed: false, - running: false, - version: None, - path: None, - problems: vec!["Local sing-box is not installed".to_string()], - actions: vec!["Install Local sing-box".to_string()], - } -} diff --git a/apps/windows-client/src-tauri/tests/singbox_command_tests.rs b/apps/windows-client/src-tauri/tests/singbox_command_tests.rs deleted file mode 100644 index 6154274..0000000 --- a/apps/windows-client/src-tauri/tests/singbox_command_tests.rs +++ /dev/null @@ -1,397 +0,0 @@ -#[path = "../src/activity.rs"] -mod activity; -#[path = "../src/commands.rs"] -mod commands; -#[path = "../src/component_detection.rs"] -mod component_detection; -#[path = "../src/models.rs"] -mod models; -#[path = "../src/adapters/proxifyre.rs"] -mod proxifyre; -#[path = "../src/adapters/proxy_router.rs"] -mod proxy_router; -#[path = "../src/adapters/singbox.rs"] -mod singbox; -#[path = "../src/singbox_service.rs"] -mod singbox_service; -#[path = "../src/storage.rs"] -mod storage; -#[path = "../src/subscription.rs"] -mod subscription; -#[path = "../src/validation.rs"] -mod validation; - -use commands::{ - fetch_singbox_subscription_with_fetcher, forget_singbox_subscription_in_storage, - generate_singbox_config_with_services, save_singbox_subscription_to_storage, - select_singbox_server_in_storage, Clock, SaveSingBoxSubscriptionInputDto, - SelectSingBoxServerInputDto, SubscriptionFetcher, -}; -use models::{ - ActivityLevel, ComponentId, LocalSingBoxConfig, ProxyProtocol, SubscriptionCache, - SubscriptionServer, TargetKind, -}; -use serde_json::{json, Map}; -use singbox::{SingBoxAdapter, SingBoxCheckResult, SingBoxConfigChecker, SingBoxConfigError}; -use std::fs; -use std::path::{Path, PathBuf}; -use std::time::{SystemTime, UNIX_EPOCH}; -use storage::JsonStorage; - -#[test] -fn saves_subscription_url_without_exposing_secret_query() { - let root = test_root("save-subscription"); - let storage = JsonStorage::new(root.clone()); - - let status = save_singbox_subscription_to_storage( - &storage, - SaveSingBoxSubscriptionInputDto { - subscription_url: " https://sub.example.test/path?token=secret ".to_string(), - }, - &FixedClock, - ) - .expect("subscription URL should be saved"); - let config = storage - .read_local_singbox_config() - .expect("read local sing-box config"); - - assert_eq!( - config.subscription_url, - Some("https://sub.example.test/path?token=secret".to_string()) - ); - assert!(status.config.has_subscription); - assert_eq!( - status.config.subscription_display_url, - Some("https://sub.example.test/...".to_string()) - ); - - cleanup(&root); -} - -#[test] -fn rejects_non_http_subscription_url() { - let root = test_root("invalid-subscription"); - let storage = JsonStorage::new(root.clone()); - - let error = save_singbox_subscription_to_storage( - &storage, - SaveSingBoxSubscriptionInputDto { - subscription_url: "file:///C:/sub.txt".to_string(), - }, - &FixedClock, - ) - .expect_err("non-http subscription URL should fail"); - - assert_eq!(error.code, "singbox_subscription_url_invalid"); - - cleanup(&root); -} - -#[test] -fn fetches_subscription_cache_and_selects_first_server() { - let root = test_root("fetch-subscription"); - let storage = JsonStorage::new(root.clone()); - save_singbox_subscription_to_storage( - &storage, - SaveSingBoxSubscriptionInputDto { - subscription_url: "https://sub.example.test/path?token=secret".to_string(), - }, - &FixedClock, - ) - .expect("save subscription URL"); - - let status = fetch_singbox_subscription_with_fetcher( - &storage, - &MockFetcher(sample_cache()), - &FixedClock, - ) - .expect("fetch subscription through mock"); - let config = storage - .read_local_singbox_config() - .expect("read local sing-box config"); - let cache = storage - .read_singbox_subscription_cache() - .expect("read cache") - .expect("cache should exist"); - let activity = storage.read_activity().expect("read activity"); - - assert_eq!(status.config.selected_server_tag, Some("nl-1".to_string())); - assert_eq!(status.cache.expect("status cache").servers.len(), 2); - assert_eq!(config.selected_server_tag, Some("nl-1".to_string())); - assert_eq!(cache.servers.len(), 2); - assert_eq!(activity[0].level, ActivityLevel::Success); - assert_eq!(activity[0].title, "Подписка Local sing-box обновлена"); - - cleanup(&root); -} - -#[test] -fn selects_server_from_cached_subscription() { - let root = test_root("select-server"); - let storage = JsonStorage::new(root.clone()); - storage - .write_singbox_subscription_cache(&sample_cache()) - .expect("write cache"); - - let status = select_singbox_server_in_storage( - &storage, - SelectSingBoxServerInputDto { - tag: "de-1".to_string(), - server: None, - server_port: None, - }, - &FixedClock, - ) - .expect("server should be selected"); - let config = storage - .read_local_singbox_config() - .expect("read local sing-box config"); - - assert_eq!(status.config.selected_server_tag, Some("de-1".to_string())); - assert_eq!(config.selected_server_tag, Some("de-1".to_string())); - - cleanup(&root); -} - -#[test] -fn selects_server_by_endpoint_when_display_tag_is_sanitized() { - let root = test_root("select-server-sanitized-tag"); - let storage = JsonStorage::new(root.clone()); - storage - .write_singbox_subscription_cache(&sample_cache_with_flag_tag()) - .expect("write cache"); - - let status = select_singbox_server_in_storage( - &storage, - SelectSingBoxServerInputDto { - tag: "Умный".to_string(), - server: Some("media.example.test".to_string()), - server_port: Some(443), - }, - &FixedClock, - ) - .expect("server should be selected by endpoint fallback"); - let config = storage - .read_local_singbox_config() - .expect("read local sing-box config"); - - assert_eq!( - status.config.selected_server_tag, - Some("Умный 🇳🇱->🇷🇺".to_string()) - ); - assert_eq!(config.selected_server_tag, Some("Умный 🇳🇱->🇷🇺".to_string())); - - cleanup(&root); -} - -#[test] -fn generate_writes_config_and_local_singbox_target() { - let root = test_root("generate-config"); - let storage = JsonStorage::new(root.clone()); - storage - .write_local_singbox_config(&LocalSingBoxConfig { - subscription_url: Some("https://sub.example.test/path".to_string()), - selected_server_tag: Some("nl-1".to_string()), - ..LocalSingBoxConfig::default() - }) - .expect("write local sing-box config"); - storage - .write_singbox_subscription_cache(&sample_cache()) - .expect("write cache"); - - let response = generate_singbox_config_with_services( - &storage, - &SingBoxAdapter::default(), - &MockChecker, - &FixedClock, - Some(Path::new("sing-box.exe")), - ) - .expect("generate sing-box config"); - let generated = fs::read_to_string(&response.generated_config_path).expect("read generated"); - let targets = storage.read_targets().expect("read targets"); - let target = targets - .iter() - .find(|target| target.id == "local-singbox") - .expect("local sing-box target"); - let activity = storage.read_activity().expect("read activity"); - - assert!(response.success); - assert_eq!(response.adapter_id, "singbox"); - assert_eq!(response.selected_server_tag, "nl-1"); - assert!(response.check.expect("check result").success); - assert!(generated.contains("\"type\": \"mixed\"")); - assert!(generated.contains("\"tag\": \"vpn\"")); - assert_eq!(target.kind, TargetKind::Local); - assert_eq!(target.protocol, ProxyProtocol::Socks5); - assert_eq!(target.host, "127.0.0.1"); - assert_eq!(target.port, 1080); - assert_eq!(target.requires_component, Some(ComponentId::Singbox)); - assert_eq!(activity[0].title, "Конфиг Local sing-box создан"); - - cleanup(&root); -} - -#[test] -fn generate_requires_cached_subscription() { - let root = test_root("generate-missing-cache"); - let storage = JsonStorage::new(root.clone()); - - let error = generate_singbox_config_with_services( - &storage, - &SingBoxAdapter::default(), - &MockChecker, - &FixedClock, - None, - ) - .expect_err("missing cache should block generation"); - - assert_eq!(error.code, "singbox_subscription_cache_missing"); - - cleanup(&root); -} - -#[test] -fn forget_subscription_clears_url_selection_and_cache() { - let root = test_root("forget-subscription"); - let storage = JsonStorage::new(root.clone()); - storage - .write_local_singbox_config(&LocalSingBoxConfig { - subscription_url: Some("https://sub.example.test/path".to_string()), - selected_server_tag: Some("nl-1".to_string()), - ..LocalSingBoxConfig::default() - }) - .expect("write local sing-box config"); - storage - .write_singbox_subscription_cache(&sample_cache()) - .expect("write cache"); - - let status = - forget_singbox_subscription_in_storage(&storage, &FixedClock).expect("forget subscription"); - let config = storage - .read_local_singbox_config() - .expect("read local sing-box config"); - let cache = storage - .read_singbox_subscription_cache() - .expect("read cache"); - - assert!(!status.config.has_subscription); - assert_eq!(config.subscription_url, None); - assert_eq!(config.selected_server_tag, None); - assert_eq!(cache, None); - - cleanup(&root); -} - -struct MockFetcher(SubscriptionCache); - -impl SubscriptionFetcher for MockFetcher { - fn fetch_subscription( - &self, - url: &str, - ) -> Result { - assert_eq!(url, "https://sub.example.test/path?token=secret"); - Ok(self.0.clone()) - } -} - -struct MockChecker; - -impl SingBoxConfigChecker for MockChecker { - fn check_config( - &self, - binary_path: &Path, - config_json: &str, - ) -> Result { - assert_eq!(binary_path, Path::new("sing-box.exe")); - assert!(config_json.contains("\"final\": \"vpn\"")); - Ok(SingBoxCheckResult { - checked: true, - success: true, - message: "mock check passed".to_string(), - }) - } -} - -struct FixedClock; - -impl Clock for FixedClock { - fn now(&self) -> String { - "2026-07-07T00:00:00Z".to_string() - } -} - -fn sample_cache() -> SubscriptionCache { - SubscriptionCache { - config: json!({ - "outbounds": [ - { - "type": "vless", - "tag": "nl-1", - "server": "nl.example.test", - "server_port": 443, - "uuid": "11111111-1111-1111-1111-111111111111" - }, - { - "type": "trojan", - "tag": "de-1", - "server": "de.example.test", - "server_port": 443, - "password": "secret" - } - ] - }), - servers: vec![ - SubscriptionServer { - tag: "nl-1".to_string(), - server_type: "vless".to_string(), - server: "nl.example.test".to_string(), - server_port: 443, - }, - SubscriptionServer { - tag: "de-1".to_string(), - server_type: "trojan".to_string(), - server: "de.example.test".to_string(), - server_port: 443, - }, - ], - user_info: Map::new(), - fetched_at: "2026-07-07T00:00:00Z".to_string(), - } -} - -fn sample_cache_with_flag_tag() -> SubscriptionCache { - SubscriptionCache { - config: json!({ - "outbounds": [ - { - "type": "vless", - "tag": "Умный 🇳🇱->🇷🇺", - "server": "media.example.test", - "server_port": 443, - "uuid": "11111111-1111-1111-1111-111111111111" - } - ] - }), - servers: vec![SubscriptionServer { - tag: "Умный 🇳🇱->🇷🇺".to_string(), - server_type: "vless".to_string(), - server: "media.example.test".to_string(), - server_port: 443, - }], - user_info: Map::new(), - fetched_at: "2026-07-07T00:00:00Z".to_string(), - } -} - -fn test_root(name: &str) -> PathBuf { - let timestamp = SystemTime::now() - .duration_since(UNIX_EPOCH) - .expect("system clock before unix epoch") - .as_nanos(); - - std::env::temp_dir().join(format!("vpn-proxy-singbox-commands-{name}-{timestamp}")) -} - -fn cleanup(root: &Path) { - let _ = fs::remove_dir_all(root); -} diff --git a/apps/windows-client/src-tauri/tests/singbox_service_tests.rs b/apps/windows-client/src-tauri/tests/singbox_service_tests.rs deleted file mode 100644 index d5ac1e6..0000000 --- a/apps/windows-client/src-tauri/tests/singbox_service_tests.rs +++ /dev/null @@ -1,128 +0,0 @@ -#[path = "../src/component_detection.rs"] -mod component_detection; -#[path = "../src/models.rs"] -mod models; -#[path = "../src/singbox_service.rs"] -mod singbox_service; - -use component_detection::DetectedSingBox; -use singbox_service::{ - build_singbox_setup_status, ensure_safe_singbox_install_dir, parse_service_command_output, - service_control_script, SingBoxServiceAction, -}; -use std::path::{Path, PathBuf}; -#[cfg(windows)] -use std::process::Command as ProcessCommand; - -#[test] -fn setup_status_reports_missing_items_when_singbox_is_absent() { - let status = build_singbox_setup_status(None); - - assert!(!status.ready); - assert_eq!(status.missing_count, 3); - assert_eq!(status.items[0].id, "sing-box-binary"); - assert!(status.items[0].details.contains("SagerNet/sing-box")); - assert_eq!(status.items[1].id, "winsw-wrapper"); - assert!(status.items[1].details.contains("winsw/winsw")); - assert_eq!(status.items[2].id, "windows-service"); -} - -#[test] -fn setup_status_reports_ready_when_binary_wrapper_and_service_exist() { - let detected = detected_singbox(true, true, true); - let status = build_singbox_setup_status(Some(&detected)); - - assert!(status.ready); - assert_eq!(status.missing_count, 0); - assert!(status.items.iter().all(|item| item.installed)); -} - -#[test] -fn parses_last_json_service_command_output_line() { - let output = br#" -noise -{"success":true,"code":"started","serviceName":"VpnProxySingBox","status":"Running","processId":42} -"#; - let parsed = parse_service_command_output(output).expect("service json should parse"); - - assert!(parsed.success); - assert_eq!(parsed.code, "started"); - assert_eq!(parsed.service_name, Some("VpnProxySingBox".to_string())); - assert_eq!(parsed.status, Some("Running".to_string())); - assert_eq!(parsed.process_id, Some(42)); -} - -#[test] -fn safe_install_dir_allows_only_vpnproxy_singbox_folder() { - assert!( - ensure_safe_singbox_install_dir(Path::new(r"C:\Program Files\VpnProxy\sing-box")).is_ok() - ); - assert!(ensure_safe_singbox_install_dir(Path::new(r"C:\Windows")).is_err()); - assert!(ensure_safe_singbox_install_dir(Path::new(r"C:\Program Files\sing-box")).is_err()); -} - -#[test] -fn service_control_script_targets_named_service_and_action() { - let script = service_control_script(SingBoxServiceAction::Start, "VpnProxySingBox", None, None); - - assert!(script.contains("$serviceName = 'VpnProxySingBox'")); - assert!(script.contains("$action = 'start'")); - assert!(script.contains("ConvertTo-Json -Compress")); -} - -#[test] -fn service_control_script_syncs_generated_config_before_start() { - let source = Path::new(r"C:\ProgramData\VpnProxy\generated\sing-box-config.json"); - let target = Path::new(r"C:\Program Files\VpnProxy\sing-box\config.json"); - let script = service_control_script( - SingBoxServiceAction::Start, - "VpnProxySingBox", - Some(source), - Some(target), - ); - - assert!(script.contains( - "$configSource = 'C:\\ProgramData\\VpnProxy\\generated\\sing-box-config.json'" - )); - assert!(script.contains( - "$configTarget = 'C:\\Program Files\\VpnProxy\\sing-box\\config.json'" - )); - assert!(script.contains("Copy-Item -LiteralPath $configSource")); - assert!(script.contains("'config_sync_failed'")); -} - -#[test] -#[cfg(windows)] -fn install_singbox_script_parses_as_powershell() { - let script_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")) - .join("..") - .join("scripts") - .join("install-singbox.ps1"); - let escaped_path = script_path.display().to_string().replace('\'', "''"); - let parser = format!( - "$tokens = $null; $errors = $null; [System.Management.Automation.Language.Parser]::ParseFile('{escaped_path}', [ref]$tokens, [ref]$errors) | Out-Null; if ($errors.Count -gt 0) {{ $errors | ForEach-Object {{ $_.Message }}; exit 1 }}" - ); - let output = ProcessCommand::new("powershell") - .args(["-NoProfile", "-NonInteractive", "-Command", &parser]) - .output() - .expect("powershell parser should run"); - - assert!( - output.status.success(), - "install-singbox.ps1 should parse\nstdout:\n{}\nstderr:\n{}", - String::from_utf8_lossy(&output.stdout), - String::from_utf8_lossy(&output.stderr), - ); -} - -fn detected_singbox(binary_exists: bool, wrapper_exists: bool, running: bool) -> DetectedSingBox { - DetectedSingBox { - install_dir: PathBuf::from(r"C:\Program Files\VpnProxy\sing-box"), - executable_path: PathBuf::from(r"C:\Program Files\VpnProxy\sing-box\sing-box.exe"), - wrapper_path: PathBuf::from(r"C:\Program Files\VpnProxy\sing-box\VpnProxySingBox.exe"), - binary_exists, - wrapper_exists, - running, - service_name: "VpnProxySingBox".to_string(), - } -} diff --git a/apps/windows-client/src-tauri/tests/storage_tests.rs b/apps/windows-client/src-tauri/tests/storage_tests.rs deleted file mode 100644 index 3fe369d..0000000 --- a/apps/windows-client/src-tauri/tests/storage_tests.rs +++ /dev/null @@ -1,302 +0,0 @@ -#[path = "../src/activity.rs"] -mod activity; -#[path = "../src/models.rs"] -mod models; -#[path = "../src/storage.rs"] -mod storage; - -use models::{ - ActivityEntry, ActivityLevel, ComponentId, ComponentState, ComponentStatus, LocalSingBoxConfig, - Profile, ProfileItem, ProfileItemType, Protocol, ProxyProtocol, SubscriptionCache, - SubscriptionServer, Target, TargetKind, -}; -use std::fs; -use std::path::{Path, PathBuf}; -use std::time::{SystemTime, UNIX_EPOCH}; -use storage::{backup_path, default_config_root, JsonStorage, StoragePaths}; - -#[test] -fn storage_defaults_to_programdata_root() { - let expected = PathBuf::from(r"C:\ProgramData\VpnProxy"); - - assert_eq!(default_config_root(), expected); - assert_eq!(StoragePaths::default().root, expected); -} - -#[test] -fn roundtrips_profiles_targets_components_and_activity() { - let root = test_root("roundtrip"); - let storage = JsonStorage::new(root.clone()); - - let profiles = vec![sample_profile("discord")]; - let targets = vec![sample_target("home-gateway")]; - let components = vec![sample_component()]; - let activity = vec![sample_activity( - "created", - "2026-01-01T10:00:00Z", - ActivityLevel::Success, - )]; - - storage.write_profiles(&profiles).expect("write profiles"); - storage.write_targets(&targets).expect("write targets"); - storage - .write_components(&components) - .expect("write components"); - storage.write_activity(&activity).expect("write activity"); - - assert_eq!(storage.read_profiles().expect("read profiles"), profiles); - assert_eq!(storage.read_targets().expect("read targets"), targets); - assert_eq!( - storage.read_components().expect("read components"), - components - ); - assert_eq!(storage.read_activity().expect("read activity"), activity); - - cleanup(&root); -} - -#[test] -fn roundtrips_local_singbox_config_and_subscription_cache() { - let root = test_root("local-singbox"); - let storage = JsonStorage::new(root.clone()); - let config = LocalSingBoxConfig { - subscription_url: Some("https://sub.example.test/path?token=secret".to_string()), - selected_server_tag: Some("nl-1".to_string()), - listen_host: "127.0.0.1".to_string(), - listen_port: 1080, - service_name: "VpnProxySingBox".to_string(), - install_root: r"C:\Program Files\VpnProxy\sing-box".to_string(), - updated_at: Some("2026-07-07T10:00:00Z".to_string()), - }; - let cache = sample_subscription_cache(); - - storage - .write_local_singbox_config(&config) - .expect("write local sing-box config"); - storage - .write_singbox_subscription_cache(&cache) - .expect("write subscription cache"); - - assert_eq!( - storage - .read_local_singbox_config() - .expect("read local sing-box config"), - config - ); - assert_eq!( - storage - .read_singbox_subscription_cache() - .expect("read subscription cache"), - Some(cache) - ); - assert_eq!( - config.subscription_display_url(), - Some("https://sub.example.test/...".to_string()) - ); - - cleanup(&root); -} - -#[test] -fn missing_local_singbox_config_defaults_to_optional_empty_state() { - let root = test_root("local-singbox-default"); - let storage = JsonStorage::new(root.clone()); - let config = storage - .read_local_singbox_config() - .expect("read default local sing-box config"); - - assert_eq!(config.subscription_url, None); - assert_eq!(config.selected_server_tag, None); - assert_eq!(config.listen_host, "127.0.0.1"); - assert_eq!(config.listen_port, 1080); - assert_eq!(config.service_name, "VpnProxySingBox"); - - cleanup(&root); -} - -#[test] -fn invalid_subscription_cache_falls_back_to_none() { - let root = test_root("invalid-subscription-cache"); - let storage = JsonStorage::new(root.clone()); - storage.ensure_dirs().expect("create storage dirs"); - fs::write( - &storage.paths().singbox_subscription_cache_file, - "{not valid json", - ) - .expect("write invalid cache"); - - assert_eq!( - storage - .read_singbox_subscription_cache() - .expect("invalid cache fallback"), - None - ); - - cleanup(&root); -} - -#[test] -fn invalid_json_falls_back_to_empty_collection() { - let root = test_root("invalid-json"); - let storage = JsonStorage::new(root.clone()); - storage.ensure_dirs().expect("create storage dirs"); - fs::write(&storage.paths().profiles_file, "{not valid json").expect("write invalid json"); - - assert_eq!( - storage.read_profiles().expect("invalid profiles fallback"), - Vec::::new() - ); - - cleanup(&root); -} - -#[test] -fn write_creates_backup_before_overwriting_source_file() { - let root = test_root("backup"); - let storage = JsonStorage::new(root.clone()); - let first = vec![sample_profile("first")]; - let second = vec![sample_profile("second")]; - - storage.write_profiles(&first).expect("first write"); - storage.write_profiles(&second).expect("second write"); - - let backup = backup_path(&storage.paths().profiles_file); - assert!(backup.exists(), "backup file should exist"); - - let backup_contents = fs::read_to_string(backup).expect("read backup"); - let backup_profiles: Vec = - serde_json::from_str(&backup_contents).expect("backup json"); - - assert_eq!(backup_profiles, first); - assert_eq!(storage.read_profiles().expect("current profiles"), second); - - cleanup(&root); -} - -#[test] -fn activity_entries_are_sorted_and_capped() { - let root = test_root("activity"); - let storage = JsonStorage::with_activity_limit(root.clone(), 2); - - storage - .append_activity(sample_activity( - "old", - "2026-01-01T10:00:00Z", - ActivityLevel::Info, - )) - .expect("append old"); - storage - .append_activity(sample_activity( - "new", - "2026-01-03T10:00:00Z", - ActivityLevel::Success, - )) - .expect("append new"); - storage - .append_activity(sample_activity( - "middle", - "2026-01-02T10:00:00Z", - ActivityLevel::Warning, - )) - .expect("append middle"); - - let entries = storage.read_activity().expect("read capped activity"); - - assert_eq!(entries.len(), 2); - assert_eq!( - entries - .iter() - .map(|entry| entry.id.as_str()) - .collect::>(), - vec!["new", "middle"] - ); - - cleanup(&root); -} - -fn test_root(name: &str) -> PathBuf { - let timestamp = SystemTime::now() - .duration_since(UNIX_EPOCH) - .expect("system clock before unix epoch") - .as_nanos(); - - std::env::temp_dir().join(format!("vpn-proxy-storage-{name}-{timestamp}")) -} - -fn cleanup(root: &Path) { - let _ = fs::remove_dir_all(root); -} - -fn sample_profile(id: &str) -> Profile { - Profile { - id: id.to_string(), - name: format!("Profile {id}"), - enabled: true, - target_id: "home-gateway".to_string(), - protocols: vec![Protocol::Tcp, Protocol::Udp], - items: vec![ProfileItem { - item_type: ProfileItemType::Process, - value: "Discord".to_string(), - recursive: false, - }], - } -} - -fn sample_target(id: &str) -> Target { - Target { - id: id.to_string(), - name: "Home Gateway".to_string(), - kind: TargetKind::External, - protocol: ProxyProtocol::Socks5, - host: "192.168.50.111".to_string(), - port: 8080, - requires_component: None, - } -} - -fn sample_component() -> ComponentStatus { - ComponentStatus { - id: ComponentId::Proxyfier, - name: "ProxiFyre".to_string(), - state: ComponentState::Missing, - installed: false, - running: false, - version: None, - path: None, - problems: vec!["ProxiFyre не установлен".to_string()], - actions: vec!["Установить ProxiFyre".to_string()], - } -} - -fn sample_subscription_cache() -> SubscriptionCache { - SubscriptionCache { - config: serde_json::json!({ - "outbounds": [ - { - "type": "vless", - "tag": "nl-1", - "server": "nl.example.test", - "server_port": 443 - } - ] - }), - servers: vec![SubscriptionServer { - tag: "nl-1".to_string(), - server_type: "vless".to_string(), - server: "nl.example.test".to_string(), - server_port: 443, - }], - user_info: serde_json::Map::new(), - fetched_at: "2026-07-07T10:00:00Z".to_string(), - } -} - -fn sample_activity(id: &str, at: &str, level: ActivityLevel) -> ActivityEntry { - ActivityEntry { - id: id.to_string(), - at: at.to_string(), - level, - title: format!("Activity {id}"), - message: "Storage test activity".to_string(), - } -} diff --git a/apps/windows-client/src-tauri/tests/subscription_tests.rs b/apps/windows-client/src-tauri/tests/subscription_tests.rs deleted file mode 100644 index 3ee69b5..0000000 --- a/apps/windows-client/src-tauri/tests/subscription_tests.rs +++ /dev/null @@ -1,99 +0,0 @@ -#[path = "../src/models.rs"] -mod models; -#[path = "../src/subscription.rs"] -mod subscription; - -use base64::{engine::general_purpose, Engine}; -use models::redact_subscription_url; -use subscription::{parse_subscription_body, parse_user_info}; - -#[test] -fn parses_singbox_json_config_servers() { - let parsed = parse_subscription_body( - r#"{ - "outbounds": [ - { "type": "direct", "tag": "direct" }, - { "type": "vless", "tag": "nl-1", "server": "nl.example.test", "server_port": 443 }, - { "type": "trojan", "tag": "de-1", "server": "de.example.test", "server_port": 8443 } - ] - }"#, - ) - .expect("json subscription should parse"); - - assert_eq!(parsed.servers.len(), 2); - assert_eq!(parsed.servers[0].tag, "nl-1"); - assert_eq!(parsed.servers[0].server_type, "vless"); - assert_eq!(parsed.servers[1].server_port, 8443); -} - -#[test] -fn parses_base64_vless_link_list() { - let link = sample_vless_link("nl-1"); - let encoded = general_purpose::STANDARD.encode(format!("{link}\n")); - - let parsed = parse_subscription_body(&encoded).expect("base64 vless list should parse"); - let outbound = &parsed.config["outbounds"][0]; - - assert_eq!(parsed.servers.len(), 1); - assert_eq!(parsed.servers[0].tag, "nl-1"); - assert_eq!(outbound["type"], "vless"); - assert_eq!(outbound["server"], "nl.example.test"); - assert_eq!(outbound["packet_encoding"], "xudp"); -} - -#[test] -fn rejects_body_without_supported_outbounds() { - let error = parse_subscription_body(r#"{"outbounds":[{"type":"direct","tag":"direct"}]}"#) - .expect_err("unsupported subscription should fail"); - - assert!(error.message.contains("No supported proxy outbounds found")); -} - -#[test] -fn rejects_vless_without_reality_parameters() { - let error = parse_subscription_body("vless://uuid@nl.example.test:443#nl-1") - .expect_err("missing reality params should fail"); - - assert!(error.message.contains("pbk and sid")); -} - -#[test] -fn parses_subscription_user_info_header() { - let user_info = parse_user_info(Some("upload=10; download=20; total=30; expire=bad")); - - assert_eq!(user_info["upload"], 10); - assert_eq!(user_info["download"], 20); - assert_eq!(user_info["total"], 30); - assert!(!user_info.contains_key("expire")); -} - -#[test] -fn rejects_invalid_or_non_http_subscription_url_before_network() { - let invalid = subscription::fetch_subscription("not a url") - .expect_err("invalid url should fail before request"); - let unsupported = subscription::fetch_subscription("file:///C:/subscription.txt") - .expect_err("non-http url should fail before request"); - - assert!(invalid.message.contains("Invalid subscription URL")); - assert!(unsupported.message.contains("http or https")); -} - -#[test] -fn redacts_subscription_url_for_display() { - assert_eq!( - redact_subscription_url("https://sub.example.test/path?token=secret"), - "https://sub.example.test/..." - ); - assert_eq!( - redact_subscription_url("vless://uuid@example.test"), - "vless://uuid@example.test/..." - ); -} - -fn sample_vless_link(tag: &str) -> String { - format!( - "vless://{}@nl.example.test:443?security=reality&sni=example.test&fp=chrome&pbk=public-key&sid=short-id&flow=xtls-rprx-vision#{}", - "11111111-1111-1111-1111-111111111111", - tag - ) -} diff --git a/apps/windows-client/src/api/tauriCommands.ts b/apps/windows-client/src/api/tauriCommands.ts deleted file mode 100644 index ab2bf7b..0000000 --- a/apps/windows-client/src/api/tauriCommands.ts +++ /dev/null @@ -1,235 +0,0 @@ -import { invoke } from '@tauri-apps/api/core'; -import type { - ActivityEntry, - ComponentStatus, - LocalSingBoxConfig, - Profile, - ProfileInput, - SubscriptionCache, - SubscriptionServer, - Target, - TargetInput, -} from '../domain/types'; - -export interface CommandError { - code: string; - message: string; - details?: Array<{ - field: string; - message: string; - }>; -} - -export interface StatusResponse { - routeLine: string; - activeProfileCount: number; - routedAppCount: number; - activeTarget?: Target; - components: ComponentStatus[]; - recentActivity: ActivityEntry[]; - generatedConfigPath: string; -} - -export interface SavedStateResponse { - profiles: Profile[]; - targets: Target[]; - generatedConfigPath: string; -} - -export interface ProxiFyreSetupItem { - id: string; - name: string; - installed: boolean; - version?: string; - details: string; -} - -export interface ProxiFyreSetupStatus { - ready: boolean; - missingCount: number; - items: ProxiFyreSetupItem[]; -} - -export type SingBoxSetupItem = ProxiFyreSetupItem; - -export interface SingBoxSetupStatus { - ready: boolean; - missingCount: number; - items: SingBoxSetupItem[]; -} - -export interface LocalSingBoxStatusResponse { - config: LocalSingBoxConfig; - cache?: SubscriptionCache; - component: ComponentStatus; - generatedConfigPath: string; - lanListenHost?: string; -} - -export interface PingServerResponse { - tag: string; - server: string; - serverPort: number; - ok: boolean; - latency?: number; - error?: string; -} - -export interface GenerateSingBoxConfigResponse { - success: boolean; - message: string; - adapterId: string; - generatedConfigPath: string; - selectedServerTag: string; - listenHost: string; - listenPort: number; - check?: { - checked: boolean; - success: boolean; - message: string; - }; - activity: ActivityEntry; -} - -export interface HelperApplyResult { - success: boolean; - changed: boolean; - action: string; - message: string; -} - -export interface ApplyProfilesResponse { - success: boolean; - changed: boolean; - message: string; - adapterId: string; - generatedConfigPath: string; - enabledProfiles: number; - routedApps: number; - helper: HelperApplyResult; - activity: ActivityEntry; -} - -export function getStatus(): Promise { - return invoke('get_status'); -} - -export function getSavedState(): Promise { - return invoke('get_saved_state'); -} - -export function getProfiles(): Promise { - return invoke('get_profiles'); -} - -export function saveProfile(input: ProfileInput): Promise { - return invoke('save_profile', { input }); -} - -export function getTargets(): Promise { - return invoke('get_targets'); -} - -export function saveTarget(input: TargetInput): Promise { - return invoke('save_target', { input }); -} - -export function getComponents(): Promise { - return invoke('get_components'); -} - -export function getProxiFyreSetupStatus(): Promise { - return invoke('get_proxifyre_setup_status'); -} - -export function getSingBoxStatus(): Promise { - return invoke('get_singbox_status'); -} - -export function getSingBoxSetupStatus(): Promise { - return invoke('get_singbox_setup_status'); -} - -export function saveSingBoxSubscription(subscriptionUrl: string): Promise { - return invoke('save_singbox_subscription', { - input: { subscriptionUrl }, - }); -} - -export function fetchSingBoxSubscription(): Promise { - return invoke('fetch_singbox_subscription'); -} - -export function forgetSingBoxSubscription(): Promise { - return invoke('forget_singbox_subscription'); -} - -export function selectSingBoxServer(server: SubscriptionServer): Promise { - return invoke('select_singbox_server', { - input: { - tag: server.tag, - server: server.server, - serverPort: server.serverPort, - }, - }); -} - -export function pingSingBoxServer(tag: string): Promise { - return invoke('ping_singbox_server', { - input: { tag }, - }); -} - -export function pingAllSingBoxServers(): Promise { - return invoke('ping_all_singbox_servers'); -} - -export function pingProxyTarget(host: string, port: number): Promise { - return invoke('ping_proxy_target', { - input: { host, port }, - }); -} - -export function generateSingBoxConfig(): Promise { - return invoke('generate_singbox_config'); -} - -export function applyProfiles(): Promise { - return invoke('apply_profiles'); -} - -export function openConfigLocation(): Promise { - return invoke('open_config_location'); -} - -export function startProxiFyreService(): Promise { - return invoke('start_proxifyre_service'); -} - -export function stopProxiFyreService(): Promise { - return invoke('stop_proxifyre_service'); -} - -export function installProxiFyre(): Promise { - return invoke('install_proxifyre'); -} - -export function uninstallProxiFyre(): Promise { - return invoke('uninstall_proxifyre'); -} - -export function startSingBoxService(): Promise { - return invoke('start_singbox_service'); -} - -export function stopSingBoxService(): Promise { - return invoke('stop_singbox_service'); -} - -export function installSingBox(): Promise { - return invoke('install_singbox'); -} - -export function uninstallSingBox(): Promise { - return invoke('uninstall_singbox'); -} diff --git a/apps/windows-client/src/app/App.tsx b/apps/windows-client/src/app/App.tsx deleted file mode 100644 index 3022f97..0000000 --- a/apps/windows-client/src/app/App.tsx +++ /dev/null @@ -1,2161 +0,0 @@ -import { useEffect, useMemo, useRef, useState } from 'react'; -import { open } from '@tauri-apps/plugin-dialog'; -import { Cpu, FileCode2, FolderOpen, Gauge, Info, Link2, Trash2, Wand2 } from 'lucide-react'; -import { - applyProfiles, - fetchSingBoxSubscription, - forgetSingBoxSubscription, - generateSingBoxConfig, - getComponents, - getProxiFyreSetupStatus, - getSavedState, - getSingBoxSetupStatus, - getSingBoxStatus, - installProxiFyre, - installSingBox, - openConfigLocation, - pingAllSingBoxServers, - pingProxyTarget, - pingSingBoxServer, - saveProfile, - saveSingBoxSubscription, - saveTarget, - selectSingBoxServer, - startProxiFyreService, - startSingBoxService, - stopProxiFyreService, - stopSingBoxService, - uninstallProxiFyre, - uninstallSingBox, - type ApplyProfilesResponse, - type LocalSingBoxStatusResponse, - type PingServerResponse, - type ProxiFyreSetupStatus, - type SingBoxSetupStatus, -} from '../api/tauriCommands'; -import type { ComponentStatus, Profile, ProfileItemInput, ProfileItemType, SubscriptionServer, Target } from '../domain/types'; -import { Button, IconButton, LogDock, ServiceControlRow, Tabs } from '../ui'; -import { getApplyReadiness } from './readiness'; -import { serviceControlState } from './viewModel'; - -type DraftItemType = Extract; -type ProxiFyreAction = 'start' | 'stop' | 'install' | 'uninstall'; -type SingBoxAction = 'start' | 'stop' | 'install' | 'uninstall' | 'fetch' | 'forget' | 'generate' | 'ping'; -type RouteMode = 'external' | 'local-singbox'; -type ServiceVisualState = 'active' | 'settling' | null; -type PanelId = 'summary' | 'proxifyre' | 'proxy'; -type StatusTone = 'ok' | 'warning' | 'error' | 'checking' | 'muted'; -type TabTransitionDirection = 'left' | 'right'; - -interface DraftItem { - id: string; - type: DraftItemType; - value: string; -} - -interface Notice { - kind: 'success' | 'error' | 'info'; - title: string; - text: string; -} - -interface LogEntry extends Notice { - id: string; - at: number; -} - -interface ConfigSnapshotItem { - type: DraftItemType; - value: string; -} - -interface ConfigSnapshot { - routeMode: RouteMode; - proxy: string; - selectedServerTag: string; - items: ConfigSnapshotItem[]; -} - -const MAIN_TARGET_ID = 'main-proxy'; -const MAIN_PROFILE_ID = 'main-profile'; -const LOCAL_SINGBOX_TARGET_ID = 'local-singbox'; -const LOG_VISIBLE_MS = 6500; -const PANEL_ORDER: PanelId[] = ['summary', 'proxifyre', 'proxy']; - -const fallbackComponents: ComponentStatus[] = [ - { - id: 'proxyfier', - name: 'ProxiFyre', - state: 'missing', - installed: false, - running: false, - problems: ['ProxiFyre не найден'], - actions: [], - }, - { - id: 'singbox', - name: 'Локальный sing-box', - state: 'missing', - installed: false, - running: false, - problems: [], - actions: [], - }, -]; - -export function App() { - const [activePanel, setActivePanel] = useState('summary'); - const [tabTransitionDirection, setTabTransitionDirection] = useState('right'); - const [proxyInput, setProxyInput] = useState(''); - const [routeMode, setRouteMode] = useState('external'); - const [profileId, setProfileId] = useState(MAIN_PROFILE_ID); - const [targetId, setTargetId] = useState(MAIN_TARGET_ID); - const [items, setItems] = useState([]); - const [appliedSnapshot, setAppliedSnapshot] = useState(null); - const [loadedProfiles, setLoadedProfiles] = useState([]); - const [isProcessInputOpen, setIsProcessInputOpen] = useState(false); - const [processInput, setProcessInput] = useState(''); - const [pickerAction, setPickerAction] = useState<'exe' | 'folder' | null>(null); - const [components, setComponents] = useState(fallbackComponents); - const [setupStatus, setSetupStatus] = useState(null); - const [singBoxStatus, setSingBoxStatus] = useState(null); - const [singBoxSetupStatus, setSingBoxSetupStatus] = useState(null); - const [subscriptionInput, setSubscriptionInput] = useState(''); - const [serverPings, setServerPings] = useState>({}); - const [proxyPing, setProxyPing] = useState(null); - const [isSingBoxSetupOpen, setIsSingBoxSetupOpen] = useState(false); - const [isSingBoxInfoOpen, setIsSingBoxInfoOpen] = useState(false); - const [generatedConfigPath, setGeneratedConfigPath] = useState(''); - const [logEntries, setLogEntries] = useState([]); - const [activeLogId, setActiveLogId] = useState(null); - const [isLogOpen, setIsLogOpen] = useState(false); - const [isLoading, setIsLoading] = useState(true); - const [isDetectingComponents, setIsDetectingComponents] = useState(true); - const [isApplying, setIsApplying] = useState(false); - const [isOpeningConfig, setIsOpeningConfig] = useState(false); - const [isProxyChecking, setIsProxyChecking] = useState(false); - const [serverPingTag, setServerPingTag] = useState(null); - const [serviceAction, setServiceAction] = useState(null); - const [singBoxAction, setSingBoxAction] = useState(null); - const [isServiceMenuOpen, setIsServiceMenuOpen] = useState(false); - const [isSingBoxMenuOpen, setIsSingBoxMenuOpen] = useState(false); - const [serviceVisualState, setServiceVisualState] = useState(null); - const serviceVisualTimerRef = useRef(null); - const appsSectionRef = useRef(null); - - const proxyfier = useMemo( - () => components.find((component) => component.id === 'proxyfier'), - [components], - ); - const singbox = useMemo( - () => singBoxStatus?.component ?? components.find((component) => component.id === 'singbox'), - [components, singBoxStatus], - ); - const activeLog = useMemo( - () => logEntries.find((entry) => entry.id === activeLogId) ?? null, - [activeLogId, logEntries], - ); - const isSingBoxInstalled = Boolean(singbox?.installed); - const selectedServerTag = singBoxStatus?.config.selectedServerTag; - const currentSnapshot = useMemo( - () => configSnapshotFromUi(routeMode, proxyInput, items, selectedServerTag), - [items, proxyInput, routeMode, selectedServerTag], - ); - const hasUnappliedChanges = appliedSnapshot - ? !sameConfigSnapshot(currentSnapshot, appliedSnapshot) - : false; - const systemSummary = systemSummaryState({ - isLoading, - isDetectingComponents, - proxyfier, - singbox, - routeMode, - singBoxStatus, - proxyPing, - hasUnappliedChanges, - }); - - useEffect(() => { - void refresh(); - }, []); - - useEffect(() => { - return () => { - if (serviceVisualTimerRef.current !== null) { - window.clearTimeout(serviceVisualTimerRef.current); - } - }; - }, []); - - useEffect(() => { - if (!activeLogId) return undefined; - - const timer = window.setTimeout(() => { - setActiveLogId((current) => (current === activeLogId ? null : current)); - }, LOG_VISIBLE_MS); - - return () => window.clearTimeout(timer); - }, [activeLogId]); - - async function refresh() { - setIsLoading(true); - try { - const saved = await getSavedState(); - applySavedState(saved.profiles, saved.targets, saved.generatedConfigPath); - } catch { - showNotice({ - kind: 'info', - title: 'Режим предпросмотра', - text: 'Запусти приложение через Tauri, чтобы увидеть найденный ProxiFyre и применить конфиг.', - }); - } finally { - setIsLoading(false); - } - - void refreshComponents(); - } - - async function refreshComponents() { - setIsDetectingComponents(true); - try { - const [detectedComponents, detectedSetupStatus, detectedSingBoxStatus, detectedSingBoxSetupStatus] = await Promise.all([ - getComponents(), - getProxiFyreSetupStatus(), - getSingBoxStatus(), - getSingBoxSetupStatus(), - ]); - setComponents(detectedComponents); - setSetupStatus(detectedSetupStatus); - setSingBoxStatus(detectedSingBoxStatus); - setSingBoxSetupStatus(detectedSingBoxSetupStatus); - setAppliedSnapshot((current) => { - if (!current || current.routeMode !== 'local-singbox' || current.selectedServerTag) return current; - return { - ...current, - selectedServerTag: normalizeServerTag(detectedSingBoxStatus.config.selectedServerTag), - }; - }); - } catch (error) { - showNotice({ - kind: 'error', - title: 'Компоненты не проверены', - text: errorMessage(error), - }); - } finally { - setIsDetectingComponents(false); - } - } - - function applySavedState( - profiles: Profile[], - targets: Target[], - generatedPath: string, - singBoxStatusForSnapshot = singBoxStatus, - ) { - const activeProfiles = profiles.filter((profile) => profile.enabled); - const mainProfile = profiles.find((profile) => profile.id === MAIN_PROFILE_ID); - const activeProfile = mainProfile ?? activeProfiles[0]; - const activeTarget = targetForUi(targets, activeProfile); - const externalTarget = targetForExternalProxy(targets); - const editableProfiles = mainProfile ? [mainProfile] : activeProfiles; - const savedProxyInput = externalTarget ? formatProxy(externalTarget) : ''; - const savedItems = itemsForProfiles(editableProfiles); - const savedRouteMode = - activeTarget?.id === LOCAL_SINGBOX_TARGET_ID || activeProfile?.targetId === LOCAL_SINGBOX_TARGET_ID - ? 'local-singbox' - : 'external'; - - setProxyInput(savedProxyInput); - setProxyPing(null); - setItems(savedItems); - setLoadedProfiles(profiles); - setProfileId(mainProfile?.id ?? MAIN_PROFILE_ID); - setTargetId(externalTarget?.id ?? MAIN_TARGET_ID); - setRouteMode(savedRouteMode); - setGeneratedConfigPath(generatedPath); - setAppliedSnapshot(configSnapshotFromUi( - savedRouteMode, - savedProxyInput, - savedItems, - singBoxStatusForSnapshot?.config.selectedServerTag, - )); - } - - function addItem(type: DraftItemType, rawValue: string) { - const value = normalizeItemValue(rawValue, type); - if (!value) { - showNotice({ - kind: 'error', - title: 'Нечего добавить', - text: emptyItemMessage(type), - }); - return false; - } - - if (items.some((item) => item.type === type && sameValue(item.value, value))) { - showNotice({ - kind: 'info', - title: 'Уже добавлено', - text: value, - }); - return false; - } - - setItems((current) => [ - ...current, - { - id: `${type}-${Date.now()}`, - type, - value, - }, - ]); - return true; - } - - function addProcess() { - if (addItem('process', processInput)) { - setProcessInput(''); - setIsProcessInputOpen(false); - } - } - - function removeItem(id: string) { - setItems((current) => current.filter((item) => item.id !== id)); - } - - function changeRouteMode(nextMode: RouteMode) { - setRouteMode(nextMode); - } - - function changeProxyInput(nextValue: string) { - setProxyInput(nextValue); - setProxyPing(null); - } - - function openAppsPanel() { - switchPanel('proxifyre'); - window.setTimeout(() => { - appsSectionRef.current?.scrollIntoView({ block: 'start' }); - }, 0); - } - - function switchPanel(nextPanel: PanelId) { - if (activePanel === nextPanel) return; - - const currentIndex = PANEL_ORDER.indexOf(activePanel); - const nextIndex = PANEL_ORDER.indexOf(nextPanel); - setTabTransitionDirection(nextIndex > currentIndex ? 'right' : 'left'); - setActivePanel(nextPanel); - } - - async function pickAndAddItem(type: Extract) { - setPickerAction(type); - try { - const selectedPath = await pickPath(type); - if (selectedPath) { - addItem(type, selectedPath); - } - } catch (error) { - showNotice({ - kind: 'error', - title: type === 'exe' ? 'EXE не выбран' : 'Папка не выбрана', - text: errorMessage(error), - }); - } finally { - setPickerAction(null); - } - } - - async function updateConfig() { - let parsedProxy: ParsedProxy | null = null; - try { - if (!items.length) throw new Error('Добавь хотя бы один процесс, EXE-файл или папку.'); - if (routeMode === 'external') { - parsedProxy = parseProxy(proxyInput); - } else if (!isSingBoxInstalled) { - throw new Error('Сначала установи Local sing-box.'); - } else if (!singBoxStatus?.config.selectedServerTag) { - throw new Error('Выбери сервер Local sing-box.'); - } - } catch (error) { - showNotice({ - kind: 'error', - title: 'Проверь данные', - text: errorMessage(error), - }); - return; - } - - setIsApplying(true); - try { - let singBoxGeneratedPath = ''; - if (routeMode === 'external') { - if (!parsedProxy) throw new Error('Прокси не разобран.'); - await saveTarget({ - id: targetId, - name: 'Основной прокси', - kind: 'external', - protocol: parsedProxy.protocol, - host: parsedProxy.host, - port: parsedProxy.port, - }); - } else { - const singBoxResult = await generateSingBoxConfig(); - singBoxGeneratedPath = singBoxResult.generatedConfigPath; - await ensureSingBoxRunningForApply(); - } - - await saveProfile({ - id: profileId, - name: 'Приложения через прокси', - enabled: true, - targetId: routeMode === 'local-singbox' ? LOCAL_SINGBOX_TARGET_ID : targetId, - protocols: ['TCP', 'UDP'], - items: items.map(profileItemInput), - }); - await Promise.all( - loadedProfiles - .filter((profile) => profile.enabled && profile.id !== profileId) - .map((profile) => saveProfile(profileInputFromProfile(profile, false))), - ); - - const result = await applyProfiles(); - const [saved, detectedComponents, detectedSetupStatus, detectedSingBoxStatus, detectedSingBoxSetupStatus] = await Promise.all([ - getSavedState(), - getComponents(), - getProxiFyreSetupStatus(), - getSingBoxStatus(), - getSingBoxSetupStatus(), - ]); - - applySavedState(saved.profiles, saved.targets, result.generatedConfigPath); - setComponents(detectedComponents); - setSetupStatus(detectedSetupStatus); - setSingBoxStatus(detectedSingBoxStatus); - setSingBoxSetupStatus(detectedSingBoxSetupStatus); - showNotice(routeMode === 'local-singbox' ? noticeFromLocalApply(result, singBoxGeneratedPath) : noticeFromApply(result)); - } catch (error) { - showNotice({ - kind: 'error', - title: 'Конфиг не обновлен', - text: errorMessage(error), - }); - } finally { - setIsApplying(false); - } - } - - async function ensureSingBoxRunningForApply() { - if (routeMode !== 'local-singbox' || !singbox?.installed) return; - - setIsSingBoxMenuOpen(false); - try { - await nextFrame(); - if (singbox.running) { - setSingBoxAction('stop'); - const stopped = await stopSingBoxService(); - setComponents((current) => upsertComponent(current, stopped)); - } - - setSingBoxAction('start'); - const component = await startSingBoxService(); - setComponents((current) => upsertComponent(current, component)); - const status = await refreshSingBoxState(); - if (!status.component.running) { - throw new Error('Local sing-box установлен, но служба не запустилась.'); - } - } finally { - setSingBoxAction(null); - } - } - - async function openConfig() { - setIsOpeningConfig(true); - try { - const openedPath = await openConfigLocation(); - showNotice({ - kind: 'info', - title: 'Конфиг открыт', - text: openedPath, - }); - } catch (error) { - showNotice({ - kind: 'error', - title: 'Не удалось открыть конфиг', - text: errorMessage(error), - }); - } finally { - setIsOpeningConfig(false); - } - } - - async function setProxiFyreServiceRunning(shouldRun: boolean) { - const action = shouldRun ? 'start' : 'stop'; - setServiceAction(action); - setIsServiceMenuOpen(false); - startServiceVisual(); - try { - await nextFrame(); - const component = shouldRun - ? await startProxiFyreService() - : await stopProxiFyreService(); - - setComponents((current) => upsertComponent(current, component)); - showNotice({ - kind: 'success', - title: shouldRun ? 'Служба запущена' : 'Служба остановлена', - text: proxyfierDetails(component, false), - }); - } catch (error) { - showNotice({ - kind: 'error', - title: shouldRun ? 'Служба не запущена' : 'Служба не остановлена', - text: errorMessage(error), - }); - } finally { - setServiceAction(null); - settleServiceVisual(); - } - } - - async function installProxiFyrePackage() { - setServiceAction('install'); - setIsServiceMenuOpen(false); - startServiceVisual(); - try { - await nextFrame(); - const component = await installProxiFyre(); - const detectedSetupStatus = await getProxiFyreSetupStatus(); - setComponents((current) => upsertComponent(current, component)); - setSetupStatus(detectedSetupStatus); - showNotice({ - kind: 'success', - title: 'ProxiFyre установлен', - text: proxyfierDetails(component, false), - }); - } catch (error) { - showNotice({ - kind: 'error', - title: 'ProxiFyre не установлен', - text: errorMessage(error), - }); - } finally { - setServiceAction(null); - settleServiceVisual(); - } - } - - async function uninstallProxiFyrePackage() { - const confirmed = window.confirm( - 'Удалить ProxiFyre с компьютера? Будет удалена служба и папка установки ProxiFyre.', - ); - if (!confirmed) return; - - setServiceAction('uninstall'); - setIsServiceMenuOpen(false); - startServiceVisual(); - try { - await nextFrame(); - const component = await uninstallProxiFyre(); - const detectedSetupStatus = await getProxiFyreSetupStatus(); - setComponents((current) => upsertComponent(current, component)); - setSetupStatus(detectedSetupStatus); - showNotice({ - kind: 'success', - title: 'ProxiFyre удален', - text: 'Служба и папка установки ProxiFyre удалены.', - }); - } catch (error) { - showNotice({ - kind: 'error', - title: 'ProxiFyre не удален', - text: errorMessage(error), - }); - } finally { - setServiceAction(null); - settleServiceVisual(); - } - } - - async function refreshSingBoxState() { - const [detectedSingBoxStatus, detectedSingBoxSetupStatus, detectedComponents] = await Promise.all([ - getSingBoxStatus(), - getSingBoxSetupStatus(), - getComponents(), - ]); - setSingBoxStatus(detectedSingBoxStatus); - setSingBoxSetupStatus(detectedSingBoxSetupStatus); - setComponents(detectedComponents); - return detectedSingBoxStatus; - } - - async function setSingBoxServiceRunning(shouldRun: boolean) { - const action: SingBoxAction = shouldRun ? 'start' : 'stop'; - setSingBoxAction(action); - setIsSingBoxMenuOpen(false); - try { - await nextFrame(); - const component = shouldRun ? await startSingBoxService() : await stopSingBoxService(); - setComponents((current) => upsertComponent(current, component)); - await refreshSingBoxState(); - showNotice({ - kind: 'success', - title: shouldRun ? 'sing-box запущен' : 'sing-box остановлен', - text: componentDetails(component, false), - }); - } catch (error) { - showNotice({ - kind: 'error', - title: shouldRun ? 'sing-box не запущен' : 'sing-box не остановлен', - text: errorMessage(error), - }); - } finally { - setSingBoxAction(null); - } - } - - async function installSingBoxPackage() { - setSingBoxAction('install'); - setIsSingBoxMenuOpen(false); - try { - await nextFrame(); - const component = await installSingBox(); - setComponents((current) => upsertComponent(current, component)); - await refreshSingBoxState(); - showNotice({ - kind: 'success', - title: 'Local sing-box установлен', - text: componentDetails(component, false), - }); - } catch (error) { - showNotice({ - kind: 'error', - title: 'Local sing-box не установлен', - text: errorMessage(error), - }); - } finally { - setSingBoxAction(null); - } - } - - async function uninstallSingBoxPackage() { - const confirmed = window.confirm( - 'Удалить Local sing-box с компьютера? Будет удалена служба и папка установки sing-box.', - ); - if (!confirmed) return; - - setSingBoxAction('uninstall'); - setIsSingBoxMenuOpen(false); - try { - await nextFrame(); - const component = await uninstallSingBox(); - setComponents((current) => upsertComponent(current, component)); - await refreshSingBoxState(); - showNotice({ - kind: 'success', - title: 'Local sing-box удален', - text: 'Служба и папка установки Local sing-box удалены.', - }); - } catch (error) { - showNotice({ - kind: 'error', - title: 'Local sing-box не удален', - text: errorMessage(error), - }); - } finally { - setSingBoxAction(null); - } - } - - async function syncSingBoxSubscription() { - const subscriptionUrl = subscriptionInput.trim(); - if (!subscriptionUrl && !singBoxStatus?.config.hasSubscription) { - showNotice({ - kind: 'error', - title: 'Ссылка не указана', - text: 'Вставь ссылку подписки Local sing-box.', - }); - return; - } - - setSingBoxAction('fetch'); - try { - if (subscriptionUrl) { - await saveSingBoxSubscription(subscriptionUrl); - } - const status = await fetchSingBoxSubscription(); - setSingBoxStatus(status); - setComponents((current) => upsertComponent(current, status.component)); - setSubscriptionInput(''); - setServerPings({}); - showNotice({ - kind: 'success', - title: 'Подписка обновлена', - text: `Серверов: ${status.cache?.servers.length ?? 0}`, - }); - } catch (error) { - showNotice({ - kind: 'error', - title: 'Подписка не обновлена', - text: errorMessage(error), - }); - } finally { - setSingBoxAction(null); - } - } - - async function forgetSingBoxSubscriptionData() { - setSingBoxAction('forget'); - setIsSingBoxMenuOpen(false); - try { - const status = await forgetSingBoxSubscription(); - setSingBoxStatus(status); - setComponents((current) => upsertComponent(current, status.component)); - setServerPings({}); - showNotice({ - kind: 'info', - title: 'Подписка очищена', - text: 'Ссылка, cache и выбранный сервер Local sing-box удалены.', - }); - } catch (error) { - showNotice({ - kind: 'error', - title: 'Подписка не очищена', - text: errorMessage(error), - }); - } finally { - setSingBoxAction(null); - } - } - - async function chooseSingBoxServer(server: SubscriptionServer) { - try { - const status = await selectSingBoxServer(server); - setSingBoxStatus(status); - setComponents((current) => upsertComponent(current, status.component)); - } catch (error) { - showNotice({ - kind: 'error', - title: 'Сервер не выбран', - text: errorMessage(error), - }); - } - } - - async function pingSingBoxServers() { - setSingBoxAction('ping'); - try { - const results = await pingAllSingBoxServers(); - setServerPings(Object.fromEntries(results.map((result) => [result.tag, result]))); - showNotice({ - kind: 'info', - title: 'Ping завершен', - text: pingSummary(results), - }); - } catch (error) { - showNotice({ - kind: 'error', - title: 'Ping не выполнен', - text: errorMessage(error), - }); - } finally { - setSingBoxAction(null); - } - } - - async function pingSingleSingBoxServer(server: SubscriptionServer) { - setServerPingTag(server.tag); - try { - const result = await pingSingBoxServer(server.tag); - setServerPings((current) => ({ - ...current, - [result.tag]: result, - })); - showNotice({ - kind: result.ok ? 'success' : 'error', - title: result.ok ? 'Сервер отвечает' : 'Сервер не ответил', - text: serverTooltip(server, result), - }); - } catch (error) { - showNotice({ - kind: 'error', - title: 'Ping не выполнен', - text: errorMessage(error), - }); - } finally { - setServerPingTag(null); - } - } - - async function pingExternalProxy() { - let parsed: ParsedProxy; - try { - parsed = parseProxy(proxyInput); - } catch (error) { - showNotice({ - kind: 'error', - title: 'Прокси не проверен', - text: errorMessage(error), - }); - return; - } - - setIsProxyChecking(true); - try { - const result = await pingProxyTarget(parsed.host, parsed.port); - setProxyPing(result); - showNotice({ - kind: result.ok ? 'success' : 'error', - title: result.ok ? 'Внешний прокси отвечает' : 'Внешний прокси не ответил', - text: pingResultText(result), - }); - } catch (error) { - showNotice({ - kind: 'error', - title: 'Прокси не проверен', - text: errorMessage(error), - }); - } finally { - setIsProxyChecking(false); - } - } - - async function generateSingBoxNow() { - setSingBoxAction('generate'); - try { - const result = await generateSingBoxConfig(); - await refreshSingBoxState(); - showNotice({ - kind: 'success', - title: 'Конфиг sing-box создан', - text: result.generatedConfigPath, - }); - } catch (error) { - showNotice({ - kind: 'error', - title: 'Конфиг sing-box не создан', - text: errorMessage(error), - }); - } finally { - setSingBoxAction(null); - } - } - - function startServiceVisual() { - if (serviceVisualTimerRef.current !== null) { - window.clearTimeout(serviceVisualTimerRef.current); - serviceVisualTimerRef.current = null; - } - - setServiceVisualState('active'); - } - - function settleServiceVisual() { - if (serviceVisualTimerRef.current !== null) { - window.clearTimeout(serviceVisualTimerRef.current); - } - - setServiceVisualState('settling'); - serviceVisualTimerRef.current = window.setTimeout(() => { - setServiceVisualState(null); - serviceVisualTimerRef.current = null; - }, 700); - } - - function showNotice(notice: Notice) { - const entry: LogEntry = { - ...notice, - id: `log-${Date.now()}-${Math.random().toString(36).slice(2)}`, - at: Date.now(), - }; - - setLogEntries((current) => [entry, ...current].slice(0, 40)); - setActiveLogId(entry.id); - } - - function renderTabs() { - const tabs: Array<{ id: PanelId; label: string }> = [ - { id: 'summary', label: 'Сводка' }, - { id: 'proxifyre', label: 'ProxiFyre' }, - { id: 'proxy', label: 'VPN / Прокси' }, - ]; - - return ; - } - - function renderSummaryPanel() { - const appliedRouteMode = appliedSnapshot?.routeMode ?? routeMode; - const draftHasLocalServer = currentSnapshot.routeMode === 'local-singbox' && currentSnapshot.selectedServerTag; - - return ( -
-
-
- -
-
-
- Применено сейчас - {appliedSnapshot ? routeModeLabel(appliedRouteMode) : 'Состояние загружается'} -
- - {hasUnappliedChanges ? 'Есть черновик' : 'Совпадает'} - -
-
-
-
ProxiFyre
-
{proxyfierTitle(proxyfier, isDetectingComponents)}
-
-
-
Маршрут
-
{snapshotRouteLine(appliedSnapshot, singBoxStatus)}
-
-
-
VPN сервер
-
{snapshotServerText(appliedSnapshot)}
-
-
-
Приложения
-
{snapshotItemsText(appliedSnapshot)}
-
-
-
Конфиг
-
{generatedConfigPath || 'Примененный config еще не создан.'}
-
-
-
- -
-
- Черновик интерфейса - {hasUnappliedChanges ? 'Есть реальные изменения' : 'Изменений нет'} -

- {hasUnappliedChanges - ? 'Текущие настройки отличаются от примененного состояния. Если вернуть значения назад, предупреждение исчезнет.' - : 'То, что выбрано в интерфейсе, совпадает с примененной конфигурацией.'} -

-
- {hasUnappliedChanges ? ( -
-
-
Будет маршрут
-
{snapshotRouteLine(currentSnapshot, singBoxStatus)}
-
-
-
VPN сервер
-
{draftHasLocalServer ? displayServerTag(currentSnapshot.selectedServerTag) : snapshotServerText(currentSnapshot)}
-
-
-
Приложения
-
{snapshotItemsText(currentSnapshot)}
-
-
- ) : null} -
- -
- - -
-
- ); - } - - function renderProxiFyreCard() { - const state = serviceControlState(proxyfier, isDetectingComponents); - const visualState = serviceVisualState === 'active' ? 'working' : serviceVisualState === 'settling' ? 'settling' : null; - const primaryAction = proxyfier?.installed - ? { - label: proxyfier.running ? 'Остановить' : 'Запустить', - onClick: () => void setProxiFyreServiceRunning(!proxyfier.running), - variant: proxyfier.running ? 'danger' as const : 'neutral' as const, - loading: serviceAction === 'start' || serviceAction === 'stop', - loadingLabel: serviceAction === 'start' ? 'Запускаю' : 'Останавливаю', - disabled: isDetectingComponents || Boolean(serviceAction), - } - : { - label: 'Установить', - onClick: () => void installProxiFyrePackage(), - variant: 'primary' as const, - loading: serviceAction === 'install', - loadingLabel: 'Устанавливаю', - disabled: isDetectingComponents || Boolean(serviceAction), - }; - - return ( - void uninstallProxiFyrePackage(), - }], - } : undefined} - /> - ); - } - - function renderProxiFyreSetupStrip() { - const stripItems = setupStatus?.items ?? proxifyreSetupPlaceholders(); - - return ( -
- Состав -
- {stripItems.map((item) => ( -
-
- ))} -
-
- ); - } - - function renderAppsSection() { - return ( -
-
-
-

Приложения

- {items.length} -
-
- {renderApplyActions('proxifyre', { showConfigPath: false })} -
-
- - {isProcessInputOpen ? ( -
- setProcessInput(event.target.value)} - onKeyDown={(event) => { - if (event.key === 'Enter') addProcess(); - if (event.key === 'Escape') { - setProcessInput(''); - setIsProcessInputOpen(false); - } - }} - placeholder="Discord" - spellCheck={false} - autoFocus - /> - - -
- ) : ( -
-
- -
- Новое приложение - Добавь процесс, EXE-файл или папку -
-
-
- setIsProcessInputOpen(true)} - label="Добавить процесс" - icon={} - /> - void pickAndAddItem('exe')} - disabled={Boolean(pickerAction)} - loading={pickerAction === 'exe'} - label="Добавить EXE-файл" - icon={} - /> - void pickAndAddItem('folder')} - disabled={Boolean(pickerAction)} - loading={pickerAction === 'folder'} - label="Добавить папку" - icon={} - /> -
-
- )} - -
- {isLoading ? ( -
- - -
- ) : items.length ? ( - items.map((item) => ( -
-
- -
- {item.value} - {itemTypeLabel(item.type)} -
-
- -
- )) - ) : ( -
Список пуст. Добавь первое приложение сверху.
- )} -
-
- ); - } - - function renderApplyActions( - context: 'proxifyre' | 'proxy', - options: { showConfigPath?: boolean } = {}, - ) { - const showConfigPath = options.showConfigPath ?? true; - const externalProxyError = routeMode === 'external' ? safeProxyError(proxyInput) : null; - const readiness = getApplyReadiness({ - routeMode, - appCount: items.length, - proxiFyreInstalled: Boolean(proxyfier?.installed), - singBoxInstalled: isSingBoxInstalled, - selectedServerTag: singBoxStatus?.config.selectedServerTag, - externalProxyValue: proxyInput, - externalProxyError, - busy: isApplying || Boolean(serviceAction) || Boolean(singBoxAction), - }); - const showBlocker = !readiness.ready && !isLoading && !isDetectingComponents; - - return ( - <> - {showBlocker ? ( -
- {readiness.title} - {readiness.text} -
- ) : hasUnappliedChanges ? ( -
- Изменения еще не применены в ProxiFyre - {applyStateText(routeMode, isSingBoxInstalled, Boolean(singbox?.running))} -
- ) : null} - -
- - -
- - {showConfigPath && generatedConfigPath ?

{generatedConfigPath}

: null} - - ); - } - - function renderProxiFyrePanel() { - return ( -
- {renderProxiFyreCard()} - {renderProxiFyreSetupStrip()} - {renderAppsSection()} -
- ); - } - - function renderExternalProxyControls() { - const proxyValidation = proxyInput.trim() ? safeProxyError(proxyInput) : null; - - return ( -
- -
-
- {proxyPing ? pingResultTitle(proxyPing) : proxyValidation ? 'Проверь формат' : 'Проверка не запускалась'} - {proxyPing ? pingResultText(proxyPing) : proxyValidation ?? 'TCP check покажет, доступен ли host:port.'} -
- -
-
- ); - } - - function renderSingBoxCard() { - const state = serviceControlState(singbox, isDetectingComponents); - const primaryAction = singbox?.installed - ? { - label: singbox.running ? 'Остановить' : 'Запустить', - onClick: () => void setSingBoxServiceRunning(!singbox.running), - variant: singbox.running ? 'danger' as const : 'neutral' as const, - loading: singBoxAction === 'start' || singBoxAction === 'stop', - loadingLabel: singBoxAction === 'start' ? 'Запускаю' : 'Останавливаю', - disabled: isDetectingComponents || Boolean(singBoxAction), - } - : { - label: 'Установить', - onClick: () => void installSingBoxPackage(), - variant: 'primary' as const, - loading: singBoxAction === 'install', - loadingLabel: 'Устанавливаю', - disabled: isDetectingComponents || Boolean(singBoxAction), - }; - - return ( - void uninstallSingBoxPackage(), - }], - } : undefined} - inlineActions={( - <> - - setIsSingBoxInfoOpen((current) => !current)} - label="Подробности Local sing-box" - aria-expanded={isSingBoxInfoOpen} - title="Подробности" - icon={} - /> - - )} - > - {isSingBoxInfoOpen ? ( -
-
- Локально - {localSingBoxAddress(singBoxStatus)} - LAN - {lanSingBoxAddress(singBoxStatus) ?? 'недоступен'} - Сервер - - {selectedServerTag - ? displayServerTag(selectedServerTag) - : 'не выбран'} - - Файл - {singbox?.path ?? 'не найден'} - Конфиг - {singBoxStatus?.generatedConfigPath ?? 'не создан'} -
-
- - -
-
- ) : null} - - {isSingBoxSetupOpen ? ( -
- {singBoxSetupStatus ? ( - singBoxSetupStatus.items.map((item) => ( -
-
- )) - ) : ( -
-
- )} -
- ) : null} - - {isSingBoxInstalled ? renderSingBoxWorkspace() : ( -
- Local sing-box не установлен - Установи компонент, чтобы подключить подписку, выбрать сервер и включить локальный маршрут. -
- )} -
- ); - } - - function renderSingBoxWorkspace() { - return ( -
-
- - setSubscriptionInput(event.target.value)} - placeholder={singBoxStatus?.config.subscriptionDisplayUrl ?? 'https://example.com/sub'} - spellCheck={false} - /> - - void forgetSingBoxSubscriptionData()} - disabled={Boolean(singBoxAction) || !singBoxStatus?.config.hasSubscription} - label="Очистить подписку Local sing-box" - title="Очистить" - icon={} - /> -
- - {singBoxStatus?.cache?.servers.length ? ( -
- {singBoxStatus.cache.servers.map((server) => { - const ping = serverPings[server.tag]; - const selected = server.tag === selectedServerTag; - return ( -
- - void pingSingleSingBoxServer(server)} - disabled={Boolean(serverPingTag)} - loading={serverPingTag === server.tag} - label={`Проверить ${displayServerTag(server.tag)}`} - title="Ping" - icon={} - /> -
- ); - })} -
- ) : ( -
Подписка Local sing-box еще не загружена.
- )} -
- ); - } - - function renderProxyPanel() { - return ( -
-
-
- Настройки VPN / Прокси -

Маршрут и проверка соединения

-
- {routeMode === 'local-singbox' ? 'Локальный прокси' : 'Внешний прокси'} -
- -
-
- - -
- - {routeMode === 'external' ? renderExternalProxyControls() : renderSingBoxCard()} -
- -
- Маршрут - {routePreviewText(routeMode, proxyInput, singBoxStatus)} -
- - {renderApplyActions('proxy')} -
- ); - } - - function renderActivePanel() { - if (activePanel === 'proxifyre') return renderProxiFyrePanel(); - if (activePanel === 'proxy') return renderProxyPanel(); - return renderSummaryPanel(); - } - - return ( -
-
-
-
-

Proxy для приложений

-
- -
- - {renderTabs()} -
- {renderActivePanel()} -
-
- - setIsLogOpen((current) => !current)} - formatTime={formatLogTime} - /> -
- ); -} - -interface ParsedProxy { - protocol: 'socks5'; - host: string; - port: number; -} - -interface SummaryStateInput { - isLoading: boolean; - isDetectingComponents: boolean; - proxyfier: ComponentStatus | undefined; - singbox: ComponentStatus | undefined; - routeMode: RouteMode; - singBoxStatus: LocalSingBoxStatusResponse | null; - proxyPing: PingServerResponse | null; - hasUnappliedChanges: boolean; -} - -interface SummaryState { - tone: StatusTone; - title: string; - text: string; -} - -function configSnapshotFromUi( - routeMode: RouteMode, - proxyInput: string, - items: Array>, - selectedServerTag?: string, -): ConfigSnapshot { - return { - routeMode, - proxy: routeMode === 'external' ? normalizeProxySnapshot(proxyInput) : '', - selectedServerTag: routeMode === 'local-singbox' ? normalizeServerTag(selectedServerTag) : '', - items: normalizeSnapshotItems(items), - }; -} - -function normalizeProxySnapshot(value: string) { - try { - const parsed = parseProxy(value); - return `${parsed.protocol}://${parsed.host.trim().toLowerCase()}:${parsed.port}`; - } catch { - return value.trim().toLowerCase(); - } -} - -function normalizeServerTag(tag: string | undefined) { - return tag?.trim() ?? ''; -} - -function normalizeSnapshotItems(items: Array>): ConfigSnapshotItem[] { - return items - .map((item) => ({ - type: item.type, - value: normalizeItemValue(item.value, item.type).toLowerCase(), - })) - .filter((item) => item.value) - .sort((left, right) => `${left.type}:${left.value}`.localeCompare(`${right.type}:${right.value}`)); -} - -function sameConfigSnapshot(left: ConfigSnapshot, right: ConfigSnapshot) { - if (left.routeMode !== right.routeMode) return false; - if (left.proxy !== right.proxy) return false; - if (left.selectedServerTag !== right.selectedServerTag) return false; - if (left.items.length !== right.items.length) return false; - - return left.items.every((item, index) => { - const other = right.items[index]; - return item.type === other.type && item.value === other.value; - }); -} - -function routeModeLabel(routeMode: RouteMode) { - return routeMode === 'local-singbox' ? 'Локальный прокси' : 'Внешний прокси'; -} - -function snapshotRouteLine(snapshot: ConfigSnapshot | null, status: LocalSingBoxStatusResponse | null) { - if (!snapshot) return 'Сохраненное состояние еще не загружено.'; - - if (snapshot.routeMode === 'local-singbox') { - const server = snapshot.selectedServerTag ? displayServerTag(snapshot.selectedServerTag) : 'сервер не выбран'; - return `Выбранные приложения -> ProxiFyre -> Local sing-box ${localSingBoxAddress(status)} -> ${server}`; - } - - return `Выбранные приложения -> ProxiFyre -> внешний прокси ${displaySnapshotProxy(snapshot.proxy)}`; -} - -function snapshotServerText(snapshot: ConfigSnapshot | null) { - if (!snapshot) return 'Сохраненное состояние еще не загружено.'; - if (snapshot.routeMode === 'external') return 'Не используется: применен внешний прокси.'; - return snapshot.selectedServerTag ? displayServerTag(snapshot.selectedServerTag) : 'Сервер не выбран.'; -} - -function snapshotItemsText(snapshot: ConfigSnapshot | null) { - if (!snapshot) return 'Сохраненное состояние еще не загружено.'; - return snapshot.items.length ? appCountText(snapshot.items.length) : 'Приложения не настроены.'; -} - -function displaySnapshotProxy(proxy: string) { - return proxy.replace(/^socks5:\/\//, '') || 'не указан'; -} - -function systemSummaryState(input: SummaryStateInput): SummaryState { - if (input.isLoading || input.isDetectingComponents) { - return { - tone: 'checking', - title: 'Проверяю', - text: 'Обновляю сохраненный маршрут, компоненты и локальный proxy path.', - }; - } - - if (!input.proxyfier?.installed) { - return { - tone: 'error', - title: 'Не настроено', - text: 'ProxiFyre не найден. Перейди в настройки ProxiFyre и установи компонент.', - }; - } - - if (!input.proxyfier.running) { - return { - tone: 'warning', - title: 'Требует внимания', - text: 'ProxiFyre установлен, но служба сейчас остановлена.', - }; - } - - if (input.routeMode === 'local-singbox') { - if (!input.singbox?.installed) { - return { - tone: 'warning', - title: 'Локальный прокси не готов', - text: 'Для локального маршрута нужно установить Local sing-box.', - }; - } - if (!input.singbox.running) { - return { - tone: 'warning', - title: 'Локальный прокси остановлен', - text: 'Local sing-box установлен, но служба не запущена.', - }; - } - if (!input.singBoxStatus?.config.selectedServerTag) { - return { - tone: 'warning', - title: 'Сервер не выбран', - text: 'Выбери сервер Local sing-box на вкладке VPN / Прокси.', - }; - } - } - - if (input.routeMode === 'external' && input.proxyPing && !input.proxyPing.ok) { - return { - tone: 'warning', - title: 'Прокси не ответил', - text: pingResultText(input.proxyPing), - }; - } - - if (input.hasUnappliedChanges) { - return { - tone: 'warning', - title: 'Есть непримененные изменения', - text: 'Настройки изменены, но ProxiFyre еще не обновлен.', - }; - } - - return { - tone: 'ok', - title: 'Работает', - text: 'Сохраненная конфигурация выглядит готовой к маршрутизации выбранных приложений.', - }; -} - -function routeEndpointLabel( - routeMode: RouteMode, - proxyInput: string, - status: LocalSingBoxStatusResponse | null, -) { - if (routeMode === 'local-singbox') { - const lan = lanSingBoxAddress(status); - return lan ? `${localSingBoxAddress(status)} · LAN: ${lan}` : localSingBoxAddress(status); - } - - try { - const parsed = parseProxy(proxyInput); - return formatHostPort(parsed.host, parsed.port); - } catch { - return proxyInput.trim() || 'не указан'; - } -} - -function appCountText(count: number) { - if (count === 1) return '1 приложение маршрутизируется через профиль.'; - if (count > 1 && count < 5) return `${count} приложения маршрутизируются через профиль.`; - return `${count} приложений маршрутизируются через профиль.`; -} - -function safeProxyError(value: string) { - try { - parseProxy(value); - return null; - } catch (error) { - return errorMessage(error); - } -} - -function pingTone(ping: PingServerResponse): StatusTone { - return ping.ok ? 'ok' : 'error'; -} - -function pingResultTitle(ping: PingServerResponse) { - return ping.ok ? 'Доступен' : 'Недоступен'; -} - -function pingResultText(ping: PingServerResponse) { - if (ping.ok) return `${ping.server}:${ping.serverPort} ответил за ${ping.latency ?? 0} ms.`; - return `${ping.server}:${ping.serverPort}: ${ping.error ?? 'нет ответа'}`; -} - -function routePreviewText( - routeMode: RouteMode, - proxyInput: string, - status: LocalSingBoxStatusResponse | null, -) { - if (routeMode === 'local-singbox') { - const server = status?.config.selectedServerTag - ? displayServerTag(status.config.selectedServerTag) - : 'сервер не выбран'; - return `Выбранные приложения -> ProxiFyre -> Local sing-box ${localSingBoxAddress(status)} -> ${server}`; - } - - return `Выбранные приложения -> ProxiFyre -> внешний прокси ${routeEndpointLabel('external', proxyInput, status)}`; -} - -function applyButtonLabel( - context: 'proxifyre' | 'proxy', - isApplying: boolean, - hasUnappliedChanges: boolean, - singBoxAction: SingBoxAction | null, -) { - if (isApplying) { - if (singBoxAction === 'start') return 'Запускаю sing-box...'; - if (singBoxAction === 'stop') return 'Перезапускаю sing-box...'; - return 'Применяю...'; - } - if (hasUnappliedChanges) return context === 'proxy' ? 'Применить маршрут' : 'Применить в ProxiFyre'; - return context === 'proxy' ? 'Обновить маршрут' : 'Обновить конфиг'; -} - -function parseProxy(rawValue: string): ParsedProxy { - const value = rawValue.trim(); - if (!value) throw new Error('Введи адрес прокси.'); - - const withProtocol = /^[a-z][a-z0-9+.-]*:\/\//i.test(value) ? value : `socks5://${value}`; - let parsed: URL; - try { - parsed = new URL(withProtocol); - } catch { - throw new Error('Формат: socks5://host:port или host:port.'); - } - - const protocol = parsed.protocol.replace(':', '').toLowerCase(); - if (protocol !== 'socks5') { - throw new Error('Сейчас поддерживается только SOCKS5.'); - } - if (parsed.username || parsed.password) { - throw new Error('Прокси с логином и паролем пока не поддерживаются.'); - } - - const host = parsed.hostname.replace(/^\[|\]$/g, ''); - const port = Number(parsed.port); - if (!host || !Number.isInteger(port) || port < 1 || port > 65535) { - throw new Error('Укажи хост и порт прокси.'); - } - - return { protocol: 'socks5', host, port }; -} - -function targetForUi(targets: Target[], profile: Profile | undefined) { - if (profile) return targets.find((target) => target.id === profile.targetId); - return targets.find((target) => target.id === MAIN_TARGET_ID) ?? targets.find((target) => target.kind === 'external'); -} - -function targetForExternalProxy(targets: Target[]) { - return targets.find((target) => target.id === MAIN_TARGET_ID) - ?? targets.find((target) => target.kind === 'external' && target.id !== LOCAL_SINGBOX_TARGET_ID); -} - -function itemsForProfiles(profiles: Profile[]): DraftItem[] { - const seen = new Set(); - const items: DraftItem[] = []; - - for (const profile of profiles) { - for (const item of profile.items) { - if (item.type !== 'process' && item.type !== 'folder' && item.type !== 'exe') continue; - - const key = `${item.type}:${item.value.trim().toLowerCase()}`; - if (seen.has(key)) continue; - seen.add(key); - items.push({ - id: `${item.type}-${items.length}-${item.value}`, - type: item.type, - value: item.value, - }); - } - } - - return items; -} - -function formatProxy(target: Target) { - return target.protocol === 'socks5' - ? `${target.host}:${target.port}` - : `${target.protocol}://${target.host}:${target.port}`; -} - -function normalizeItemValue(value: string, type: DraftItemType) { - const clean = value.trim().replace(/^"|"$/g, ''); - if (!clean) return ''; - if (type === 'folder' || type === 'exe') return clean; - - return clean - .split(/[\\/]/) - .pop() - ?.replace(/\.exe$/i, '') - .trim() ?? ''; -} - -async function pickPath(type: Extract) { - const selected = await open( - type === 'folder' - ? { - title: 'Выбери папку', - directory: true, - multiple: false, - } - : { - title: 'Выбери EXE-файл', - directory: false, - multiple: false, - filters: [{ name: 'EXE-файлы', extensions: ['exe'] }], - }, - ); - - if (Array.isArray(selected)) return selected[0] ?? null; - return selected; -} - -function nextFrame() { - return new Promise((resolve) => { - window.requestAnimationFrame(() => resolve()); - }); -} - -function profileItemInput(item: DraftItem): ProfileItemInput { - return { - type: item.type, - value: item.value, - recursive: item.type === 'folder', - }; -} - -function emptyItemMessage(type: DraftItemType) { - if (type === 'process') return 'Введи имя процесса.'; - if (type === 'folder') return 'Введи путь к папке.'; - return 'Введи путь к EXE-файлу.'; -} - -function itemTypeLabel(type: DraftItemType) { - if (type === 'process') return 'процесс'; - if (type === 'folder') return 'папка'; - return 'EXE-файл'; -} - -function itemIcon(type: DraftItemType) { - if (type === 'process') return ; - if (type === 'folder') return ; - return ; -} - -function setupItemDetails(installed: boolean, version: string | undefined, details: string) { - if (!installed) return `Нужно установить. ${details}`; - if (version) return `${version}. ${details}`; - return details; -} - -function proxifyreSetupPlaceholders(): ProxiFyreSetupStatus['items'] { - return [ - { id: 'vc-runtime', name: 'Среда запуска', installed: false, details: 'Проверяю' }, - { id: 'packet-filter', name: 'Сетевой драйвер', installed: false, details: 'Проверяю' }, - { id: 'proxifyre', name: 'Клиент ProxiFyre', installed: false, details: 'Проверяю' }, - ]; -} - -function setupItemUserName(id: string, fallbackName: string) { - if (id === 'vc-runtime') return 'Среда запуска'; - if (id === 'packet-filter') return 'Сетевой драйвер'; - if (id === 'proxifyre') return 'Клиент ProxiFyre'; - return fallbackName; -} - -function setupItemShortStatus(item: ProxiFyreSetupStatus['items'][number]) { - if (item.details === 'Проверяю') return 'проверяю'; - if (!item.installed) return 'нужно установить'; - if (item.id === 'proxifyre') return item.version?.includes('не запущена') ? 'остановлен' : 'запущен'; - return 'готово'; -} - -function profileInputFromProfile(profile: Profile, enabled: boolean) { - return { - id: profile.id, - name: profile.name, - enabled, - targetId: profile.targetId, - protocols: profile.protocols, - items: profile.items.map((item) => ({ - type: item.type, - value: item.value, - recursive: item.recursive, - })), - }; -} - -function proxyfierTitle(component: ComponentStatus | undefined, checking: boolean) { - if (checking) return 'Проверяю ProxiFyre'; - if (!component) return 'ProxiFyre не проверен'; - if (component.running) return 'ProxiFyre найден и запущен'; - if (component.installed) return 'ProxiFyre найден'; - return 'ProxiFyre не найден'; -} - -function proxyfierDetails(component: ComponentStatus | undefined, checking: boolean) { - if (checking) return 'Ищу установленный клиент и состояние службы.'; - if (!component) return 'Нажми «Обновить», чтобы проверить компьютер.'; - if (component.path) return component.path; - return component.problems[0] ?? 'Путь установки не найден.'; -} - -function singBoxTitle(component: ComponentStatus | undefined, checking: boolean) { - if (checking) return 'Проверяю Local sing-box'; - if (!component) return 'Local sing-box не проверен'; - if (component.running) return 'Local sing-box найден и запущен'; - if (component.installed) return 'Local sing-box найден'; - return 'Local sing-box не установлен'; -} - -function singBoxDetails( - component: ComponentStatus | undefined, - status: LocalSingBoxStatusResponse | null, - checking: boolean, -) { - if (checking) return 'Ищу sing-box, wrapper и службу.'; - if (component?.running && status) { - const lanAddress = lanSingBoxAddress(status); - return lanAddress - ? `Доступен локально: ${localSingBoxAddress(status)} · LAN: ${lanAddress}` - : `Доступен локально: ${localSingBoxAddress(status)}`; - } - if (component?.installed) return 'Служба остановлена. Запусти Local sing-box перед применением маршрута.'; - if (status?.config.hasSubscription) { - return status.config.subscriptionDisplayUrl ?? 'Подписка сохранена.'; - } - return component?.problems[0] ?? 'Установи компонент, чтобы подключить подписку и выбрать сервер.'; -} - -function componentDetails(component: ComponentStatus | undefined, checking: boolean) { - if (checking) return 'Проверяю состояние службы.'; - if (!component) return 'Компонент не проверен.'; - if (component.path) return component.path; - return component.problems[0] ?? 'Путь установки не найден.'; -} - -function noticeFromApply(result: ApplyProfilesResponse): Notice { - return { - kind: result.success ? 'success' : 'error', - title: result.success ? 'Конфиг обновлен' : 'Конфиг создан, но не применен', - text: result.message, - }; -} - -function noticeFromLocalApply(result: ApplyProfilesResponse, singBoxGeneratedPath: string): Notice { - return { - kind: result.success ? 'success' : 'error', - title: result.success ? 'Local sing-box применен' : 'Конфиг создан, но не применен', - text: singBoxGeneratedPath ? `${result.message} sing-box: ${singBoxGeneratedPath}` : result.message, - }; -} - -function applyStateText(routeMode: RouteMode, isSingBoxInstalled: boolean, isSingBoxRunning: boolean) { - if (routeMode === 'local-singbox' && isSingBoxInstalled && !isSingBoxRunning) { - return 'Local sing-box сейчас остановлен. При применении клиент сначала запустит службу, затем обновит ProxiFyre.'; - } - if (routeMode === 'local-singbox' && isSingBoxInstalled && isSingBoxRunning) { - return 'При применении клиент обновит конфиг, перезапустит Local sing-box и затем обновит ProxiFyre.'; - } - return 'Нажми «Применить в ProxiFyre». Если служба уже запущена и маршрут не обновился, перезапусти ProxiFyre.'; -} - -function upsertComponent(components: ComponentStatus[], component: ComponentStatus) { - const index = components.findIndex((current) => current.id === component.id); - if (index === -1) return [...components, component]; - - return [ - ...components.slice(0, index), - component, - ...components.slice(index + 1), - ]; -} - -function sameValue(left: string, right: string) { - return left.trim().toLowerCase() === right.trim().toLowerCase(); -} - -function formatLogTime(timestamp: number) { - return new Date(timestamp).toLocaleTimeString('ru-RU', { - hour: '2-digit', - minute: '2-digit', - second: '2-digit', - }); -} - -function serverLabel(server: SubscriptionServer) { - return `${server.type} · ${server.server}:${server.serverPort}`; -} - -function serverTooltip(server: SubscriptionServer, ping: PingServerResponse | undefined) { - const details = serverLabel(server); - if (!ping) return details; - return ping.ok ? `${details} · ping ${ping.latency ?? 0} ms` : `${details} · ping fail`; -} - -function displayServerTag(tag: string) { - const withoutFlags = tag - .replace(/[\u{1f1e6}-\u{1f1ff}]/gu, '') - .replace(/\s*->\s*/g, ' -> ') - .replace(/\s*->\s*$/g, '') - .replace(/^\s*->\s*/g, '') - .replace(/\s{2,}/g, ' ') - .trim(); - - return withoutFlags || tag; -} - -function pingSummary(results: PingServerResponse[]) { - if (!results.length) return 'Серверов для проверки нет.'; - const ok = results.filter((result) => result.ok); - if (!ok.length) return `Не ответил ни один сервер из ${results.length}.`; - const best = ok.reduce((current, result) => { - if ((result.latency ?? Number.MAX_SAFE_INTEGER) < (current.latency ?? Number.MAX_SAFE_INTEGER)) return result; - return current; - }); - return `Ответили ${ok.length}/${results.length}; быстрее ${best.tag}: ${best.latency ?? 0} ms.`; -} - -function localSingBoxAddress(status: LocalSingBoxStatusResponse | null) { - if (!status) return 'не загружен'; - const host = status.config.listenHost.trim(); - const displayHost = host === '0.0.0.0' || host === '::' || !host ? '127.0.0.1' : host; - return formatHostPort(displayHost, status.config.listenPort); -} - -function lanSingBoxAddress(status: LocalSingBoxStatusResponse | null) { - if (!status) return null; - const host = status.config.listenHost.trim().toLowerCase(); - if (host === '127.0.0.1' || host === 'localhost' || host === '::1') return null; - const lanHost = host === '0.0.0.0' || host === '::' || !host - ? status.lanListenHost - : status.config.listenHost; - if (!lanHost) return null; - return formatHostPort(lanHost, status.config.listenPort); -} - -function formatHostPort(host: string, port: number) { - return host.includes(':') && !host.startsWith('[') ? `[${host}]:${port}` : `${host}:${port}`; -} - -function errorMessage(error: unknown) { - if (error instanceof Error) return error.message; - if (typeof error === 'string') return error; - if (error && typeof error === 'object' && 'message' in error) { - return String((error as { message: unknown }).message); - } - return 'Неизвестная ошибка.'; -} diff --git a/apps/windows-client/src/app/readiness.ts b/apps/windows-client/src/app/readiness.ts deleted file mode 100644 index 04b8068..0000000 --- a/apps/windows-client/src/app/readiness.ts +++ /dev/null @@ -1,83 +0,0 @@ -export type RouteMode = 'external' | 'local-singbox'; - -export interface ApplyReadinessInput { - routeMode: RouteMode; - appCount: number; - proxiFyreInstalled: boolean; - singBoxInstalled: boolean; - selectedServerTag?: string; - externalProxyValue: string; - externalProxyError?: string | null; - busy: boolean; -} - -export interface ApplyReadiness { - ready: boolean; - title?: string; - text?: string; -} - -export function getApplyReadiness(input: ApplyReadinessInput): ApplyReadiness { - if (input.busy) { - return { - ready: false, - title: 'Операция уже выполняется', - text: 'Дождись завершения текущего действия перед повторным применением.', - }; - } - - if (!input.proxiFyreInstalled) { - return { - ready: false, - title: 'ProxiFyre не установлен', - text: 'Установи ProxiFyre, чтобы маршрутизировать выбранные приложения.', - }; - } - - if (input.appCount < 1) { - return { - ready: false, - title: 'Нет приложений', - text: 'Добавь хотя бы один процесс, EXE-файл или папку.', - }; - } - - if (input.routeMode === 'external') { - if (!input.externalProxyValue.trim()) { - return { - ready: false, - title: 'Прокси не указан', - text: 'Введи адрес SOCKS5 прокси в формате host:port или socks5://host:port.', - }; - } - - if (input.externalProxyError) { - return { - ready: false, - title: 'Проверь формат прокси', - text: input.externalProxyError, - }; - } - } - - if (input.routeMode === 'local-singbox') { - if (!input.singBoxInstalled) { - return { - ready: false, - title: 'Local sing-box не установлен', - text: 'Установи Local sing-box, чтобы применить локальный маршрут.', - }; - } - - if (!input.selectedServerTag) { - return { - ready: false, - title: 'Сервер не выбран', - text: 'Выбери сервер Local sing-box перед применением маршрута.', - }; - } - } - - return { ready: true }; -} - diff --git a/apps/windows-client/src/app/viewModel.ts b/apps/windows-client/src/app/viewModel.ts deleted file mode 100644 index 2e337ac..0000000 --- a/apps/windows-client/src/app/viewModel.ts +++ /dev/null @@ -1,15 +0,0 @@ -import type { ComponentStatus } from '../domain/types'; -import type { ServiceControlState } from '../ui'; - -export function serviceControlState( - component: ComponentStatus | undefined, - checking: boolean, -): ServiceControlState { - if (checking) return 'checking'; - if (!component) return 'missing'; - if (component.state === 'error') return 'error'; - if (component.running) return 'running'; - if (component.installed) return 'stopped'; - return 'missing'; -} - diff --git a/apps/windows-client/src/domain/types.ts b/apps/windows-client/src/domain/types.ts deleted file mode 100644 index c0a446e..0000000 --- a/apps/windows-client/src/domain/types.ts +++ /dev/null @@ -1,101 +0,0 @@ -export type Protocol = 'TCP' | 'UDP'; -export type ProfileItemType = 'process' | 'folder' | 'exe'; -export type TargetKind = 'local' | 'external'; -export type ProxyProtocol = 'socks5' | 'http'; -export type ComponentId = 'control-app' | 'proxyfier' | 'singbox'; -export type ComponentState = 'installed' | 'missing' | 'stopped' | 'running' | 'error'; -export type ActivityLevel = 'info' | 'warning' | 'error' | 'success'; - -export interface ProfileItemInput { - type: ProfileItemType | string; - value: string; - recursive?: boolean; -} - -export interface ProfileInput { - id?: string; - name: string; - enabled?: boolean; - targetId?: string; - protocols?: string[]; - items?: ProfileItemInput[]; -} - -export interface ProfileItem { - type: ProfileItemType; - value: string; - recursive: boolean; -} - -export interface Profile { - id: string; - name: string; - enabled: boolean; - targetId: string; - protocols: Protocol[]; - items: ProfileItem[]; -} - -export interface TargetInput { - id?: string; - name: string; - kind?: TargetKind | string; - protocol?: ProxyProtocol | string; - host: string; - port: number; - requiresComponent?: ComponentId | string; -} - -export interface Target { - id: string; - name: string; - kind: TargetKind; - protocol: ProxyProtocol; - host: string; - port: number; - requiresComponent?: ComponentId; -} - -export interface ComponentStatus { - id: ComponentId; - name: string; - state: ComponentState; - installed: boolean; - running: boolean; - version?: string; - path?: string; - problems: string[]; - actions: string[]; -} - -export interface LocalSingBoxConfig { - subscriptionDisplayUrl?: string; - hasSubscription: boolean; - selectedServerTag?: string; - listenHost: string; - listenPort: number; - serviceName: string; - installRoot: string; - updatedAt?: string; -} - -export interface SubscriptionServer { - tag: string; - type: string; - server: string; - serverPort: number; -} - -export interface SubscriptionCache { - servers: SubscriptionServer[]; - userInfo: Record; - fetchedAt: string; -} - -export interface ActivityEntry { - id: string; - at: string; - level: ActivityLevel; - title: string; - message: string; -} diff --git a/apps/windows-client/src/main.tsx b/apps/windows-client/src/main.tsx deleted file mode 100644 index 40916e5..0000000 --- a/apps/windows-client/src/main.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import React from 'react'; -import { createRoot } from 'react-dom/client'; -import { App } from './app/App'; -import './styles/app.css'; - -createRoot(document.getElementById('root') as HTMLElement).render( - - - , -); - diff --git a/apps/windows-client/src/styles/app.css b/apps/windows-client/src/styles/app.css deleted file mode 100644 index 2c8308e..0000000 --- a/apps/windows-client/src/styles/app.css +++ /dev/null @@ -1,2668 +0,0 @@ -:root { - --app-footer-height: 54px; - --app-header-row-height: 50px; - --app-tab-height: 46px; - --app-header-height: calc(var(--app-header-row-height) + var(--app-tab-height)); - --motion-fast: 120ms; - --motion-standard: 180ms; - --motion-panel: 220ms; - --ease-out: cubic-bezier(0.23, 1, 0.32, 1); - --ease-standard: cubic-bezier(0.22, 0.72, 0.18, 1); - --surface-canvas: #101216; - --surface-panel: #131720; - --surface-raised: #151923; - --surface-control: #242a35; - --surface-inset: #0d1016; - --border-soft: #2b3342; - --border-strong: #343b49; - --focus-ring: #3b82f6; - font-family: - Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", - sans-serif; - color: #e5e7eb; - background: #101216; - font-synthesis: none; - height: 100%; - overflow: hidden; - scrollbar-color: #4b5568 #101216; - scrollbar-gutter: stable; - scrollbar-width: thin; - text-rendering: optimizeLegibility; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; -} - -::-webkit-scrollbar { - width: 8px; - height: 8px; -} - -::-webkit-scrollbar-button { - display: none; - width: 0; - height: 0; -} - -::-webkit-scrollbar-track { - background: #101216; -} - -::-webkit-scrollbar-thumb { - min-height: 36px; - border: 2px solid #101216; - border-radius: 999px; - background: #4b5568; -} - -::-webkit-scrollbar-thumb:hover { - background: #64748b; -} - -::-webkit-scrollbar-corner { - background: #101216; -} - -* { - box-sizing: border-box; -} - -body { - margin: 0; - height: 100vh; - background: #101216; - overflow: hidden; -} - -button, -input { - font: inherit; -} - -button { - border: 0; -} - -button:disabled { - cursor: not-allowed; - opacity: 0.56; -} - -.ui-button, -.ui-icon-button { - appearance: none; - display: inline-flex; - align-items: center; - justify-content: center; - gap: 8px; - border: 1px solid var(--border-strong); - border-radius: 4px; - background: var(--surface-control); - color: #eef2ff; - cursor: pointer; - font-weight: 750; - line-height: 1; - text-decoration: none; - transition: - background-color var(--motion-fast) var(--ease-out), - border-color var(--motion-fast) var(--ease-out), - color var(--motion-fast) var(--ease-out), - opacity var(--motion-fast) var(--ease-out), - transform var(--motion-fast) var(--ease-out); -} - -.ui-button:hover, -.ui-icon-button:hover { - background: #2d3543; -} - -.ui-button:active, -.ui-icon-button:active { - transform: scale(0.98); -} - -.ui-button:focus-visible, -.ui-icon-button:focus-visible, -.ui-tab:focus-visible, -.ui-action-menu-popover button:focus-visible { - outline: 0; - box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.72); -} - -.ui-button:disabled, -.ui-icon-button:disabled { - cursor: not-allowed; - opacity: 0.56; - transform: none; -} - -.ui-button--sm { - min-height: 32px; - padding: 6px 10px; - font-size: 13px; -} - -.ui-button--md { - min-height: 36px; - padding: 8px 12px; - font-size: 14px; -} - -.ui-button--lg { - min-height: 46px; - padding: 10px 14px; - font-size: 14px; -} - -.ui-button--primary { - border-color: #16a34a; - background: #22c55e; - color: #04130a; -} - -.ui-button--primary:hover { - background: #4ade80; -} - -.ui-button--add, -.ui-icon-button--add { - border-color: #2b3342; - background: #202633; - color: #dbeafe; -} - -.ui-button--add:hover, -.ui-icon-button--add:hover { - border-color: var(--focus-ring); - background: #182033; -} - -.ui-button--danger, -.ui-icon-button--danger { - color: #fecaca; -} - -.ui-button--danger:hover, -.ui-icon-button--danger:hover { - border-color: rgba(239, 68, 68, 0.52); - background: rgba(127, 29, 29, 0.34); -} - -.ui-icon-button { - width: 42px; - min-width: 42px; - min-height: 38px; - padding: 0; -} - -.ui-button-icon, -.ui-button-label { - display: inline-flex; - align-items: center; - min-width: 0; -} - -.ui-button-spinner { - width: 14px; - height: 14px; - border: 2px solid currentColor; - border-top-color: transparent; - border-radius: 999px; - animation: spin 0.75s linear infinite; -} - -.ui-tabs { - position: fixed; - top: var(--app-header-row-height); - right: 0; - left: 0; - z-index: 40; - display: flex; - align-items: stretch; - gap: 0; - min-height: var(--app-tab-height); - overflow-x: auto; - border-top: 1px solid #242a35; - border-bottom: 1px solid var(--border-soft); - background: #181b22; - scrollbar-width: none; - margin: 0; - padding: 0 18px; -} - -.ui-tabs::-webkit-scrollbar { - display: none; -} - -.ui-tab { - appearance: none; - position: relative; - flex: 1 1 0; - min-width: 0; - min-height: var(--app-tab-height); - border: 0; - border-radius: 0; - background: transparent; - color: #9aa8bd; - cursor: pointer; - font-weight: 750; - margin: 0; - padding: 10px 12px 12px; - transition: - background-color var(--motion-fast) var(--ease-out), - color var(--motion-fast) var(--ease-out); - white-space: nowrap; -} - -.ui-tab:hover { - background: #1c212b; - color: #eef2ff; -} - -.ui-tab.is-active { - color: #eff6ff; -} - -.ui-tab.is-active::after { - position: absolute; - right: 12px; - bottom: 0; - left: 12px; - height: 3px; - border-radius: 999px 999px 0 0; - background: var(--focus-ring); - box-shadow: 0 -6px 18px rgba(59, 130, 246, 0.34); - content: ""; -} - -.ui-status-pill { - display: inline-flex; - align-items: center; - width: fit-content; - border: 1px solid var(--border-soft); - border-radius: 999px; - background: #1a202b; - color: #cbd5e1; - padding: 5px 9px; - font-size: 12px; - font-weight: 750; -} - -.ui-status-pill--ok { - border-color: rgba(34, 197, 94, 0.38); - color: #86efac; -} - -.ui-status-pill--warning { - border-color: rgba(245, 158, 11, 0.46); - color: #fcd34d; -} - -.ui-status-pill--error { - border-color: rgba(239, 68, 68, 0.46); - color: #fecaca; -} - -.ui-status-pill--checking { - border-color: rgba(59, 130, 246, 0.46); - color: #bfdbfe; -} - -.ui-field { - display: grid; - gap: 7px; -} - -.ui-field-label { - color: #8d99ae; -} - -.ui-field-row { - display: grid; - grid-template-columns: minmax(0, 1fr) auto; - gap: 8px; - align-items: stretch; -} - -.ui-field input { - min-height: 42px; - width: 100%; - border: 1px solid var(--border-strong); - border-radius: 4px; - background: var(--surface-inset); - color: #f8fafc; - outline: none; - padding: 9px 11px; -} - -.ui-field input:focus { - border-color: var(--focus-ring); - box-shadow: 0 0 0 1px var(--focus-ring); -} - -.ui-field-help { - color: #8d99ae; - font-size: 12px; -} - -.ui-field-help.is-error { - color: #fcd34d; -} - -.ui-action-menu { - position: relative; - display: flex; -} - -.ui-action-menu-popover { - position: absolute; - top: calc(100% + 6px); - right: 0; - z-index: 8; - min-width: 172px; - border: 1px solid var(--border-strong); - border-radius: 4px; - background: #171c26; - box-shadow: 0 12px 30px rgba(0, 0, 0, 0.38); - padding: 5px; - transform-origin: top right; - animation: ui-popover-in var(--motion-standard) var(--ease-out); -} - -.ui-action-menu-popover button { - width: 100%; - min-height: 34px; - border: 0; - border-radius: 3px; - background: transparent; - color: #e5e7eb; - cursor: pointer; - font: inherit; - padding: 7px 9px; - text-align: left; -} - -.ui-action-menu-popover button:hover { - background: #242a35; -} - -.ui-action-menu-popover button.is-danger { - color: #fecaca; -} - -.ui-action-menu-popover button.is-danger:hover { - background: rgba(127, 29, 29, 0.42); -} - -.ui-service-row { - position: relative; - isolation: isolate; - display: grid; - grid-template-columns: auto minmax(220px, 1fr) auto; - align-items: center; - gap: 12px; - justify-content: stretch; - min-height: 56px; - overflow: visible; - border: 1px solid var(--border-soft); - border-radius: 4px; - background: var(--surface-raised); - padding: 12px; -} - -.ui-service-row > :not(.ui-service-border-glow) { - position: relative; - z-index: 1; -} - -.ui-service-border-glow { - position: absolute; - display: none; - z-index: 0; - inset: 0; - overflow: hidden; - border-radius: inherit; - opacity: 0; - contain: layout paint; - pointer-events: none; - transition: opacity var(--motion-panel) var(--ease-out); -} - -.ui-service-row--checking .ui-service-border-glow, -.ui-service-row--working .ui-service-border-glow { - display: block; - opacity: 0.78; -} - -.ui-service-row--settling .ui-service-border-glow { - display: block; - opacity: 0; - transition-duration: 0.7s; -} - -.ui-service-border-glow-segment { - position: absolute; - display: block; - background: #93c5fd; - box-shadow: - 0 0 8px rgba(96, 165, 250, 0.95), - 0 0 16px rgba(34, 197, 94, 0.36); - opacity: 0; -} - -.ui-service-border-glow-segment.top, -.ui-service-border-glow-segment.bottom { - width: 108px; - height: 2px; - background: linear-gradient(90deg, transparent, #60a5fa 24%, #bbf7d0 54%, transparent); -} - -.ui-service-border-glow-segment.right, -.ui-service-border-glow-segment.left { - width: 2px; - height: 64px; - background: linear-gradient(180deg, transparent, #60a5fa 24%, #bbf7d0 54%, transparent); -} - -.ui-service-border-glow-segment.top { - top: 0; - animation: finder-border-top 1.6s linear infinite; -} - -.ui-service-border-glow-segment.right { - right: 0; - animation: finder-border-right 1.6s linear infinite; -} - -.ui-service-border-glow-segment.bottom { - bottom: 0; - animation: finder-border-bottom 1.6s linear infinite; -} - -.ui-service-border-glow-segment.left { - left: 0; - animation: finder-border-left 1.6s linear infinite; -} - -.ui-service-dot { - flex: 0 0 auto; - width: 11px; - height: 11px; - border-radius: 999px; - background: #f59e0b; - box-shadow: 0 0 0 4px rgba(245, 158, 11, 0.12); -} - -.ui-service-row--running .ui-service-dot, -.ui-service-row--installed .ui-service-dot { - background: #22c55e; - box-shadow: 0 0 0 4px rgba(34, 197, 94, 0.12); -} - -.ui-service-row--stopped .ui-service-dot, -.ui-service-row--missing .ui-service-dot { - background: #f59e0b; -} - -.ui-service-row--error .ui-service-dot { - background: #ef4444; - box-shadow: 0 0 0 4px rgba(239, 68, 68, 0.12); -} - -.ui-service-row--checking .ui-service-dot { - border: 2px solid var(--focus-ring); - border-top-color: transparent; - background: transparent; - box-shadow: none; - animation: spin 0.75s linear infinite; -} - -.ui-service-text { - min-width: 0; -} - -.ui-service-text > strong, -.ui-service-text > span { - display: block; - overflow-wrap: anywhere; -} - -.ui-service-text > span { - color: #8d99ae; -} - -.ui-service-inline-actions { - display: flex; - align-items: center; - flex-wrap: wrap; - gap: 6px; - margin-top: 6px; -} - -.ui-service-actions { - display: flex; - align-items: center; - gap: 6px; -} - -.ui-service-row > .setup-details, -.ui-service-row > .singbox-info-popover, -.ui-service-row > .singbox-workspace, -.ui-service-row > .singbox-install-note { - grid-column: 1 / -1; -} - -.log-raw-detail { - color: #7d8aa0; - margin-top: 3px; -} - -@keyframes ui-popover-in { - from { - opacity: 0; - transform: scale(0.97); - } - - to { - opacity: 1; - transform: scale(1); - } -} - -.simple-shell { - display: block; - height: 100vh; - overflow: hidden; - background: #101216; - padding: var(--app-header-height) 0 var(--app-footer-height); -} - -.simple-panel { - display: grid; - align-content: start; - min-height: 0; - height: calc(100vh - var(--app-header-height) - var(--app-footer-height)); - width: 100%; - border: 0; - border-radius: 0; - background: #101216; - box-shadow: none; - overflow-x: hidden; - overflow-y: scroll; - scrollbar-gutter: stable; - padding: 14px 18px 20px; -} - -.simple-header, -.section-head, -.add-toolbar, -.process-add-line, -.app-row, -.route-switch, -.singbox-info-actions, -.panel-tabs, -.server-row { - display: flex; - align-items: center; - justify-content: space-between; - gap: 12px; -} - -.simple-header { - position: fixed; - top: 0; - right: 0; - left: 0; - z-index: 40; - min-height: var(--app-header-row-height); - margin: 0; - background: #181b22; - padding: 7px 14px; -} - -.simple-header small, -.simple-field span, -.app-row-main > div > span, -.config-path, -.finder-card span { - color: #8d99ae; -} - -.simple-header h1, -.section-head h2 { - margin: 0; - letter-spacing: 0; -} - -.simple-header h1 { - margin-top: 0; - font-size: 18px; - line-height: 1.1; - font-weight: 650; -} - -.simple-header .ghost-button { - min-height: 32px; - padding: 6px 10px; - font-size: 13px; -} - -.section-head h2 { - font-size: 16px; -} - -.panel-tabs { - position: fixed; - top: var(--app-header-row-height); - right: 0; - left: 0; - z-index: 40; - display: flex; - align-items: stretch; - gap: 0; - min-height: var(--app-tab-height); - overflow-x: auto; - border-top: 1px solid #242a35; - border-bottom: 1px solid #2b3342; - background: #181b22; - scrollbar-width: none; - margin: 0; - padding: 0 18px; -} - -.panel-tabs::-webkit-scrollbar { - display: none; -} - -.panel-tabs button { - appearance: none; - position: relative; - flex: 1 1 0; - min-width: 0; - min-height: var(--app-tab-height); - border: 0; - border-radius: 0; - background: transparent; - color: #9aa8bd; - font-weight: 750; - cursor: pointer; - margin: 0; - padding: 10px 12px 12px; - white-space: nowrap; -} - -.panel-tabs button:hover { - background: #1c212b; - color: #eef2ff; -} - -.panel-tabs button.active { - background: #181b22; - color: #eff6ff; - box-shadow: none; -} - -.panel-tabs button.active::after { - position: absolute; - right: 12px; - bottom: 0; - left: 12px; - height: 3px; - border-radius: 999px 999px 0 0; - background: #3b82f6; - box-shadow: 0 -6px 18px rgba(59, 130, 246, 0.34); - content: ""; -} - -.tab-panel-frame { - min-width: 0; - will-change: opacity, transform; -} - -.tab-panel-frame.swipe-right { - animation: panel-swipe-right 0.24s cubic-bezier(0.22, 0.72, 0.18, 1); -} - -.tab-panel-frame.swipe-left { - animation: panel-swipe-left 0.24s cubic-bezier(0.22, 0.72, 0.18, 1); -} - -.tab-panel { - display: grid; - gap: 10px; -} - -.panel-section-head { - display: flex; - align-items: end; - justify-content: space-between; - gap: 12px; - min-height: 48px; - border: 1px solid #2b3342; - border-radius: 4px; - background: #131720; - padding: 11px 12px; -} - -.panel-section-head span { - display: block; - color: #8d99ae; - font-size: 12px; -} - -.panel-section-head h2 { - margin: 2px 0 0; - color: #eef2ff; - font-size: 17px; - letter-spacing: 0; -} - -.panel-section-head > strong { - min-width: 0; - color: #bfdbfe; - font-size: 13px; - overflow-wrap: anywhere; - text-align: right; -} - -.summary-panel { - gap: 12px; -} - -.summary-hero { - display: grid; - grid-template-columns: auto minmax(0, 1fr); - gap: 12px; - align-items: start; - border: 1px solid #2b3342; - border-radius: 4px; - background: #151923; - padding: 14px; -} - -.summary-hero.ok { - border-color: rgba(34, 197, 94, 0.44); -} - -.summary-hero.warning { - border-color: rgba(245, 158, 11, 0.48); -} - -.summary-hero.error { - border-color: rgba(239, 68, 68, 0.52); -} - -.summary-hero.checking { - border-color: rgba(59, 130, 246, 0.54); -} - -.summary-status-dot { - width: 14px; - height: 14px; - border-radius: 999px; - background: #64748b; - margin-top: 5px; -} - -.summary-status-dot.ok { - background: #22c55e; - box-shadow: 0 0 0 4px rgba(34, 197, 94, 0.12); -} - -.summary-status-dot.warning { - background: #f59e0b; - box-shadow: 0 0 0 4px rgba(245, 158, 11, 0.12); -} - -.summary-status-dot.error { - background: #ef4444; - box-shadow: 0 0 0 4px rgba(239, 68, 68, 0.12); -} - -.summary-status-dot.checking { - border: 2px solid #3b82f6; - border-top-color: transparent; - background: transparent; - animation: spin 0.75s linear infinite; -} - -.summary-hero span, -.summary-card-label { - color: #8d99ae; - font-size: 12px; - font-weight: 700; -} - -.summary-hero strong { - display: block; - color: #f8fafc; - font-size: 22px; - line-height: 1.15; - margin-top: 2px; -} - -.summary-hero p { - color: #b6c2d4; - margin: 5px 0 0; - overflow-wrap: anywhere; -} - -.summary-readout, -.summary-draft { - border: 1px solid #2b3342; - border-radius: 4px; - background: #131720; - padding: 13px 14px; -} - -.summary-readout-head { - display: flex; - align-items: start; - justify-content: space-between; - gap: 12px; -} - -.summary-readout-head span, -.summary-draft span { - display: block; - color: #8d99ae; - font-size: 12px; - font-weight: 700; -} - -.summary-readout-head strong, -.summary-draft strong { - display: block; - color: #f8fafc; - font-size: 18px; - line-height: 1.2; - margin-top: 2px; -} - -.summary-pill { - flex: 0 0 auto; - border: 1px solid #2b3342; - border-radius: 999px; - background: #1a202b; - padding: 5px 9px; -} - -.summary-pill.ok { - border-color: rgba(34, 197, 94, 0.38); - color: #86efac; -} - -.summary-pill.warning { - border-color: rgba(245, 158, 11, 0.46); - color: #fcd34d; -} - -.summary-lines { - display: grid; - gap: 0; - margin: 12px 0 0; -} - -.summary-lines.compact { - margin-top: 10px; -} - -.summary-lines div { - display: grid; - grid-template-columns: 132px minmax(0, 1fr); - gap: 12px; - border-top: 1px solid #242b38; - padding: 9px 0; -} - -.summary-lines dt, -.summary-lines dd { - margin: 0; -} - -.summary-lines dt { - color: #8d99ae; - font-size: 12px; - font-weight: 750; -} - -.summary-lines dd { - color: #dbe4f0; - overflow-wrap: anywhere; -} - -.summary-draft { - display: grid; - gap: 8px; -} - -.summary-draft.ok { - border-color: rgba(34, 197, 94, 0.34); -} - -.summary-draft.warning { - border-color: rgba(245, 158, 11, 0.48); - background: rgba(120, 53, 15, 0.16); -} - -.summary-draft p { - color: #b6c2d4; - margin: 5px 0 0; - overflow-wrap: anywhere; -} - -.summary-actions { - display: flex; - gap: 8px; - align-items: center; -} - -.summary-actions button { - min-height: 34px; - border: 1px solid #343b49; - border-radius: 4px; - background: #242a35; - color: #eef2ff; - padding: 7px 10px; - cursor: pointer; -} - -.summary-actions button:hover { - background: #2d3543; -} - -.summary-grid { - display: grid; - grid-template-columns: repeat(3, minmax(0, 1fr)); - gap: 9px; -} - -.summary-card { - display: grid; - gap: 5px; - align-content: start; - min-height: 104px; - border: 1px solid #2b3342; - border-radius: 4px; - background: #131720; - color: #dbeafe; - padding: 11px 12px; - text-align: left; -} - -button.summary-card { - cursor: pointer; -} - -button.summary-card:hover { - border-color: #3b82f6; - background: #172033; -} - -.summary-card.ok { - border-color: rgba(34, 197, 94, 0.36); -} - -.summary-card.warning { - border-color: rgba(245, 158, 11, 0.42); -} - -.summary-card.error { - border-color: rgba(239, 68, 68, 0.42); -} - -.summary-card.checking { - border-color: rgba(59, 130, 246, 0.42); -} - -.summary-card strong, -.summary-card span { - overflow-wrap: anywhere; -} - -.summary-card strong { - color: #f8fafc; - font-size: 15px; - line-height: 1.25; -} - -.summary-card > span:not(.summary-card-label) { - color: #9aa8bd; - font-size: 13px; -} - -.summary-card.readout { - cursor: default; -} - -.ghost-button, -.add-toolbar button, -.process-add-line button, -.app-row button, -.open-config-button, -.service-button, -.service-menu-button, -.subscription-line button, -.route-switch button, -.singbox-info-actions button, -.server-row { - min-height: 36px; - border: 1px solid #343b49; - border-radius: 4px; - background: #242a35; - color: #eef2ff; - padding: 8px 12px; - cursor: pointer; -} - -.ghost-button:hover, -.add-toolbar button:hover, -.process-add-line button:hover, -.app-row button:hover, -.open-config-button:hover, -.service-button:hover, -.service-menu-button:hover, -.subscription-line button:hover, -.route-switch button:hover, -.singbox-info-actions button:hover, -.server-row:hover { - background: #2d3543; -} - -.finder-card { - position: relative; - isolation: isolate; - display: grid; - grid-template-columns: auto minmax(220px, 1fr) auto; - align-items: center; - gap: 12px; - justify-content: stretch; - min-height: 56px; - overflow: visible; - border: 1px solid #2b3342; - border-radius: 4px; - background: #151923; - padding: 12px; -} - -.singbox-card { - margin-top: 10px; -} - -.finder-card .finder-border-glow { - position: absolute; - display: none; - z-index: 0; - inset: 0; - overflow: hidden; - border-radius: inherit; - opacity: 0; - contain: layout paint; - pointer-events: none; - transition: opacity 0.22s ease; -} - -.finder-card > :not(.finder-border-glow) { - position: relative; - z-index: 1; -} - -.finder-card.checking .finder-border-glow, -.finder-card.working .finder-border-glow { - display: block; - opacity: 1; -} - -.finder-card.checking .finder-border-glow { - opacity: 0.78; -} - -.finder-card.settling .finder-border-glow { - display: block; - opacity: 0; - transition-duration: 0.7s; -} - -.finder-border-glow-segment { - position: absolute; - display: block; - background: #93c5fd; - box-shadow: - 0 0 8px rgba(96, 165, 250, 0.95), - 0 0 16px rgba(34, 197, 94, 0.36); - opacity: 0; -} - -.finder-border-glow-segment.top, -.finder-border-glow-segment.bottom { - width: 108px; - height: 2px; - background: linear-gradient(90deg, transparent, #60a5fa 24%, #bbf7d0 54%, transparent); -} - -.finder-border-glow-segment.right, -.finder-border-glow-segment.left { - width: 2px; - height: 64px; - background: linear-gradient(180deg, transparent, #60a5fa 24%, #bbf7d0 54%, transparent); -} - -.finder-border-glow-segment.top { - top: 0; - animation: finder-border-top 1.6s linear infinite; -} - -.finder-border-glow-segment.right { - right: 0; - animation: finder-border-right 1.6s linear infinite; -} - -.finder-border-glow-segment.bottom { - bottom: 0; - animation: finder-border-bottom 1.6s linear infinite; -} - -.finder-border-glow-segment.left { - left: 0; - animation: finder-border-left 1.6s linear infinite; -} - -.finder-text, -.app-row > div, -.app-row-main > div { - min-width: 0; -} - -.finder-text > strong, -.finder-text > span, -.app-row-main strong, -.app-row-main > div > span { - display: block; - overflow-wrap: anywhere; -} - -.finder-text > strong { - max-width: 520px; -} - -.finder-text > span { - max-width: 760px; -} - -.finder-card .subscription-icon, -.finder-card .server-icon { - display: grid; -} - -.status-light { - flex: 0 0 auto; - width: 11px; - height: 11px; - border-radius: 999px; - background: #ef4444; -} - -.finder-card.found .status-light { - background: #22c55e; - box-shadow: 0 0 0 4px rgba(34, 197, 94, 0.12); -} - -.finder-card.missing .status-light { - background: #f59e0b; - box-shadow: 0 0 0 4px rgba(245, 158, 11, 0.12); -} - -.finder-card.checking .status-light { - border: 2px solid #3b82f6; - border-top-color: transparent; - background: transparent; - box-shadow: none; - animation: spin 0.75s linear infinite; -} - -.setup-toggle { - display: inline-flex; - gap: 7px; - align-items: center; - width: fit-content; - min-height: 0; - border: 0; - background: transparent; - color: #93c5fd; - padding: 4px 0 0; - text-align: left; - cursor: pointer; -} - -.setup-toggle:hover { - color: #bfdbfe; -} - -.setup-toggle .ui-button-label > span { - display: inline-block; - border: 1px solid #343b49; - border-radius: 4px; - background: #1b202b; - color: #cbd5e1; - padding: 1px 6px; - font-size: 11px; - font-weight: 700; -} - -.setup-strip { - display: grid; - grid-template-columns: auto minmax(0, 1fr); - gap: 10px; - align-items: center; - min-height: 42px; - border: 1px solid #2b3342; - border-radius: 4px; - background: #111720; - padding: 7px 10px; -} - -.setup-strip-title { - color: #8d99ae; - font-size: 12px; - font-weight: 800; -} - -.setup-strip-items { - display: flex; - gap: 7px; - min-width: 0; - overflow-x: auto; - scrollbar-width: thin; -} - -.setup-strip-item { - display: inline-flex; - flex: 0 0 auto; - gap: 7px; - align-items: center; - min-height: 28px; - border: 1px solid #2b3342; - border-radius: 999px; - background: #151b25; - color: #dbeafe; - padding: 5px 9px; -} - -.setup-strip-dot { - width: 8px; - height: 8px; - border-radius: 999px; - background: #f59e0b; - box-shadow: 0 0 0 3px rgba(245, 158, 11, 0.11); -} - -.setup-strip-item.installed .setup-strip-dot { - background: #22c55e; - box-shadow: 0 0 0 3px rgba(34, 197, 94, 0.11); -} - -.setup-strip-item strong { - font-size: 12px; - white-space: nowrap; -} - -.setup-strip-item span:not(.setup-strip-dot) { - color: #9aa8bd; - font-size: 12px; - white-space: nowrap; -} - -.setup-details { - display: grid; - grid-column: 2 / -1; - gap: 6px; - border-top: 1px solid #2b3342; - margin-top: 2px; - padding-top: 10px; -} - -.setup-item { - display: grid; - grid-template-columns: auto minmax(0, 1fr); - gap: 9px; - align-items: start; - min-height: 34px; - border: 1px solid #263040; - border-radius: 4px; - background: #111720; - padding: 8px 9px; -} - -.setup-state-dot { - width: 9px; - height: 9px; - border-radius: 999px; - background: #f59e0b; - box-shadow: 0 0 0 3px rgba(245, 158, 11, 0.12); - margin-top: 5px; -} - -.setup-item.installed .setup-state-dot { - background: #22c55e; - box-shadow: 0 0 0 3px rgba(34, 197, 94, 0.12); -} - -.setup-item strong, -.setup-item span { - display: block; - overflow-wrap: anywhere; -} - -.setup-item span { - color: #9aa8bd; - font-size: 13px; -} - -.service-actions { - position: relative; - display: flex; - gap: 6px; - align-items: center; -} - -.service-button { - min-width: 102px; - white-space: nowrap; -} - -.service-button.stop { - color: #fecaca; -} - -.service-button.install { - border-color: #166534; - background: #14532d; - color: #dcfce7; - font-weight: 700; -} - -.service-button.install:hover { - background: #166534; -} - -.service-menu { - position: relative; - display: flex; -} - -.service-menu-button { - display: grid; - place-items: center; - width: 42px; - padding: 0; -} - -.service-menu-button svg { - display: block; -} - -.service-menu-popover { - position: absolute; - top: calc(100% + 6px); - right: 0; - z-index: 6; - min-width: 172px; - border: 1px solid #343b49; - border-radius: 4px; - background: #171c26; - box-shadow: 0 12px 30px rgba(0, 0, 0, 0.38); - padding: 5px; -} - -.service-menu-popover button { - width: 100%; - min-height: 34px; - border: 0; - border-radius: 3px; - background: transparent; - color: #fecaca; - text-align: left; - padding: 7px 9px; - cursor: pointer; -} - -.service-menu-popover button:hover { - background: rgba(127, 29, 29, 0.42); -} - -.route-panel { - display: grid; - gap: 8px; - margin-top: 10px; - border: 1px solid #2b3342; - border-radius: 4px; - background: #151923; - padding: 10px; -} - -.external-proxy-card { - display: grid; - gap: 9px; -} - -.proxy-check-line { - display: grid; - grid-template-columns: minmax(0, 1fr) 132px; - gap: 8px; - align-items: stretch; -} - -.proxy-check-state, -.route-preview { - display: grid; - gap: 3px; - min-height: 46px; - border: 1px solid #2b3342; - border-radius: 4px; - background: #111720; - padding: 9px 11px; -} - -.proxy-check-state.ok { - border-color: rgba(34, 197, 94, 0.42); -} - -.proxy-check-state.warning { - border-color: rgba(245, 158, 11, 0.42); -} - -.proxy-check-state.error { - border-color: rgba(239, 68, 68, 0.42); -} - -.proxy-check-state strong, -.route-preview strong { - color: #eef2ff; - overflow-wrap: anywhere; -} - -.proxy-check-state span, -.route-preview span { - color: #8d99ae; - overflow-wrap: anywhere; -} - -.route-preview { - margin-top: 2px; -} - -.simple-field { - display: grid; - gap: 7px; - margin: 14px 0; -} - -.route-proxy-field { - margin: 0; -} - -.simple-field input, -.process-add-line input, -.subscription-line input { - min-height: 42px; - width: 100%; - border: 1px solid #343b49; - border-radius: 4px; - background: #0d1016; - color: #f8fafc; - outline: none; - padding: 9px 11px; -} - -.simple-field input:focus, -.process-add-line input:focus, -.subscription-line input:focus { - border-color: #3b82f6; - box-shadow: 0 0 0 1px #3b82f6; -} - -.singbox-workspace { - display: grid; - grid-column: 1 / -1; - gap: 8px; - border-top: 1px solid #2b3342; - margin-top: 2px; - padding-top: 10px; -} - -.singbox-inline-actions { - display: flex; - align-items: center; - flex-wrap: wrap; - gap: 6px; -} - -.info-toggle { - display: grid; - place-items: center; - width: 28px; - height: 24px; - border: 1px solid #263040; - border-radius: 4px; - background: #111720; - color: #bfdbfe; - cursor: pointer; -} - -.info-toggle:hover { - background: #1d2735; -} - -.info-toggle svg { - display: block; -} - -.singbox-info-popover { - position: relative; - z-index: 8; - display: grid; - grid-column: 1 / -1; - gap: 10px; - width: 100%; - border: 1px solid #343b49; - border-radius: 4px; - background: #111720; - box-shadow: none; - padding: 12px; -} - -.singbox-info-grid { - display: grid; - grid-template-columns: 86px minmax(0, 1fr); - gap: 7px 10px; - align-items: start; -} - -.singbox-info-grid span { - color: #8d99ae; - font-size: 12px; -} - -.singbox-info-grid strong { - min-width: 0; - color: #dbeafe; - font-size: 12px; - overflow-wrap: anywhere; -} - -.singbox-info-actions { - justify-content: flex-start; - gap: 7px; -} - -.singbox-info-actions button { - display: inline-flex; - align-items: center; - gap: 7px; - min-height: 32px; - color: #dbeafe; - font-weight: 700; -} - -.singbox-info-actions button svg { - display: block; -} - -.route-switch { - display: grid; - grid-template-columns: repeat(2, minmax(0, 1fr)); - gap: 6px; -} - -.route-switch button { - min-height: 38px; - width: 100%; - color: #cbd5e1; - font-weight: 700; -} - -.route-switch button.active { - border-color: #2563eb; - background: #1d4ed8; - color: #eff6ff; -} - -.route-switch .ui-button--primary { - border-color: #2563eb; - background: #1d4ed8; - color: #eff6ff; -} - -.subscription-line { - display: grid; - grid-template-columns: 38px minmax(0, 1fr) 112px 42px; - gap: 7px; - align-items: center; -} - -.subscription-icon, -.server-icon { - display: grid; - place-items: center; - color: #bfdbfe; -} - -.subscription-icon { - width: 38px; - height: 42px; - border: 1px solid #2b3342; - border-radius: 4px; - background: #0d1016; -} - -.subscription-line button { - min-height: 42px; - white-space: nowrap; -} - -.subscription-line .icon-command { - display: grid; - place-items: center; - width: 42px; - padding: 0; - color: #fecaca; -} - -.server-list { - display: grid; - grid-template-columns: repeat(5, minmax(0, 1fr)); - gap: 7px; -} - -.server-row { - display: grid; - grid-template-columns: minmax(0, 1fr) 34px; - align-items: center; - justify-content: stretch; - gap: 7px; - width: 100%; - min-width: 0; - min-height: 38px; - text-align: left; - padding: 7px 9px; -} - -.server-row.selected { - border-color: #2563eb; - background: #17213a; -} - -.server-row.ok { - border-color: rgba(34, 197, 94, 0.42); -} - -.server-row.error { - border-color: rgba(239, 68, 68, 0.42); -} - -.server-select-button, -.server-ping-button { - border: 0; - background: transparent; - color: inherit; - cursor: pointer; - padding: 0; -} - -.server-select-button { - display: flex; - align-items: center; - gap: 7px; - min-width: 0; - text-align: left; -} - -.server-ping-button { - display: grid; - place-items: center; - width: 30px; - height: 28px; - border: 1px solid #263040; - border-radius: 4px; - color: #bfdbfe; -} - -.server-ping-button:hover { - background: #1d2735; -} - -.server-select-dot { - width: 10px; - height: 10px; - border: 1px solid #64748b; - border-radius: 999px; - background: #0d1016; -} - -.server-row.selected .server-select-dot { - border-color: #93c5fd; - background: #60a5fa; - box-shadow: 0 0 0 3px rgba(96, 165, 250, 0.16); -} - -.server-row strong { - min-width: 0; - overflow: hidden; - color: #dbeafe; - font-size: 13px; - text-overflow: ellipsis; - white-space: nowrap; -} - -.server-ping-badge { - flex: 0 0 auto; - border: 1px solid #263040; - border-radius: 4px; - color: #bfdbfe; - padding: 2px 5px; - font-size: 11px; - font-weight: 800; -} - -.singbox-install-note { - display: grid; - grid-column: 1 / -1; - gap: 3px; - border-top: 1px solid #2b3342; - color: #9aa8bd; - margin-top: 2px; - padding-top: 10px; -} - -.singbox-install-note strong { - color: #e5e7eb; -} - -.apply-state { - display: grid; - gap: 3px; - margin-top: 14px; - border: 1px solid rgba(245, 158, 11, 0.48); - border-radius: 4px; - background: rgba(120, 53, 15, 0.2); - color: #fcd34d; - padding: 10px 12px; -} - -.apply-state span { - color: #d6b875; - overflow-wrap: anywhere; -} - -.apps-section { - display: grid; - gap: 9px; - margin-top: 8px; - border: 1px solid #2b3342; - border-radius: 4px; - background: #10141c; - padding: 12px; -} - -.apps-header { - display: grid; - grid-template-columns: minmax(0, 1fr) minmax(340px, 520px); - gap: 12px; - align-items: center; - min-height: 48px; -} - -.apps-title { - min-height: 48px; - align-items: center; - justify-content: flex-start; -} - -.apps-config-actions { - display: grid; - gap: 8px; - align-items: center; -} - -.apps-config-actions .apply-state { - margin: 0; -} - -.apps-config-actions .command-row { - grid-template-columns: minmax(0, 1fr) 118px; - margin-top: 0; -} - -.apps-config-actions .apply-button, -.apps-config-actions .open-config-button { - height: 48px; - min-height: 48px; -} - -.apps-config-actions .config-path { - margin: 0; - color: #8d99ae; -} - -.section-head span { - min-width: 28px; - height: 30px; - border: 1px solid #343b49; - border-radius: 4px; - background: #1b202b; - color: #dbeafe; - padding: 0 9px; - text-align: center; - font-size: 12px; - font-weight: 700; - line-height: 28px; -} - -.add-toolbar { - justify-content: flex-end; - padding: 0; -} - -.add-tile { - display: inline-flex; - align-items: center; - justify-content: center; - position: relative; - width: 44px; - height: 38px; - border-color: #2b3342; - background: #202633; - color: #dbeafe; - line-height: 0; - padding: 0; -} - -.add-tile svg { - width: 20px; - height: 20px; - display: block; - flex: 0 0 auto; - transform: translate(0.5px, 0.5px); -} - -.add-tile:hover { - border-color: #3b82f6; - background: #182033; -} - -.add-tile[data-tooltip]::before, -.add-tile[data-tooltip]::after { - position: absolute; - left: 50%; - z-index: 30; - pointer-events: none; - opacity: 0; - transition: opacity 90ms ease, transform 120ms ease; - transition-delay: 55ms; -} - -.add-tile[data-tooltip]::before { - bottom: calc(100% + 3px); - width: 8px; - height: 8px; - background: #0d1118; - border-right: 1px solid #334155; - border-bottom: 1px solid #334155; - content: ""; - transform: translate(-50%, 2px) rotate(45deg); -} - -.add-tile[data-tooltip]::after { - bottom: calc(100% + 8px); - border: 1px solid #334155; - border-radius: 4px; - background: #0d1118; - box-shadow: 0 10px 24px rgba(0, 0, 0, 0.34); - color: #e5e7eb; - content: attr(data-tooltip); - font-size: 12px; - font-weight: 750; - line-height: 1; - padding: 7px 8px; - transform: translate(-50%, 4px); - white-space: nowrap; -} - -.add-tile[data-tooltip]:hover::before, -.add-tile[data-tooltip]:hover::after, -.add-tile[data-tooltip]:focus-visible::before, -.add-tile[data-tooltip]:focus-visible::after { - opacity: 1; -} - -.add-tile[data-tooltip]:hover::before, -.add-tile[data-tooltip]:focus-visible::before { - transform: translate(-50%, 0) rotate(45deg); -} - -.add-tile[data-tooltip]:hover::after, -.add-tile[data-tooltip]:focus-visible::after { - transform: translate(-50%, 0); -} - -.add-tile[aria-pressed="true"] { - border-color: #3b82f6; - background: #1e3a8a; -} - -.tile-loading { - color: #dbeafe; - font-weight: 800; - animation: pulse 1s ease-in-out infinite; -} - -.app-add-skeleton { - display: grid; - grid-template-columns: minmax(0, 1fr) auto; - gap: 12px; - align-items: center; - min-height: 58px; - border: 1px dashed #334155; - border-radius: 4px; - background: - linear-gradient(90deg, transparent, rgba(148, 163, 184, 0.08), transparent), - #121722; - background-size: 220% 100%; - color: #94a3b8; - padding: 9px 12px; -} - -.app-add-main { - display: flex; - gap: 10px; - align-items: center; - min-width: 0; -} - -.app-add-main strong, -.app-add-main span { - display: block; -} - -.app-add-main strong { - color: #cbd5e1; -} - -.app-add-main span { - color: #7d8aa0; - font-size: 13px; -} - -.process-add-line { - display: grid; - grid-template-columns: minmax(0, 1fr) 72px 88px; -} - -.process-add-line button { - border-color: #166534; - background: #14532d; - font-weight: 700; -} - -.process-add-line .process-cancel-button { - border-color: #343b49; - background: #242a35; - color: #cbd5e1; - font-weight: 600; -} - -.process-add-line button:hover { - background: #166534; -} - -.process-add-line .process-cancel-button:hover { - background: #2d3543; -} - -.app-list { - display: grid; - gap: 6px; - margin-top: 1px; -} - -.app-row-main { - display: flex; - align-items: center; - gap: 10px; -} - -.app-row .item-icon, -.app-add-main .item-icon { - display: inline-flex; - align-items: center; - justify-content: center; - flex: 0 0 auto; - align-self: center; - width: 34px; - height: 34px; - border: 1px solid #2b3342; - border-radius: 4px; - background: #0d1016; - color: #dbeafe; - line-height: 0; -} - -.app-add-main .app-add-icon { - color: #64748b; -} - -.app-row .item-icon svg, -.app-add-main .item-icon svg { - width: 20px; - height: 20px; - display: block; - flex: 0 0 auto; -} - -.app-row, -.empty-state { - border: 1px solid #2b3342; - border-radius: 4px; - background: #131720; - padding: 10px 12px; -} - -.app-row button { - color: #fecaca; -} - -.empty-state { - color: #8d99ae; - min-height: 48px; -} - -.list-skeleton { - display: grid; - gap: 8px; -} - -.list-skeleton span { - min-height: 56px; - border: 1px solid #2b3342; - border-radius: 4px; - background: - linear-gradient(90deg, transparent, rgba(148, 163, 184, 0.12), transparent), - #131720; - background-size: 220% 100%; - animation: shimmer 1.15s linear infinite; -} - -.command-row { - display: grid; - grid-template-columns: minmax(0, 1fr) 132px; - gap: 8px; - margin-top: 14px; -} - -.command-row .ui-button, -.proxy-check-line .ui-button, -.subscription-line .ui-button { - width: 100%; -} - -.apps-config-actions .ui-button { - height: 48px; - min-height: 48px; -} - -.apply-button { - min-height: 46px; - width: 100%; - border: 1px solid #16a34a; - border-radius: 4px; - background: #22c55e; - color: #04130a; - font-weight: 800; - cursor: pointer; -} - -.apply-button:hover { - background: #4ade80; -} - -.open-config-button { - min-height: 46px; - width: 100%; -} - -.config-path { - margin: 10px 0 0; - font-size: 12px; - overflow-wrap: anywhere; -} - -.sr-only { - position: absolute; - width: 1px; - height: 1px; - padding: 0; - margin: -1px; - overflow: hidden; - clip: rect(0, 0, 0, 0); - white-space: nowrap; - border: 0; -} - -.log-dock { - position: fixed; - right: 0; - bottom: 0; - left: 0; - z-index: 40; - display: grid; - grid-template-columns: minmax(0, 1fr) auto; - gap: 8px; - align-items: center; - min-height: var(--app-footer-height); - border-top: 1px solid #2b3342; - background: #151923; - box-shadow: none; - margin: 0; - padding: 8px 14px; -} - -.log-dock.error { - border-top-color: rgba(239, 68, 68, 0.58); -} - -.log-dock.success { - border-top-color: rgba(34, 197, 94, 0.58); -} - -.log-dock.info { - border-top-color: rgba(59, 130, 246, 0.58); -} - -.log-current { - display: flex; - min-width: 0; - gap: 10px; - align-items: baseline; - padding: 0 8px; - transition: opacity 180ms ease, transform 180ms ease; -} - -.log-current.hidden { - color: #8d99ae; - opacity: 0.72; -} - -.log-current.visible { - opacity: 1; -} - -.log-current strong, -.log-current span { - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} - -.log-current strong { - flex: 0 1 auto; - min-width: 0; -} - -.log-current span { - min-width: 0; -} - -.log-muted { - color: #8d99ae; -} - -.log-toggle { - display: inline-flex; - gap: 8px; - align-items: center; - min-height: 32px; - border: 1px solid #343b49; - border-radius: 4px; - background: #242a35; - color: #eef2ff; - padding: 6px 10px; - cursor: pointer; -} - -.log-toggle:hover { - background: #2d3543; -} - -.log-toggle .log-count { - min-width: 22px; - border-radius: 4px; - background: #1b202b; - color: #dbeafe; - padding: 2px 6px; - text-align: center; - font-size: 12px; - font-weight: 700; -} - -.log-history { - position: absolute; - right: 0; - bottom: 100%; - left: 0; - display: grid; - grid-column: 1 / -1; - gap: 6px; - max-height: 230px; - overflow: auto; - border: 1px solid #2b3342; - border-bottom: 0; - background: #151923; - padding: 7px 14px; -} - -.log-history-row { - display: grid; - grid-template-columns: 76px minmax(0, 1fr); - gap: 10px; - align-items: start; - border: 1px solid #2b3342; - border-radius: 4px; - background: #0d1016; - padding: 8px; -} - -.log-history-row.error { - border-color: rgba(239, 68, 68, 0.42); -} - -.log-history-row.success { - border-color: rgba(34, 197, 94, 0.36); -} - -.log-history-row.info { - border-color: rgba(59, 130, 246, 0.36); -} - -.log-history-row time { - color: #8d99ae; - font-size: 12px; -} - -.log-history-row strong, -.log-history-row span { - display: block; - overflow-wrap: anywhere; -} - -.log-history-row span { - color: #b6c2d4; -} - -@keyframes spin { - to { - transform: rotate(360deg); - } -} - -@keyframes pulse { - 50% { - opacity: 0.45; - } -} - -@keyframes finder-border-top { - 0% { - left: -116px; - opacity: 0; - } - - 4%, - 23% { - opacity: 1; - } - - 25%, - 100% { - left: calc(100% + 8px); - opacity: 0; - } -} - -@keyframes finder-border-right { - 0%, - 25% { - top: -72px; - opacity: 0; - } - - 29%, - 48% { - opacity: 1; - } - - 50%, - 100% { - top: calc(100% + 8px); - opacity: 0; - } -} - -@keyframes finder-border-bottom { - 0%, - 50% { - right: -116px; - opacity: 0; - } - - 54%, - 73% { - opacity: 1; - } - - 75%, - 100% { - right: calc(100% + 8px); - opacity: 0; - } -} - -@keyframes finder-border-left { - 0%, - 75% { - bottom: -72px; - opacity: 0; - } - - 79%, - 98% { - opacity: 1; - } - - 100% { - bottom: calc(100% + 8px); - opacity: 0; - } -} - -@keyframes shimmer { - from { - background-position: 220% 0; - } - - to { - background-position: -220% 0; - } -} - -@keyframes panel-swipe-right { - from { - opacity: 0; - transform: translateX(34px); - } - - to { - opacity: 1; - transform: translateX(0); - } -} - -@keyframes panel-swipe-left { - from { - opacity: 0; - transform: translateX(-34px); - } - - to { - opacity: 1; - transform: translateX(0); - } -} - -@media (prefers-reduced-motion: reduce) { - .tab-panel-frame, - .tab-panel, - .finder-card .finder-border-glow, - .finder-border-glow-segment, - .status-light, - .summary-status-dot.checking, - .list-skeleton span, - .tile-loading { - animation: none; - transition: none; - } -} - -@media (max-width: 680px) { - .simple-shell { - padding: var(--app-header-height) 0 var(--app-footer-height); - } - - .simple-panel { - height: calc(100vh - var(--app-header-height) - var(--app-footer-height)); - padding: 12px 12px 16px; - } - - .app-row { - align-items: stretch; - flex-direction: column; - } - - .simple-header { - align-items: center; - flex-direction: row; - gap: 10px; - margin: 0; - padding: 7px 10px; - } - - .simple-header h1 { - font-size: 16px; - } - - .simple-header .ghost-button { - min-height: 30px; - padding: 5px 8px; - font-size: 12px; - } - - .panel-tabs { - gap: 1px; - padding: 0 10px; - } - - .ui-tabs { - gap: 1px; - padding: 0 10px; - } - - .panel-tabs button { - flex: 1 0 110px; - min-height: var(--app-tab-height); - padding: 8px 10px 9px; - font-size: 13px; - } - - .ui-tab { - flex: 1 0 110px; - min-height: var(--app-tab-height); - padding: 8px 10px 9px; - font-size: 13px; - } - - .panel-section-head { - align-items: stretch; - flex-direction: column; - } - - .panel-section-head > strong { - text-align: left; - } - - .summary-grid { - grid-template-columns: 1fr; - } - - .summary-readout-head { - align-items: stretch; - flex-direction: column; - } - - .summary-pill { - width: fit-content; - } - - .summary-lines div { - grid-template-columns: 1fr; - gap: 4px; - } - - .summary-actions { - display: grid; - grid-template-columns: 1fr; - } - - .setup-strip { - grid-template-columns: 1fr; - align-items: stretch; - } - - .apps-header { - grid-template-columns: 1fr; - min-height: 0; - } - - .apps-title { - min-height: 34px; - } - - .apps-config-actions .command-row { - grid-template-columns: 1fr; - } - - .app-add-skeleton { - grid-template-columns: 1fr; - } - - .proxy-check-line { - grid-template-columns: 1fr; - } - - .add-toolbar { - display: grid; - grid-template-columns: repeat(3, minmax(0, 1fr)); - } - - .add-tile { - width: 100%; - } - - .process-add-line { - grid-template-columns: 1fr; - } - - .subscription-line { - grid-template-columns: 38px minmax(0, 1fr) 42px; - } - - .subscription-line input { - grid-column: 2 / 4; - } - - .subscription-line button:not(.icon-command) { - grid-column: 1 / 3; - } - - .subscription-line .icon-command { - grid-column: 3; - width: 42px; - } - - .finder-card { - grid-template-columns: auto minmax(0, 1fr); - } - - .ui-service-row { - grid-template-columns: auto minmax(0, 1fr); - } - - .service-actions { - grid-column: 1 / -1; - } - - .ui-service-actions { - grid-column: 1 / -1; - display: grid; - grid-template-columns: minmax(0, 1fr) 42px; - width: 100%; - } - - .ui-service-actions > .ui-button { - width: 100%; - } - - .ui-service-actions > .ui-button:only-child { - grid-column: 1 / -1; - } - - .setup-strip-items { - flex-wrap: wrap; - overflow: visible; - } - - .setup-details { - grid-column: 1 / -1; - } - - .service-button { - flex: 1; - } - - .singbox-info-popover { - width: auto; - } - - .singbox-info-grid { - grid-template-columns: 70px minmax(0, 1fr); - } - - .server-list { - grid-template-columns: repeat(2, minmax(0, 1fr)); - } - - .command-row { - grid-template-columns: 1fr; - } - - .log-dock { - position: fixed; - right: 0; - bottom: 0; - left: 0; - grid-template-columns: minmax(0, 1fr) auto; - margin: 0; - padding: 7px 10px; - } - - .log-current { - align-items: baseline; - flex-direction: row; - gap: 8px; - padding: 0 4px; - } - - .log-current strong { - white-space: nowrap; - } - - .log-current span:not(.log-muted) { - display: none; - } - - .log-history { - max-height: 220px; - padding: 7px 10px; - } - - .log-history-row { - grid-template-columns: 1fr; - } -} diff --git a/apps/windows-client/src/ui/ActionMenu.tsx b/apps/windows-client/src/ui/ActionMenu.tsx deleted file mode 100644 index eab534a..0000000 --- a/apps/windows-client/src/ui/ActionMenu.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import { MoreHorizontal } from 'lucide-react'; -import { IconButton } from './IconButton'; - -export interface ActionMenuItem { - label: string; - onClick: () => void; - danger?: boolean; - disabled?: boolean; -} - -export interface ActionMenuProps { - open: boolean; - onOpenChange: (open: boolean) => void; - label: string; - items: ActionMenuItem[]; - disabled?: boolean; -} - -export function ActionMenu({ - open, - onOpenChange, - label, - items, - disabled, -}: ActionMenuProps) { - return ( -
- } - onClick={() => onOpenChange(!open)} - disabled={disabled} - aria-expanded={open} - /> - {open ? ( -
- {items.map((item) => ( - - ))} -
- ) : null} -
- ); -} - diff --git a/apps/windows-client/src/ui/Button.tsx b/apps/windows-client/src/ui/Button.tsx deleted file mode 100644 index 10c7311..0000000 --- a/apps/windows-client/src/ui/Button.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import type { ButtonHTMLAttributes, ReactNode } from 'react'; - -export type ButtonVariant = 'primary' | 'neutral' | 'add' | 'danger'; -export type ButtonSize = 'sm' | 'md' | 'lg'; - -export interface ButtonProps extends ButtonHTMLAttributes { - variant?: ButtonVariant; - size?: ButtonSize; - loading?: boolean; - loadingLabel?: string; - leftIcon?: ReactNode; - rightIcon?: ReactNode; -} - -export function Button({ - variant = 'neutral', - size = 'md', - loading = false, - loadingLabel, - leftIcon, - rightIcon, - className, - children, - disabled, - ...props -}: ButtonProps) { - const classes = [ - 'ui-button', - `ui-button--${variant}`, - `ui-button--${size}`, - loading ? 'is-loading' : '', - className ?? '', - ].filter(Boolean).join(' '); - - return ( - - ); -} - diff --git a/apps/windows-client/src/ui/Field.tsx b/apps/windows-client/src/ui/Field.tsx deleted file mode 100644 index e2b669a..0000000 --- a/apps/windows-client/src/ui/Field.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import type { InputHTMLAttributes, ReactNode } from 'react'; - -export interface FieldProps extends InputHTMLAttributes { - label: string; - error?: string | null; - hint?: string; - action?: ReactNode; -} - -export function Field({ - label, - error, - hint, - action, - className, - id, - ...props -}: FieldProps) { - const inputId = id ?? `field-${label.toLowerCase().replace(/\s+/g, '-')}`; - const helpId = `${inputId}-help`; - - return ( - - ); -} - diff --git a/apps/windows-client/src/ui/IconButton.tsx b/apps/windows-client/src/ui/IconButton.tsx deleted file mode 100644 index bd553b7..0000000 --- a/apps/windows-client/src/ui/IconButton.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import type { ButtonHTMLAttributes, ReactNode } from 'react'; - -export type IconButtonVariant = 'neutral' | 'add' | 'danger'; - -export interface IconButtonProps extends ButtonHTMLAttributes { - label: string; - icon: ReactNode; - variant?: IconButtonVariant; - loading?: boolean; -} - -export function IconButton({ - label, - icon, - variant = 'neutral', - loading = false, - className, - disabled, - title, - ...props -}: IconButtonProps) { - const classes = [ - 'ui-icon-button', - `ui-icon-button--${variant}`, - loading ? 'is-loading' : '', - className ?? '', - ].filter(Boolean).join(' '); - - return ( - - ); -} - diff --git a/apps/windows-client/src/ui/LogDock.tsx b/apps/windows-client/src/ui/LogDock.tsx deleted file mode 100644 index 176796b..0000000 --- a/apps/windows-client/src/ui/LogDock.tsx +++ /dev/null @@ -1,82 +0,0 @@ -import { Button } from './Button'; - -export interface LogDockEntry { - id: string; - kind: 'success' | 'error' | 'info'; - title: string; - text: string; - at: number; -} - -export interface LogDockProps { - entries: LogDockEntry[]; - activeEntry: LogDockEntry | null; - open: boolean; - onToggle: () => void; - formatTime: (timestamp: number) => string; -} - -function isNativePreviewError(entry: LogDockEntry | null) { - if (!entry) return false; - return entry.text.includes("reading 'invoke'") || entry.text.includes('undefined (reading'); -} - -function displayEntry(entry: LogDockEntry | null) { - if (!entry) return null; - if (!isNativePreviewError(entry)) return entry; - return { - ...entry, - title: 'Desktop-команды недоступны', - text: 'Запусти клиент через Tauri, чтобы управлять службами и применять конфиг.', - }; -} - -export function LogDock({ - entries, - activeEntry, - open, - onToggle, - formatTime, -}: LogDockProps) { - const current = displayEntry(activeEntry); - - return ( -
-
- {current ? ( - <> - {current.title} - {current.text} - - ) : ( - Журнал событий - )} -
- - {open ? ( -
- {entries.length ? entries.map((entry) => { - const friendly = displayEntry(entry); - return ( -
- -
- {friendly?.title ?? entry.title} - {friendly?.text ?? entry.text} - {isNativePreviewError(entry) ? Детали: {entry.text} : null} -
-
- ); - }) : ( -
- - Событий пока нет. -
- )} -
- ) : null} -
- ); -} diff --git a/apps/windows-client/src/ui/ServiceControlRow.tsx b/apps/windows-client/src/ui/ServiceControlRow.tsx deleted file mode 100644 index b2c3bc8..0000000 --- a/apps/windows-client/src/ui/ServiceControlRow.tsx +++ /dev/null @@ -1,93 +0,0 @@ -import type { ReactNode } from 'react'; -import { Button, type ButtonVariant } from './Button'; -import { ActionMenu, type ActionMenuItem } from './ActionMenu'; - -export type ServiceControlState = 'checking' | 'missing' | 'installed' | 'running' | 'stopped' | 'error'; - -export interface ServicePrimaryAction { - label: string; - onClick: () => void; - variant?: ButtonVariant; - loading?: boolean; - loadingLabel?: string; - disabled?: boolean; -} - -export interface ServiceControlRowProps { - state: ServiceControlState; - title: string; - detail: string; - primaryAction?: ServicePrimaryAction; - menu?: { - label: string; - open: boolean; - onOpenChange: (open: boolean) => void; - items: ActionMenuItem[]; - disabled?: boolean; - }; - visualState?: 'working' | 'settling' | null; - className?: string; - inlineActions?: ReactNode; - children?: ReactNode; -} - -export function ServiceControlRow({ - state, - title, - detail, - primaryAction, - menu, - visualState, - className, - inlineActions, - children, -}: ServiceControlRowProps) { - const classes = [ - 'ui-service-row', - `ui-service-row--${state}`, - visualState ? `ui-service-row--${visualState}` : '', - className ?? '', - ].filter(Boolean).join(' '); - - return ( -
-
- ); -} - diff --git a/apps/windows-client/src/ui/StatusPill.tsx b/apps/windows-client/src/ui/StatusPill.tsx deleted file mode 100644 index 3107f50..0000000 --- a/apps/windows-client/src/ui/StatusPill.tsx +++ /dev/null @@ -1,11 +0,0 @@ -export type StatusPillTone = 'ok' | 'warning' | 'error' | 'checking' | 'muted'; - -export interface StatusPillProps { - tone?: StatusPillTone; - children: string; -} - -export function StatusPill({ tone = 'muted', children }: StatusPillProps) { - return {children}; -} - diff --git a/apps/windows-client/src/ui/Tabs.tsx b/apps/windows-client/src/ui/Tabs.tsx deleted file mode 100644 index 1632bd5..0000000 --- a/apps/windows-client/src/ui/Tabs.tsx +++ /dev/null @@ -1,81 +0,0 @@ -import { useRef, type KeyboardEvent } from 'react'; - -export interface TabItem { - id: T; - label: string; -} - -export interface TabsProps { - items: Array>; - activeId: T; - onChange: (id: T) => void; - ariaLabel: string; -} - -export function Tabs({ - items, - activeId, - onChange, - ariaLabel, -}: TabsProps) { - const refs = useRef>([]); - - function moveFocus(currentId: T, direction: 1 | -1) { - const currentIndex = items.findIndex((item) => item.id === currentId); - const nextIndex = (currentIndex + direction + items.length) % items.length; - const next = items[nextIndex]; - if (!next) return; - onChange(next.id); - window.requestAnimationFrame(() => refs.current[nextIndex]?.focus()); - } - - function handleKeyDown(event: KeyboardEvent, id: T) { - if (event.key === 'ArrowRight') { - event.preventDefault(); - moveFocus(id, 1); - } else if (event.key === 'ArrowLeft') { - event.preventDefault(); - moveFocus(id, -1); - } else if (event.key === 'Home') { - event.preventDefault(); - const first = items[0]; - if (!first) return; - onChange(first.id); - window.requestAnimationFrame(() => refs.current[0]?.focus()); - } else if (event.key === 'End') { - event.preventDefault(); - const last = items[items.length - 1]; - if (!last) return; - onChange(last.id); - window.requestAnimationFrame(() => refs.current[items.length - 1]?.focus()); - } - } - - return ( -
- {items.map((item, index) => { - const active = item.id === activeId; - return ( - - ); - })} -
- ); -} - diff --git a/apps/windows-client/src/ui/index.ts b/apps/windows-client/src/ui/index.ts deleted file mode 100644 index 3c1edc2..0000000 --- a/apps/windows-client/src/ui/index.ts +++ /dev/null @@ -1,17 +0,0 @@ -export { ActionMenu } from './ActionMenu'; -export type { ActionMenuItem } from './ActionMenu'; -export { Button } from './Button'; -export type { ButtonProps, ButtonSize, ButtonVariant } from './Button'; -export { Field } from './Field'; -export type { FieldProps } from './Field'; -export { IconButton } from './IconButton'; -export type { IconButtonProps, IconButtonVariant } from './IconButton'; -export { LogDock } from './LogDock'; -export type { LogDockEntry, LogDockProps } from './LogDock'; -export { ServiceControlRow } from './ServiceControlRow'; -export type { ServiceControlRowProps, ServiceControlState } from './ServiceControlRow'; -export { StatusPill } from './StatusPill'; -export type { StatusPillProps, StatusPillTone } from './StatusPill'; -export { Tabs } from './Tabs'; -export type { TabItem, TabsProps } from './Tabs'; - diff --git a/apps/windows-client/tsconfig.json b/apps/windows-client/tsconfig.json deleted file mode 100644 index dde57bf..0000000 --- a/apps/windows-client/tsconfig.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2022", - "useDefineForClassFields": true, - "lib": ["DOM", "DOM.Iterable", "ES2022"], - "allowJs": false, - "skipLibCheck": true, - "esModuleInterop": true, - "allowSyntheticDefaultImports": true, - "strict": true, - "forceConsistentCasingInFileNames": true, - "module": "ESNext", - "moduleResolution": "Bundler", - "resolveJsonModule": true, - "isolatedModules": true, - "noEmit": true, - "jsx": "react-jsx" - }, - "include": ["src"], - "references": [] -} - diff --git a/apps/windows-client/vite.config.ts b/apps/windows-client/vite.config.ts deleted file mode 100644 index b1fcac5..0000000 --- a/apps/windows-client/vite.config.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { defineConfig } from 'vite'; -import react from '@vitejs/plugin-react'; - -const host = process.env.TAURI_DEV_HOST || '127.0.0.1'; - -export default defineConfig({ - plugins: [react()], - clearScreen: false, - server: { - host: host || false, - port: 5173, - strictPort: true, - watch: { - ignored: ['**/src-tauri/**'], - }, - }, -}); diff --git a/docs/audits/windows-client-ux-2026-07-08/01-summary-desktop.png b/docs/audits/windows-client-ux-2026-07-08/01-summary-desktop.png deleted file mode 100644 index 3d6b798..0000000 Binary files a/docs/audits/windows-client-ux-2026-07-08/01-summary-desktop.png and /dev/null differ diff --git a/docs/audits/windows-client-ux-2026-07-08/02-proxifyre-desktop.png b/docs/audits/windows-client-ux-2026-07-08/02-proxifyre-desktop.png deleted file mode 100644 index 160ccea..0000000 Binary files a/docs/audits/windows-client-ux-2026-07-08/02-proxifyre-desktop.png and /dev/null differ diff --git a/docs/audits/windows-client-ux-2026-07-08/03-proxy-desktop.png b/docs/audits/windows-client-ux-2026-07-08/03-proxy-desktop.png deleted file mode 100644 index 3ea37c3..0000000 Binary files a/docs/audits/windows-client-ux-2026-07-08/03-proxy-desktop.png and /dev/null differ diff --git a/docs/audits/windows-client-ux-2026-07-08/04-proxy-validation-desktop.png b/docs/audits/windows-client-ux-2026-07-08/04-proxy-validation-desktop.png deleted file mode 100644 index 497bb6d..0000000 Binary files a/docs/audits/windows-client-ux-2026-07-08/04-proxy-validation-desktop.png and /dev/null differ diff --git a/docs/audits/windows-client-ux-2026-07-08/05-summary-narrow.png b/docs/audits/windows-client-ux-2026-07-08/05-summary-narrow.png deleted file mode 100644 index 066b5e2..0000000 Binary files a/docs/audits/windows-client-ux-2026-07-08/05-summary-narrow.png and /dev/null differ diff --git a/docs/audits/windows-client-ux-2026-07-08/06-proxifyre-narrow.png b/docs/audits/windows-client-ux-2026-07-08/06-proxifyre-narrow.png deleted file mode 100644 index 6521916..0000000 Binary files a/docs/audits/windows-client-ux-2026-07-08/06-proxifyre-narrow.png and /dev/null differ diff --git a/docs/audits/windows-client-ux-2026-07-08/AUDIT.md b/docs/audits/windows-client-ux-2026-07-08/AUDIT.md deleted file mode 100644 index e5655ae..0000000 --- a/docs/audits/windows-client-ux-2026-07-08/AUDIT.md +++ /dev/null @@ -1,188 +0,0 @@ -# UX/UI-аудит Windows-клиента - -Дата: 2026-07-08 - -## Область аудита - -Проверен текущий React UI `apps/windows-client` для Vite/Tauri как компактная Windows-утилита управления proxy-маршрутизацией приложений. Аудит выполнялся в browser-preview на `http://127.0.0.1:5174/`, поэтому нативные Tauri-команды были недоступны, а интерфейс показывал preview/runtime-ошибки. Все выводы ниже привязаны к скриншотам, снятым в этом прогоне. - -## Цель пользователя - -Главная задача пользователя - быстро управлять службами и понять: - -- какие компоненты установлены; -- какие службы запущены, остановлены или отсутствуют; -- какое действие доступно прямо сейчас: установить, удалить, запустить, остановить, обновить, добавить или применить; -- что сейчас маршрутизируется; -- через какую цепочку компонентов идет трафик; -- готовы ли ProxiFyre и опциональный Local sing-box; -- можно ли безопасно применить изменения; -- что делать дальше, если что-то сломано. - -Цель по доступности: интерфейс должен быть управляемым с клавиатуры, с понятными статусами, читаемым восстановлением после ошибок, предсказуемыми focus states и устойчивой адаптацией к узким размерам Windows-окна. - -## Доказательства - -1. `01-summary-desktop.png` - панель "Сводка", desktop-ширина. Состояние: смешанное. Главный статус виден, но примененное состояние еще "загружается", а рядом уже показан бейдж "Совпадает". -2. `02-proxifyre-desktop.png` - панель ProxiFyre, desktop-ширина. Состояние: в целом рабочее. Состав компонента и список приложений компактны, но главное действие apply доступно даже при отсутствующих prerequisites. -3. `03-proxy-desktop.png` - панель "VPN / Прокси", desktop-ширина. Состояние: рабочее. Настройка маршрута понятна, но route path выглядит как вторичный текст, а apply-action выглядит валидным до того, как маршрут может успешно примениться. -4. `04-proxy-validation-desktop.png` - невалидный proxy input. Состояние: хорошая база. Inline-валидация находится рядом с полем и отключает действие проверки. -5. `05-summary-narrow.png` - панель "Сводка", узкая ширина. Состояние: напряженное. Контент перестраивается, но нижний log dock и длинные сообщения обрезают важную информацию. -6. `06-proxifyre-narrow.png` - панель ProxiFyre, узкая ширина. Состояние: напряженное. Контролы складываются неплохо, но setup chips переполняются по горизонтали, а нижний dock конкурирует с основной задачей. - -## Доменное направление - -Ключевые понятия домена: цепочка маршрута, здоровье компонентов, локальный runtime, внешний endpoint, выбранные приложения, сгенерированный конфиг, граница apply/restart, диагностика. - -Цветовой мир: темная Windows-оболочка, терминальный черный, зеленый для driver/service ready, amber для предупреждений, красный для блокеров/firewall, синий для links/actions, slate для config-файлов. - -Сигнатурный элемент, который стоит усилить: `Service Control Row` - повторяемая строка компонента, где слева состояние службы, в центре человекочитаемый статус, справа одно главное действие и меню дополнительных действий. Route-chain readout тоже нужен, но как объяснение эффекта этих служб: `Apps -> ProxiFyre -> target -> VPN/server`. - -Дефолты, которые стоит отбрасывать: - -- generic dashboard cards -> service-control utility; -- большая зеленая apply-кнопка всегда видна -> apply gated by readiness + inline blockers; -- raw logs как главный error surface -> сначала человеческое recovery-сообщение, raw details вторым уровнем. -- разрозненные кнопки и анимации -> shared component base с едиными variants/states/motion tokens. - -## Сильные стороны - -- Приложение уже ощущается как компактная desktop-утилита, а не как сайт. Фиксированный header, tabs, плотные панели и темная системная палитра подходят задаче. -- Продуктовая модель сильная: ProxiFyre и Local sing-box разделены, sing-box не сделан обязательным. -- Inline-валидация SOCKS5-поля расположена рядом с полем и отключает "Проверить", когда формат неверный. -- Зона добавления приложения использует иконки с доступными labels и tooltip, поэтому плотный workflow остается сканируемым. -- Для основных анимаций есть `prefers-reduced-motion`. -- UI использует настоящие `button` и tab roles, а не click-only divs. Это хорошая база для доступности. - -## UX-риски - -1. Apply выглядит доступным, когда маршрут еще не actionable. - - Доказательства: `02-proxifyre-desktop.png`, `03-proxy-desktop.png`. - - ProxiFyre отсутствует и список приложений пуст, но "Обновить конфиг" / "Обновить маршрут" ярко-зеленые. Это провоцирует failed action вместо направленного setup. - -2. Главная цепочка маршрута не является визуальным фокусом. - - Доказательства: `01-summary-desktop.png`, `03-proxy-desktop.png`. - - Самая важная mental model - путь трафика, но сейчас он показан plain text в таблице или вторичном блоке. Пользователь вынужден читать фрагменты статуса вместо того, чтобы сразу увидеть цепочку. - -3. В "Сводке" есть противоречивое состояние. - - Доказательство: `01-summary-desktop.png`. - - "Состояние загружается" показано рядом с "Совпадает", а система одновременно говорит "Не настроено". Это может создать ощущение, что действий не требуется. - -4. Preview/native command failures слишком сырые. - - Доказательства: все скриншоты. - - Dock показывает `Cannot read properties of undefined (reading 'invoke')`. Для разработчика это полезно, но как первичный пользовательский error surface это шум. - -5. Bottom log dock конфликтует с узкой компоновкой. - - Доказательства: `05-summary-narrow.png`, `06-proxifyre-narrow.png`. - - Важные сообщения обрезаются до "Компонен..." и "Cannot read prop..."; dock постоянно занимает вертикальное место в и так коротком окне. - -6. ProxiFyre setup chips плохо перестраиваются. - - Доказательство: `06-proxifyre-narrow.png`. - - Горизонтальная chip-лента скрывает третий dependency и ухудшает диагностическую ясность. - -7. Терминология немного смешана. - - Доказательства: `01-summary-desktop.png`, `03-proxy-desktop.png`. - - `config`, `TCP check`, `VPN сервер`, `Local sing-box`, `ProxiFyre` - все эти термины допустимы, но нужен единый принцип: сначала пользовательский русский, технический идентификатор вторым уровнем. - -8. Компонентная база пока не выражена как система. - - Доказательства: `02-proxifyre-desktop.png`, `03-proxy-desktop.png`, `06-proxifyre-narrow.png`. - - Кнопки "Обновить", "Установить", "Открыть", "Проверить", add-icon buttons и service actions выглядят близко, но пока не читаются как строгая система variants. Из-за этого сложно гарантировать одинаковые hover/loading/disabled/focus states и одинаковую motion-модель. - -## Риски доступности - -- Tablist использует `role="tab"`, но не видно поддержки ожидаемого arrow-key behavior и roving tab index. Пользователю с клавиатурой, вероятно, придется проходить все tabs через Tab вместо Left/Right. -- Focus visibility есть через browser defaults, но визуально она тяжелая и не согласована со стилем приложения. Нужен отдельный `:focus-visible` token для buttons, tabs, inputs и icon controls. -- Status dots сильно полагаются на цвет. Стоит дублировать состояние видимым текстом или `aria-label`/screen-reader text там, где текст рядом не объясняет статус. -- Footer использует `aria-live`, но повторяющиеся raw runtime errors могут быть шумными для assistive tech. Сначала стоит объявлять короткий статус, а подробный error text держать за "Посмотреть". -- На узкой ширине truncation может скрывать actionable-часть сообщений и button labels. -- Скриншоты не доказывают полный keyboard order, screen-reader output или contrast ratios. Для этого нужен интерактивный accessibility pass. - -## План улучшений - -### P0 - Сделать service-control flow заслуживающим доверия - -1. Ввести shared component base: - - `Button` с variants: `primary`, `neutral`, `add`, `danger`, `icon`; - - `Tabs` с единым keyboard behavior и animation contract; - - `ServiceControlRow` для ProxiFyre, Local sing-box и будущих служб; - - `StatusPill`, `Field`, `ActionMenu`, `LogDock`; - - единые states: default, hover, active, focus-visible, disabled, loading. - -2. Нормализовать motion tokens: - - button press: 100-140ms; - - tab switch: 180-240ms, только `opacity` + `transform`; - - popover/menu: 150-180ms; - - service operation: видимый loading state, чтобы интерфейс не ощущался зависшим; - - не использовать `transition: all`. - -3. Заменить raw preview/native invocation failures на дружелюбное состояние: - - "Desktop-команды недоступны в browser preview. Запусти через Tauri для управления службами." - - Raw error оставить только в деталях лога. - -4. Заблокировать primary apply actions до готовности: - - disabled + explanation, когда список приложений пуст; - - disabled + explanation, когда ProxiFyre отсутствует; - - disabled + explanation, когда выбран local route без установленного/запущенного sing-box или выбранного сервера; - - "Открыть конфиг" оставить вторичным действием, не равным apply по весу. - -5. Исправить язык summary-state: - - использовать взаимоисключающие состояния: "Проверяю", "Не готово", "Готово к применению", "Применено", "Есть черновик"; - - не показывать "Совпадает", пока saved/applied state реально не загружен. - -### P1 - Пересобрать главную mental model - -6. Вынести route-chain component как объясняющий элемент: - - узел `Apps` с количеством; - - узел `ProxiFyre` со статусом installed/running; - - узел `Target`: external SOCKS5 или Local sing-box; - - узел `Server`, когда активен local sing-box; - - каждый сломанный узел показывает одно следующее действие. - -7. Переработать "Сводку" в command center: - - сверху: route-chain и readiness системы; - - посередине: applied vs draft diff, только если есть отличие; - - снизу: максимум два next actions; - - длинные config paths и подробный состав компонентов убрать из первого взгляда. - -8. Разделить "настроить маршрут" и "применить маршрут": - - Proxy panel отвечает за endpoint choice и connection check; - - ProxiFyre panel отвечает за app selection и service readiness; - - Summary подтверждает объединенный маршрут и применяет только при готовности. - -### P2 - Усилить responsive и component craft - -7. Превратить setup chips в wrapping checklist на узких ширинах. - -8. Сделать log dock адаптивным: - - desktop: persistent compact dock подходит; - - narrow: collapsed toast/details sheet вместо постоянного full-width footer. - -9. Определить design tokens: - - surfaces: canvas, panel, inset, elevated; - - text: primary, secondary, muted, disabled; - - status: ready, warning, blocked, checking; - - focus ring и border intensities. - -10. Стандартизировать copy: - - последовательно использовать "конфиг" или "конфигурация"; - - заменить "TCP check" на "TCP-проверка"; - - `Local sing-box` оставить техническим именем компонента, но в user-facing headings связывать с "локальный прокси". - -### P3 - Verification pass - -11. Добавить focused interaction QA: - - keyboard tab order; - - arrow-key tab navigation; - - focus return из menus/popovers; - - disabled-action explanations; - - empty, loading, error, missing-component, ready и unapplied-change states. - -12. Добавить responsive visual snapshots: - - 1280x720 desktop; - - 800x600 compact desktop; - - 420x720 narrow window; - - длинные proxy/server names и длинные Windows paths. - -## Рекомендуемый первый redesign slice - -Сначала стоит сделать shared UI foundation: `Button`, `Tabs`, `ServiceControlRow`, `StatusPill`, `Field`, `ActionMenu`, `LogDock` и motion tokens. После этого собрать ProxiFyre и Local sing-box через один `ServiceControlRow`, а route-chain использовать как объясняющий компонент в "Сводке" и "VPN / Прокси". Такой порядок сначала стабилизирует поведение кнопок/табы/анимации, затем улучшит ориентацию и recovery без переписывания всего UI. diff --git a/docs/goals/windows-client-service-control-ui-system/EVIDENCE.md b/docs/goals/windows-client-service-control-ui-system/EVIDENCE.md deleted file mode 100644 index 7df1cc6..0000000 --- a/docs/goals/windows-client-service-control-ui-system/EVIDENCE.md +++ /dev/null @@ -1,82 +0,0 @@ -# Windows Client Service-Control UI System Evidence - -## Acceptance Evidence - -- Desktop Summary screenshot: `docs/goals/windows-client-service-control-ui-system/evidence/01-summary-desktop.png` - - Shows shared tabs, shared refresh button, and friendly preview error: `Desktop-команды недоступны`. -- Desktop ProxiFyre screenshot: `docs/goals/windows-client-service-control-ui-system/evidence/02-proxifyre-desktop.png` - - Shows ProxiFyre rendered through the shared service-control row pattern. - - Shows apply blocker: `ProxiFyre не установлен`. - - Shows `Обновить конфиг` disabled instead of green/ready. -- Desktop VPN / Proxy screenshot: `docs/goals/windows-client-service-control-ui-system/evidence/03-proxy-desktop.png` - - Shows route mode buttons through shared button variants. - - Shows apply blocker before route apply. -- Narrow ProxiFyre screenshot: `docs/goals/windows-client-service-control-ui-system/evidence/04-proxifyre-narrow.png` - - Shows service row and setup checklist reflow at 420px. - - Shows compact log dock title instead of long raw runtime error text. - -Captured DOM state from Browser preview: - -```json -{ - "proxState": { - "disabledApply": true, - "blockerText": true, - "friendlyError": true, - "rawInvokeCurrent": false - }, - "routeState": { - "externalButtonClass": "ui-button ui-button--primary ui-button--md", - "externalAriaPressed": "true" - }, - "narrowState": { - "width": 420, - "hasHorizontalOverflow": false, - "setupStripOverflow": false - } -} -``` - -Native/elevated install/start/stop/uninstall service actions were not executed in this evidence pass. They are implemented through the existing handlers and shared callbacks, but real elevated service execution remains `implemented but unproven`. - -## Verification - -Build was run with bundled Node because system `npm` is not in PATH in this Codex shell. - -```powershell -& 'C:\Users\PC\.cache\codex-runtimes\codex-primary-runtime\dependencies\node\bin\node.exe' '.\node_modules\typescript\bin\tsc' -& 'C:\Users\PC\.cache\codex-runtimes\codex-primary-runtime\dependencies\node\bin\node.exe' '.\node_modules\vite\bin\vite.js' build -``` - -Relevant output: - -```text -vite v7.3.6 building client environment for production... -✓ 1800 modules transformed. -✓ built in 1.14s -``` - -Cutover check: - -```powershell -rg -n 'transition:\s*all|className=.*(apply-button|service-button|ghost-button|add-tile|open-config-button)' apps\windows-client\src -``` - -Relevant output: - -```text -no legacy className or transition: all matches -``` - -Notes: -- `pnpm run build` was attempted through the bundled runtime, but it began reinstalling `node_modules` because dependencies were originally installed by another package manager. The process was stopped; no package or lockfile changes were recorded by git. -- Browser preview was served from `http://127.0.0.1:5174/`. - -## Review Notes - -- Visual self-review found that active route mode lost its active styling after switching to shared `Button`; CSS was corrected with `.route-switch .ui-button--primary`. -- Visual self-review found setup chips still horizontally scrolled at 420px; narrow CSS was corrected to wrap `.setup-strip-items`. -- Residual product issue from the earlier audit remains: Summary still has `Состояние загружается` beside `Совпадает`. That state language was outside the implemented high-value slice and should be handled in a follow-up Summary view-model cleanup. -- POST plan review: aligned. Implementation stayed inside the approved ownership and contract boundaries: shared UI components are presentational, `App.tsx` still owns orchestration, and Tauri wrappers remain the command boundary. -- Correctness review: no blocker found. Real elevated service actions were not executed, so that lane remains `implemented but unproven`. -- Maintainability review: no blocker found. Old dominant `className` paths for apply/service/ghost/add/open buttons were displaced from `App.tsx`; stale legacy CSS selectors remain in `app.css` as non-dominant compatibility residue and should be removed in a follow-up CSS pruning pass if desired. diff --git a/docs/goals/windows-client-service-control-ui-system/GOAL.md b/docs/goals/windows-client-service-control-ui-system/GOAL.md deleted file mode 100644 index 43a3fa0..0000000 --- a/docs/goals/windows-client-service-control-ui-system/GOAL.md +++ /dev/null @@ -1,11 +0,0 @@ -# Goal: Windows Client Service-Control UI System - -Use Krypton Execution to execute `docs/goals/windows-client-service-control-ui-system/PLAN.md`. - -Core rules: -- Treat PLAN.md as the source plan. -- Preserve intent, ownership, contract, cutover, evidence, and kill criteria. -- Do not add a new dominant path without deleting, redirecting, demoting, or shimming the displaced path. -- Capture acceptance evidence from the target perspective and record it in EVIDENCE.md. -- Say "implemented but unproven" if that evidence cannot be captured. - diff --git a/docs/goals/windows-client-service-control-ui-system/PLAN.md b/docs/goals/windows-client-service-control-ui-system/PLAN.md deleted file mode 100644 index 1c40c98..0000000 --- a/docs/goals/windows-client-service-control-ui-system/PLAN.md +++ /dev/null @@ -1,503 +0,0 @@ -# Windows Client Service-Control UI System Implementation Plan - -**Intent:** Превратить текущий Windows-клиент в простую service-control утилиту с единой компонентной базой, предсказуемыми состояниями кнопок/табов/служебных строк, readiness-блокерами и аккуратными motion tokens. -**Current Behavior:** `apps/windows-client/src/app/App.tsx` и `apps/windows-client/src/styles/app.css` остаются крупным монолитом. Кнопки, табы, service cards, add controls, apply/open actions, log dock, validation и анимации задаются ad hoc через локальные JSX-блоки и классы вроде `service-button`, `apply-button`, `ghost-button`, `add-tile`, `route-switch`, `finder-card`, `log-dock`. -**Expected Outcome:** Пользователь видит плотную Windows-утилиту, где ProxiFyre и Local sing-box управляются через один повторяемый service-control паттерн, все кнопки имеют понятные variants/states, apply недоступен до готовности prerequisites, ошибки preview/native команд показываются человечески, а вкладки/меню/loading состояния двигаются одинаково и не создают ощущения зависшего UI. -**Target-Perspective Output:** Пользователь открывает клиент и за несколько секунд понимает, какие компоненты установлены, какие службы запущены/остановлены/отсутствуют, какую одну кнопку можно нажать дальше, почему apply заблокирован, и где посмотреть детали. На desktop и узкой ширине интерфейс не обрезает ключевые действия и не показывает raw runtime errors как основной текст. -**Truth Owner:** Источник правды для профилей, targets, компонентов, Local sing-box, generated config и apply остается в Rust/Tauri командах и JSON-файлах под `C:\ProgramData\VpnProxy`. Источник правды для UI-паттернов и визуальных контрактов после этого плана: `.interface-design/system.md` и shared UI components под `apps/windows-client/src/ui`. -**Contract Boundary:** React UI вызывает typed wrappers из `apps/windows-client/src/api/tauriCommands.ts`. Shared UI components не вызывают Tauri напрямую и не владеют persistent state; они получают props, emit callbacks и отображают states. `App.tsx`/panel containers владеют orchestration, draft state, loading state, readiness blockers и вызовами существующих handlers. -**Cutover:** Старые ad hoc JSX/CSS paths для кнопок, табов, service cards, log dock, fields и menus постепенно заменяются shared components. После cutover в `App.tsx` не должно оставаться нового прямого ` - ))} - - ); -} - -function ProfileDetails({ profile, targets, onChange }) { - const [newItem, setNewItem] = useState(''); - const [newType, setNewType] = useState('process'); - if (!profile) { - return
Select or add a profile.
; - } - function patch(patchValue) { - onChange({ ...profile, ...patchValue }); - } - function addItem() { - const value = newItem.trim(); - if (!value) return; - patch({ - items: [ - ...profile.items, - { type: newType, value, recursive: newType === 'folder' }, - ], - }); - setNewItem(''); - } - return ( -
- - -
- - setNewItem(event.target.value)} - onKeyDown={(event) => event.key === 'Enter' && addItem()} - /> - -
-
- {profile.items.map((item, index) => ( -
- {item.value} - {item.type} - -
- ))} -
-
- ); -} - -export function WindowsOverviewPage({ pushToast }) { - const [status, setStatus] = useState(null); - const [profiles, setProfiles] = useState([]); - const [targets, setTargets] = useState([]); - const [selectedId, setSelectedId] = useState(''); - const [busy, setBusy] = useState(false); - - async function load() { - const data = await api.windows.status(); - setStatus(data); - const nextProfiles = data.profiles || []; - setProfiles(nextProfiles); - setTargets(data.targets || []); - setSelectedId((current) => current || nextProfiles[0]?.id || ''); - } - - useEffect(() => { - load().catch((error) => pushToast?.({ kind: 'danger', title: 'Windows status failed', message: error.message })); - const timer = setInterval(() => load().catch(() => {}), 5000); - return () => clearInterval(timer); - }, []); - - const selected = useMemo( - () => profiles.find((profile) => profile.id === selectedId) || null, - [profiles, selectedId], - ); - - function replaceProfile(nextProfile) { - setProfiles((prev) => prev.map((profile) => profile.id === nextProfile.id ? nextProfile : profile)); - } - - async function saveProfiles(nextProfiles = profiles) { - setBusy(true); - try { - const data = await api.windows.profiles.save(nextProfiles); - setProfiles(data.summaries || data.profiles || []); - pushToast?.({ kind: 'success', title: 'Profiles saved' }); - } finally { - setBusy(false); - } - } - - async function applyProfiles() { - setBusy(true); - try { - await api.windows.profiles.save(profiles); - await api.windows.profiles.apply(); - await load(); - pushToast?.({ kind: 'success', title: 'ProxiFyre updated' }); - } finally { - setBusy(false); - } - } - - function addProfile() { - const profile = emptyProfile(); - setProfiles((prev) => [...prev, profile]); - setSelectedId(profile.id); - } - - return ( -
-
-
- -
-

{routeTitle(status)}

-

Profiles send selected apps through ProxiFyre to local sing-box or an existing proxy target.

-
-
-
- Selected apps->ProxiFyre->Proxy target -
-
- -
-
-
-
-

Profiles

- {profiles.filter((profile) => profile.enabled).length} enabled -
- -
- -
- -
-
-
-

{selected?.name || 'Profile'}

- {selected ? targetLabel(targets.find((target) => target.id === selected.proxyTargetId)) : 'No selection'} -
- -
- -
-
- -
-
-

Recent activity

- -
- {(status?.activity || []).slice(0, 5).map((entry) => ( -
- {entry.type} - {entry.message} - {entry.ts} -
- ))} -
-
- ); -} -``` - -- [ ] **Step 3: Wire Windows page in `App.jsx`** - -Add import: - -```jsx -import { WindowsOverviewPage } from './components/WindowsOverviewPage.jsx'; -``` - -Add this after `const isClientMode = state?.mode === 'client';` if present, or near equivalent mode checks: - -```jsx - const isWindowsMode = state?.mode === 'windows'; -``` - -Update the client-mode redirect effect to include Windows: - -```jsx - useEffect(() => { - if ((state?.mode === 'client' || state?.mode === 'windows') && page !== 'overview') { - navigate('overview'); - } - }, [state?.mode, page]); -``` - -In the page rendering section, render Windows overview before generic overview: - -```jsx - {page === 'overview' && isWindowsMode && ( - - )} - {page === 'overview' && isClientMode && !isWindowsMode && ( - - )} -``` - -Keep existing gateway overview rendering under `!isClientMode && !isWindowsMode`. - -- [ ] **Step 4: Update sidebar/topbar labels** - -In `src/web/components/Sidebar.jsx`, make Windows nav compact: - -```jsx -const WINDOWS_ITEMS = [ - { id: 'overview', label: 'Overview', ico: 'O' }, - { id: 'logs', label: 'Logs', ico: 'L' }, - { id: 'settings', label: 'Settings', ico: 'S' }, -]; -``` - -Then select items: - -```jsx - const items = mode === 'windows' - ? WINDOWS_ITEMS - : mode === 'client' - ? CLIENT_ITEMS - : ITEMS; -``` - -In `src/web/components/Topbar.jsx`, display `VPN Proxy Windows` when `state?.mode === 'windows'`. - -- [ ] **Step 5: Add CSS** - -Append to `src/web/styles.css`: - -```css -.windows-page { - display: grid; - gap: 16px; -} - -.windows-status-panel { - display: grid; - grid-template-columns: minmax(0, 1fr) auto; - gap: 16px; - align-items: center; - padding: 16px; - border: 1px solid var(--border); - border-radius: 8px; - background: var(--panel); -} - -.windows-status-main { - display: flex; - gap: 12px; - align-items: flex-start; -} - -.windows-status-dot { - width: 12px; - height: 12px; - border-radius: 999px; - background: var(--success); - margin-top: 8px; - box-shadow: 0 0 0 5px rgba(34, 197, 94, 0.12); -} - -.windows-status-panel h1 { - margin: 0 0 6px; - font-size: 24px; -} - -.windows-status-panel p { - margin: 0; - color: var(--muted); -} - -.windows-route-line { - display: flex; - flex-wrap: wrap; - gap: 8px; - align-items: center; - color: var(--muted); - font-size: 13px; -} - -.windows-route-line span { - border: 1px solid var(--border); - border-radius: 999px; - padding: 6px 10px; - background: var(--bg-soft); -} - -.windows-workspace { - display: grid; - grid-template-columns: minmax(0, 1fr) minmax(320px, 420px); - gap: 16px; -} - -.win-profile-list { - display: grid; - gap: 8px; - padding: 10px; -} - -.win-profile-row { - display: grid; - grid-template-columns: 26px minmax(0, 1fr) auto; - gap: 10px; - align-items: center; - width: 100%; - text-align: left; - border: 1px solid transparent; - border-radius: 8px; - padding: 10px; - background: transparent; - color: var(--text); -} - -.win-profile-row.active { - border-color: var(--border-strong); - background: var(--bg-soft); -} - -.win-profile-row strong, -.win-profile-row small { - display: block; -} - -.win-profile-row small, -.win-profile-row em { - color: var(--muted); - font-size: 12px; - font-style: normal; -} - -.win-profile-check { - width: 22px; - height: 22px; - border-radius: 999px; - display: grid; - place-items: center; - background: var(--border); - color: var(--muted); - font-size: 12px; -} - -.win-profile-check.on { - background: var(--accent); - color: #fff; -} - -.win-detail { - display: grid; - gap: 12px; - padding: 14px; -} - -.win-detail label { - display: grid; - gap: 6px; - color: var(--muted); - font-size: 12px; -} - -.win-add-item { - display: grid; - grid-template-columns: 110px minmax(0, 1fr) auto; - gap: 8px; -} - -.win-items { - display: grid; - gap: 8px; -} - -.win-item { - display: grid; - grid-template-columns: minmax(0, 1fr) auto auto; - gap: 8px; - align-items: center; - border: 1px solid var(--border); - border-radius: 7px; - padding: 8px; -} - -.win-item small { - color: var(--muted); -} - -.windows-activity-row { - display: grid; - grid-template-columns: 130px minmax(0, 1fr) auto; - gap: 10px; - padding: 10px 14px; - border-top: 1px solid var(--border); - color: var(--muted); - font-size: 13px; -} - -.windows-activity-row strong { - color: var(--info); -} - -@media (max-width: 960px) { - .windows-status-panel, - .windows-workspace, - .windows-activity-row { - grid-template-columns: 1fr; - } - - .win-add-item { - grid-template-columns: 1fr; - } -} -``` - -- [ ] **Step 6: Build** - -Run: - -```bash -npm run build -``` - -Expected: PASS. - -- [ ] **Step 7: Commit** - -```bash -git add src/web/api.js src/web/App.jsx src/web/components/WindowsOverviewPage.jsx src/web/components/Sidebar.jsx src/web/components/Topbar.jsx src/web/styles.css -git commit -m "feat: add windows client UI" -``` - ---- - -### Task 6: PowerShell Helper Scripts - -**Files:** -- Create: `scripts/windows/VpnProxy.Windows.psm1` -- Create: `scripts/windows/helper.ps1` -- Create: `scripts/windows/manage.ps1` - -- [ ] **Step 1: Create helper module** - -Create `scripts/windows/VpnProxy.Windows.psm1`: - -```powershell -Set-StrictMode -Version Latest -$ErrorActionPreference = "Stop" - -$script:InstallRoot = $env:VPN_PROXY_WINDOWS_ROOT -if ([string]::IsNullOrWhiteSpace($script:InstallRoot)) { - $script:InstallRoot = "C:\Tools\vpn-proxy-windows" -} -$script:ProxiFyreRoot = $env:PROXIFYRE_ROOT -if ([string]::IsNullOrWhiteSpace($script:ProxiFyreRoot)) { - $script:ProxiFyreRoot = "C:\Tools\ProxiFyre" -} - -function New-VpnProxyResult { - param( - [string]$Action, - [bool]$Success, - [object]$Result = $null, - [string]$Message = "", - [string]$ErrorMessage = "" - ) - $value = [ordered]@{ - success = $Success - action = $Action - } - if ($null -ne $Result) { $value.result = $Result } - if ($Message) { $value.message = $Message } - if ($ErrorMessage) { $value.error = $ErrorMessage } - return $value -} - -function Get-VpnProxyStatus { - $task = Get-ScheduledTask -TaskName "SingBoxProxy" -ErrorAction SilentlyContinue - $singboxProcess = Get-Process -Name "sing-box" -ErrorAction SilentlyContinue - $proxifyre = Get-Service -Name "ProxiFyreService" -ErrorAction SilentlyContinue - return [ordered]@{ - singbox = if ($singboxProcess) { "Running" } elseif ($task) { [string]$task.State } else { "NotInstalled" } - proxifyre = if ($proxifyre) { [string]$proxifyre.Status } else { "NotInstalled" } - installRoot = $script:InstallRoot - proxifyreRoot = $script:ProxiFyreRoot - } -} - -function Write-ProxiFyreConfig { - param( - [Parameter(Mandatory=$true)][string]$ConfigPath, - [Parameter(Mandatory=$true)][object]$Config - ) - $dir = Split-Path -Parent $ConfigPath - New-Item -ItemType Directory -Force -Path $dir | Out-Null - if (Test-Path $ConfigPath) { - $backup = "$ConfigPath.bak" - Copy-Item $ConfigPath $backup -Force - } - $Config | ConvertTo-Json -Depth 20 | Set-Content -Path $ConfigPath -Encoding UTF8 -} - -function Restart-ProxiFyre { - $exe = Join-Path $script:ProxiFyreRoot "ProxiFyre.exe" - if (-not (Test-Path $exe)) { - throw "ProxiFyre.exe not found at $exe" - } - & $exe stop 2>$null | Out-Null - & $exe install 2>$null | Out-Null - & $exe start 2>$null | Out-Null -} - -function Invoke-ProxiFyreApply { - param([object]$Payload) - Write-ProxiFyreConfig -ConfigPath $Payload.configPath -Config $Payload.config - Restart-ProxiFyre - return New-VpnProxyResult -Action "proxifyre.apply" -Success $true -Message "ProxiFyre config applied and service restarted" -} - -function Invoke-ServiceControl { - param([object]$Payload) - $service = [string]$Payload.service - $action = [string]$Payload.action - if ($service -eq "proxifyre") { - if ($action -eq "restart") { Restart-ProxiFyre } - elseif ($action -eq "start") { Start-Service -Name "ProxiFyreService" } - elseif ($action -eq "stop") { Stop-Service -Name "ProxiFyreService" -Force } - } elseif ($service -eq "sing-box") { - if ($action -eq "restart") { - Stop-ScheduledTask -TaskName "SingBoxProxy" -ErrorAction SilentlyContinue - Start-ScheduledTask -TaskName "SingBoxProxy" - } elseif ($action -eq "start") { - Start-ScheduledTask -TaskName "SingBoxProxy" - } elseif ($action -eq "stop") { - Stop-ScheduledTask -TaskName "SingBoxProxy" - } - } elseif ($service -eq "ui") { - return New-VpnProxyResult -Action "service.control" -Success $true -Message "UI is controlled by manage.ps1 -OpenUi" - } else { - throw "Unknown service: $service" - } - return New-VpnProxyResult -Action "service.control" -Success $true -Message "$service $action complete" -} - -function Get-VpnProxyLogs { - $paths = @( - (Join-Path $script:InstallRoot "runtime\sing-box\singbox.log"), - (Join-Path $script:ProxiFyreRoot "ProxiFyre.log") - ) - $logs = @() - foreach ($path in $paths) { - if (Test-Path $path) { - $logs += [ordered]@{ - path = $path - lines = @(Get-Content $path -Tail 120 -ErrorAction SilentlyContinue) - } - } - } - return $logs -} - -function Invoke-VpnProxyAction { - param( - [Parameter(Mandatory=$true)][string]$Action, - [object]$Payload = @{} - ) - switch ($Action) { - "status.get" { - return New-VpnProxyResult -Action $Action -Success $true -Result (Get-VpnProxyStatus) - } - "proxifyre.apply" { - return Invoke-ProxiFyreApply -Payload $Payload - } - "service.control" { - return Invoke-ServiceControl -Payload $Payload - } - "logs.get" { - return New-VpnProxyResult -Action $Action -Success $true -Result (Get-VpnProxyLogs) - } - default { - throw "Unknown action: $Action" - } - } -} - -Export-ModuleMember -Function Invoke-VpnProxyAction, Get-VpnProxyStatus -``` - -- [ ] **Step 2: Create JSON wrapper** - -Create `scripts/windows/helper.ps1`: - -```powershell -Set-StrictMode -Version Latest -$ErrorActionPreference = "Stop" - -$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path -Import-Module (Join-Path $ScriptDir "VpnProxy.Windows.psm1") -Force - -try { - $raw = [Console]::In.ReadToEnd() - if ([string]::IsNullOrWhiteSpace($raw)) { - throw "Missing JSON input" - } - $request = $raw | ConvertFrom-Json - $payload = if ($request.PSObject.Properties.Name -contains "payload") { $request.payload } else { @{} } - $result = Invoke-VpnProxyAction -Action ([string]$request.action) -Payload $payload - $result | ConvertTo-Json -Depth 30 -Compress - exit 0 -} catch { - $errorResult = [ordered]@{ - success = $false - action = "error" - error = $_.Exception.Message - } - $errorResult | ConvertTo-Json -Depth 10 -Compress - exit 1 -} -``` - -- [ ] **Step 3: Create management script** - -Create `scripts/windows/manage.ps1`: - -```powershell -param( - [switch]$OpenUi, - [switch]$Status, - [switch]$RestartServices -) - -Set-StrictMode -Version Latest -$ErrorActionPreference = "Stop" - -$Root = Split-Path -Parent (Split-Path -Parent $MyInvocation.MyCommand.Path) -$env:APP_MODE = "windows" -$env:DATA_DIR = Join-Path $Root "data" -$env:DIST_DIR = Join-Path $Root "app\dist" -$env:PROXY_PORT = "1080" -$env:PROXY_BIND_IP = "127.0.0.1" -$env:SING_BOX_CONFIG = Join-Path $Root "runtime\sing-box\config.json" -$env:SING_BOX_CACHE = Join-Path $Root "runtime\sing-box\cache.db" -$env:WINDOWS_HELPER = Join-Path $Root "app\scripts\windows\helper.ps1" - -function Get-NodeCommand { - $portable = Join-Path $Root "runtime\node\node.exe" - if (Test-Path $portable) { return $portable } - return "node" -} - -if ($Status) { - $inputJson = @{ action = "status.get"; payload = @{} } | ConvertTo-Json -Compress - $inputJson | & (Join-Path $Root "app\scripts\windows\helper.ps1") - exit $LASTEXITCODE -} - -if ($RestartServices) { - $helper = Join-Path $Root "app\scripts\windows\helper.ps1" - (@{ action = "service.control"; payload = @{ service = "proxifyre"; action = "restart" } } | ConvertTo-Json -Compress) | & $helper - (@{ action = "service.control"; payload = @{ service = "sing-box"; action = "restart" } } | ConvertTo-Json -Compress) | & $helper - exit 0 -} - -if ($OpenUi) { - $node = Get-NodeCommand - Start-Process "http://127.0.0.1:3456" - & $node (Join-Path $Root "app\src\server\index.js") - exit $LASTEXITCODE -} - -Write-Host "VPN Proxy Windows" -Write-Host " -OpenUi Start local UI" -Write-Host " -Status Print JSON status" -Write-Host " -RestartServices Restart ProxiFyre and sing-box" -``` - -- [ ] **Step 4: Syntax-check PowerShell scripts when `pwsh` exists** - -Run: - -```bash -pwsh -NoProfile -Command "\$errors = \$null; [System.Management.Automation.Language.Parser]::ParseFile('scripts/windows/helper.ps1', [ref]\$null, [ref]\$errors) > \$null; if (\$errors) { \$errors; exit 1 }" -pwsh -NoProfile -Command "\$errors = \$null; [System.Management.Automation.Language.Parser]::ParseFile('scripts/windows/manage.ps1', [ref]\$null, [ref]\$errors) > \$null; if (\$errors) { \$errors; exit 1 }" -``` - -Expected: exit code 0. If `pwsh` is unavailable on macOS, record that syntax check was skipped and run `npm test` before commit. - -- [ ] **Step 5: Commit** - -```bash -git add scripts/windows/VpnProxy.Windows.psm1 scripts/windows/helper.ps1 scripts/windows/manage.ps1 -git commit -m "feat: add windows helper scripts" -``` - ---- - -### Task 7: Curl-Friendly Windows Installer - -**Files:** -- Create: `scripts/install-windows-client.ps1` -- Modify: `README.md` - -- [ ] **Step 1: Create installer script** - -Create `scripts/install-windows-client.ps1`: - -```powershell -Set-StrictMode -Version Latest -$ErrorActionPreference = "Stop" -[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 - -$InstallRoot = $env:VPN_PROXY_WINDOWS_ROOT -if ([string]::IsNullOrWhiteSpace($InstallRoot)) { $InstallRoot = "C:\Tools\vpn-proxy-windows" } -$AppDir = Join-Path $InstallRoot "app" -$DataDir = Join-Path $InstallRoot "data" -$RuntimeDir = Join-Path $InstallRoot "runtime" -$SingBoxDir = Join-Path $RuntimeDir "sing-box" -$RepoZipUrl = "https://git.dokops.ru/dokril/vpn-proxy/archive/master.zip" -$SingBoxVersion = "1.12.13" -$SingBoxUrl = "https://github.com/SagerNet/sing-box/releases/download/v$SingBoxVersion/sing-box-$SingBoxVersion-windows-amd64.zip" - -function Assert-Admin { - $principal = New-Object Security.Principal.WindowsPrincipal([Security.Principal.WindowsIdentity]::GetCurrent()) - if (-not $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) { - throw "Run PowerShell 7 as Administrator" - } -} - -function Assert-PowerShell7 { - if ($PSVersionTable.PSVersion.Major -lt 7) { - throw "PowerShell 7 is required" - } -} - -function Download-File { - param([string]$Url, [string]$Destination) - Invoke-WebRequest -Uri $Url -OutFile $Destination -UseBasicParsing - Unblock-File -Path $Destination -ErrorAction SilentlyContinue -} - -function Install-AppFiles { - New-Item -ItemType Directory -Force -Path $InstallRoot, $DataDir, $RuntimeDir | Out-Null - $zip = Join-Path $env:TEMP "vpn-proxy-windows.zip" - $extract = Join-Path $env:TEMP "vpn-proxy-windows-extract" - Remove-Item $zip -Force -ErrorAction SilentlyContinue - Remove-Item $extract -Recurse -Force -ErrorAction SilentlyContinue - Download-File -Url $RepoZipUrl -Destination $zip - Expand-Archive -Path $zip -DestinationPath $extract -Force - $source = Get-ChildItem $extract -Directory | Where-Object { $_.Name -match "vpn-proxy-(master|main)" } | Select-Object -First 1 - if (-not $source) { throw "Downloaded archive layout is not recognized" } - if (Test-Path $AppDir) { - $backup = "$AppDir.backup" - Remove-Item $backup -Recurse -Force -ErrorAction SilentlyContinue - Move-Item $AppDir $backup - } - Move-Item $source.FullName $AppDir - Remove-Item $zip -Force -ErrorAction SilentlyContinue - Remove-Item $extract -Recurse -Force -ErrorAction SilentlyContinue -} - -function Install-SingBox { - New-Item -ItemType Directory -Force -Path $SingBoxDir | Out-Null - if (Test-Path (Join-Path $SingBoxDir "sing-box.exe")) { return } - $zip = Join-Path $env:TEMP "sing-box-windows.zip" - $extract = Join-Path $env:TEMP "sing-box-windows-extract" - Remove-Item $zip -Force -ErrorAction SilentlyContinue - Remove-Item $extract -Recurse -Force -ErrorAction SilentlyContinue - Download-File -Url $SingBoxUrl -Destination $zip - Expand-Archive -Path $zip -DestinationPath $extract -Force - $exe = Get-ChildItem $extract -Recurse -Filter "sing-box.exe" | Select-Object -First 1 - if (-not $exe) { throw "sing-box.exe was not found in archive" } - Copy-Item $exe.FullName (Join-Path $SingBoxDir "sing-box.exe") -Force -} - -function Select-InstallMode { - Write-Host "" - Write-Host "Choose install mode:" - Write-Host " [1] Full install: local sing-box + ProxiFyre" - Write-Host " [2] ProxiFyre only: use existing proxy target" - $choice = Read-Host "Mode [1]" - if ($choice -eq "2") { return "proxifyre-only" } - return "full" -} - -function Write-InitialTargets { - param([string]$Mode) - $targetsPath = Join-Path $DataDir "proxy-targets.json" - if (Test-Path $targetsPath) { return } - if ($Mode -eq "proxifyre-only") { - $target = Read-Host "Existing SOCKS5 proxy target host:port" - if ($target -notmatch "^([^:]+):(\d+)$") { throw "Expected host:port" } - @(@{ id = "existing-proxy"; name = "Existing proxy"; protocol = "socks5"; host = $matches[1]; port = [int]$matches[2] }) | - ConvertTo-Json -Depth 5 | - Set-Content $targetsPath -Encoding UTF8 - } -} - -function Install-NodeDependencies { - Push-Location $AppDir - try { - npm install - npm run build - } finally { - Pop-Location - } -} - -function Start-Ui { - $manage = Join-Path $AppDir "scripts\windows\manage.ps1" - Start-Process pwsh -ArgumentList "-NoProfile", "-ExecutionPolicy", "Bypass", "-File", "`"$manage`"", "-OpenUi" -} - -Assert-Admin -Assert-PowerShell7 -$mode = Select-InstallMode -Install-AppFiles -if ($mode -eq "full") { Install-SingBox } -Write-InitialTargets -Mode $mode -Install-NodeDependencies -Set-Content -Path (Join-Path $DataDir "windows-state.json") -Encoding UTF8 -Value (@{ installMode = $mode } | ConvertTo-Json) -Start-Ui - -Write-Host "" -Write-Host "VPN Proxy Windows is installed." -Write-Host "UI: http://127.0.0.1:3456" -Write-Host "Recovery:" -Write-Host "& `"$AppDir\scripts\windows\manage.ps1`" -OpenUi" -Write-Host "& `"$AppDir\scripts\windows\manage.ps1`" -Status" -``` - -- [ ] **Step 2: Add README Windows section** - -In `README.md`, add this section after the macOS Docker client section: - -```markdown -## Windows: app proxy client - -Windows mode restores the native workflow for Discord, Vesktop, games, and other apps that do not expose proxy settings. - -Run PowerShell 7 as Administrator: - -```powershell -irm https://git.dokops.ru/dokril/vpn-proxy/raw/branch/master/scripts/install-windows-client.ps1 | iex -``` - -Installer modes: - -- `Full install`: local native `sing-box.exe` on `127.0.0.1:1080` plus ProxiFyre/WinPacketFilter. -- `ProxiFyre only`: ProxiFyre/WinPacketFilter only, pointed at an existing SOCKS5 proxy such as `127.0.0.1:8080` or `192.168.50.111:8080`. - -Local UI: - -```text -http://127.0.0.1:3456 -``` - -Recovery commands: - -```powershell -& "C:\Tools\vpn-proxy-windows\app\scripts\windows\manage.ps1" -OpenUi -& "C:\Tools\vpn-proxy-windows\app\scripts\windows\manage.ps1" -Status -& "C:\Tools\vpn-proxy-windows\app\scripts\windows\manage.ps1" -RestartServices -``` - -The UI manages profiles made of process names, folders, and explicit `.exe` files. It generates ProxiFyre config and restarts ProxiFyre only when the user applies changes. -``` - -- [ ] **Step 3: Syntax-check installer when `pwsh` exists** - -Run: - -```bash -pwsh -NoProfile -Command "\$errors = \$null; [System.Management.Automation.Language.Parser]::ParseFile('scripts/install-windows-client.ps1', [ref]\$null, [ref]\$errors) > \$null; if (\$errors) { \$errors; exit 1 }" -``` - -Expected: exit code 0. If `pwsh` is unavailable, record that the syntax check was skipped. - -- [ ] **Step 4: Commit** - -```bash -git add scripts/install-windows-client.ps1 README.md -git commit -m "feat: add windows client installer" -``` - ---- - -### Task 8: Final Verification And Polish - -**Files:** -- Modify as needed based on verification failures. - -- [ ] **Step 1: Run full automated tests** - -Run: - -```bash -npm test -``` - -Expected: PASS. - -- [ ] **Step 2: Build frontend** - -Run: - -```bash -npm run build -``` - -Expected: PASS. - -- [ ] **Step 3: Check compose config still works for macOS client** - -Run: - -```bash -docker compose -f docker-compose.client.yml config -``` - -Expected: command exits 0 and still shows `APP_MODE: client`. - -- [ ] **Step 4: Inspect generated diff** - -Run: - -```bash -git status --short -git diff --check -git diff --stat -``` - -Expected: - -- no whitespace errors; -- only Windows-client, UI, installer, README, and planned config files changed; -- no changes under `_archive/`. - -- [ ] **Step 5: Commit verification fixes if needed** - -If Step 1, Step 2, or Step 3 required code changes, commit them: - -```bash -git add src/server src/web scripts README.md test -git commit -m "fix: polish windows client implementation" -``` - -- [ ] **Step 6: Prepare Windows manual verification checklist** - -Add the final manual checklist to the implementation summary: - -```text -Windows manual checks: -1. Fresh Full install in PowerShell 7 as Administrator. -2. Fresh ProxiFyre-only install with target 127.0.0.1:8080. -3. Re-run installer over an existing install and confirm profiles survive. -4. Add process profile for Discord and apply. -5. Add folder profile for Vesktop and apply. -6. Add explicit exe profile for a test program and apply. -7. Switch a profile to external gateway target and apply. -8. Restart ProxiFyre from UI and confirm status. -9. Copy diagnostics after entering an unreachable proxy target. -``` diff --git a/docs/superpowers/specs/2026-05-21-windows-client-design.md b/docs/superpowers/specs/2026-05-21-windows-client-design.md deleted file mode 100644 index 6f2e851..0000000 --- a/docs/superpowers/specs/2026-05-21-windows-client-design.md +++ /dev/null @@ -1,236 +0,0 @@ -# Windows Client Design - -> Superseded: this document describes the earlier Node/web-control Windows direction. -> The active Windows direction is a standalone Tauri 2 desktop app under -> `apps/windows-client`, driven by `docs/windows-client-product-tech-brief.md` -> and `docs/goals/windows-modular-client/PLAN.md`. -> Content below is retained for historical context and may contradict the active -> Tauri plan. - -## Goal - -Restore the old Windows workflow in a cleaner product shape: a one-command PowerShell installer can install either a full local `sing-box` + ProxiFyre setup or ProxiFyre-only routing to an existing proxy, then expose a small local web UI for profiles, folders, executable files, status, and logs. - -## Product Shape - -The Windows mode is script-first and UI-assisted. The installer remains the durable entrypoint because Windows driver/service setup needs administrator rights and must stay easy to debug from PowerShell. The web UI is a local control surface on top of the same scripts, not a separate desktop app in the first version. - -The installer supports two paths: - -- **Full install:** install native `sing-box.exe`, configure a local SOCKS/HTTP proxy on `127.0.0.1:1080`, install WinPacketFilter and ProxiFyre, then route selected Windows apps through the local proxy. -- **ProxiFyre only:** install WinPacketFilter and ProxiFyre, then point selected Windows apps to an existing proxy target such as `127.0.0.1:8080`, `192.168.50.111:8080`, or another reachable SOCKS5 endpoint. - -The default UI direction is the approved cleaner mockup: one route status, profiles as the main object, selected profile details on the right, and a short recent activity/log section below. The first screen should answer: what is the active proxy target, whether services are running, and which profiles are currently enabled. - -## User Flows - -### Install - -The user opens PowerShell 7 as Administrator and runs: - -```powershell -irm https://git.dokops.ru/dokril/vpn-proxy/raw/branch/master/scripts/install-windows-client.ps1 | iex -``` - -The installer checks administrator rights, PowerShell version, architecture, internet access, and required paths. It installs under `C:\Tools\vpn-proxy-windows` and keeps third-party runtime files in focused subdirectories: - -- `C:\Tools\vpn-proxy-windows\app` for this project checkout or archive. -- `C:\Tools\vpn-proxy-windows\runtime\node` for portable Node.js when no suitable Node is installed. -- `C:\Tools\vpn-proxy-windows\runtime\sing-box` for `sing-box.exe`, config, and logs. -- `C:\Tools\ProxiFyre` for ProxiFyre, matching the legacy script path. - -If the user chooses Full install, the installer asks for a subscription or VLESS link, parses it through the existing subscription logic where possible, lets the user select a server, writes the native `sing-box` config, installs a scheduled task for `sing-box`, and starts it. - -If the user chooses ProxiFyre only, the installer asks for a SOCKS5 proxy target and verifies TCP connectivity before writing ProxiFyre config. - -After setup, the installer starts the local control UI on `http://127.0.0.1:3456` and prints recovery commands: - -```powershell -& "C:\Tools\vpn-proxy-windows\manage.ps1" -& "C:\Tools\vpn-proxy-windows\manage.ps1" -OpenUi -& "C:\Tools\vpn-proxy-windows\manage.ps1" -Status -``` - -### Profile Management - -Profiles are the central unit. A profile contains a name, enabled flag, proxy target, protocol list, and app items. Supported item types: - -- `process`: process name such as `Discord`, `Update`, or `Vesktop`. -- `folder`: folder path; the system scans `.exe` files and converts them to routable entries. -- `exe`: explicit executable file path; the system resolves it to the executable name for ProxiFyre and keeps the full path for display and diagnostics. - -Folder and exe entries are intentionally stored as user-facing source items, while the generated ProxiFyre config is derived. This keeps the UI understandable and makes future ProxiFyre/Proxifier adapter changes possible without changing the profile model. - -When a profile changes, the UI marks it as pending. The user applies changes once. Apply regenerates ProxiFyre `app-config.json`, restarts the ProxiFyre service, then writes an activity entry showing what changed. - -### Runtime Operations - -The UI exposes these actions: - -- start, stop, restart `sing-box` when local mode is installed; -- start, stop, restart ProxiFyre; -- switch a profile between `local-singbox` and an external proxy target; -- add process, folder, or exe entries; -- scan folder entries again; -- copy diagnostics for support/debugging; -- open logs. - -The UI does not auto-change global Windows proxy settings. Routing happens only through ProxiFyre profiles. - -## Architecture - -The active project already has a plain Node API server, React/Vite UI, subscription parser, `sing-box` config generator, logs, traffic parsing, and client/gateway modes. Windows mode should reuse those pieces and add a Windows helper boundary. - -### App Mode - -Add `APP_MODE=windows`. In Windows mode: - -- the HTTP server binds to `127.0.0.1`; -- the UI uses Windows labels and hides gateway-only TProxy/device controls; -- config generation is proxy-only like client mode, but it targets native `sing-box.exe` rather than Docker; -- service and driver actions go through the PowerShell helper, not direct Node assumptions. - -### Windows Helper Boundary - -Create a PowerShell helper module that owns privileged Windows operations: - -- install/update `sing-box.exe`; -- install/start/stop scheduled tasks; -- install/update WinPacketFilter; -- install/update ProxiFyre; -- write ProxiFyre config; -- query service/task status; -- read recent log files; -- test proxy connectivity; -- manage firewall rules for local proxy ports. - -The Node server calls the helper with explicit command names and JSON input/output. The helper returns structured JSON for every operation: - -```json -{ - "success": true, - "action": "proxies.apply", - "changed": true, - "message": "ProxiFyre config applied and service restarted" -} -``` - -Errors use the same shape with `success: false`, `error`, and optional `details`. The UI never parses raw PowerShell text. - -### Data Files - -Windows mode stores state under `C:\Tools\vpn-proxy-windows\data`: - -- `windows-profiles.json` for profile source data. -- `proxy-targets.json` for `local-singbox` and external proxy targets. -- `windows-state.json` for last applied profile revision and UI status. -- `subscription-cache.json` and `state.json` stay compatible with existing subscription/server selection logic. - -Profile shape: - -```json -{ - "id": "discord-vesktop", - "name": "Discord + Vesktop", - "enabled": true, - "proxyTargetId": "local-singbox", - "protocols": ["TCP", "UDP"], - "items": [ - { "type": "process", "value": "Discord" }, - { "type": "process", "value": "Update" }, - { - "type": "folder", - "value": "%LOCALAPPDATA%\\vesktop", - "recursive": true - }, - { - "type": "exe", - "value": "C:\\Games\\SomeGame\\game.exe" - } - ] -} -``` - -Generated ProxiFyre config is not edited directly. It is derived from enabled profiles and proxy targets, then written to `C:\Tools\ProxiFyre\app-config.json`. - -### API Surface - -Add Windows-specific endpoints: - -- `GET /api/windows/status`: returns install mode, `sing-box` status, ProxiFyre status, active target, pending changes, and recent activity. -- `GET /api/windows/profiles`: returns profile source data with resolved executable counts. -- `PUT /api/windows/profiles`: saves profiles without applying. -- `POST /api/windows/profiles/apply`: generates ProxiFyre config and restarts service. -- `POST /api/windows/profiles/scan`: resolves folder and exe entries for preview. -- `GET /api/windows/targets`: returns `local-singbox` and external proxy targets. -- `PUT /api/windows/targets`: saves external proxy targets after validation. -- `POST /api/windows/service`: start, stop, or restart `sing-box`, ProxiFyre, or the UI service. -- `GET /api/windows/logs`: returns recent helper, `sing-box`, and ProxiFyre logs. - -Existing generic endpoints for subscription fetch, server selection, apply, logs, and config validation should be reused where the behavior matches Windows mode. - -## UI Design - -The approved direction is a restrained operational UI: - -- top bar: product name, restart/stop/add profile actions; -- left nav: Overview, Profiles, Targets, Logs, Settings; -- main status panel: one sentence describing the active route, plus a compact route line such as `Selected apps -> ProxiFyre -> sing-box -> VPN`; -- main workspace: profile list on the left, selected profile details on the right; -- profile details: target selector, add process/folder/exe input, resolved items list, save/apply controls; -- activity panel: recent traffic/service events, not a full noisy log dump. - -Avoid duplicate status blocks. Avoid showing raw implementation concepts like WinPacketFilter unless the user is in diagnostics/settings. The primary terms should be `Profile`, `Proxy target`, `Local sing-box`, `Existing proxy`, `App/folder/exe`, and `Apply changes`. - -## Safety And Constraints - -The UI binds only to `127.0.0.1`. Windows actions that require elevation stay in the installer/helper path. The installer must not delete existing `C:\Tools\vpn-proxy` legacy folders without confirmation. - -Folder and exe routing needs a clear diagnostic note: ProxiFyre routing ultimately depends on what the installed ProxiFyre version accepts. The first implementation should resolve folders and exe paths to executable names for compatibility, while preserving full paths in profile data and diagnostics. If direct path matching is verified in ProxiFyre, the adapter can emit full paths without changing the UI model. - -The installer should be idempotent: - -- re-running it updates project files; -- existing subscriptions and profiles are preserved unless the user chooses reset; -- existing ProxiFyre config is backed up before overwrite; -- failed applies leave the previous generated config available for rollback. - -## Testing And Verification - -Use focused tests for pure logic: - -- profile normalization; -- folder/exe item resolution; -- ProxiFyre config generation; -- proxy target validation; -- Windows helper JSON command contract; -- `APP_MODE=windows` public state and config generation. - -Use manual Windows verification for privileged operations: - -- fresh Full install; -- fresh ProxiFyre-only install; -- re-run installer over existing install; -- add process profile; -- add folder profile; -- add explicit exe profile; -- switch a profile from local sing-box to external proxy; -- restart ProxiFyre and verify service status; -- copy diagnostics after a failed proxy target check. - -Local non-Windows development should still run `npm test` and `npm run build`. Windows-only helper commands should have dry-run or mockable modes so logic can be tested without installing drivers on the development machine. - -## Non-Goals For First Version - -- No Electron or Tauri wrapper. -- No global Windows system proxy changes. -- No transparent routing without ProxiFyre. -- No remote multi-device management. -- No automatic uninstall of unrelated WinPacketFilter users. -- No Proxifier support until ProxiFyre behavior is stable. - -## Implementation Defaults - -- The UI server runs when opened by `manage.ps1 -OpenUi` in the first version. An at-logon scheduled UI task can be added later after the helper and UI are stable. -- Full install uses portable Node/npm when the machine has no suitable Node.js. The installer builds the React UI locally for MVP; a prebuilt release artifact can replace that later without changing user-facing behavior. -- ProxiFyre generation emits process names in the first version for compatibility. Full folder and exe paths remain in profile data and diagnostics; the adapter can start emitting full paths later if ProxiFyre path matching is verified. diff --git a/docs/windows-client-product-tech-brief.md b/docs/windows-client-product-tech-brief.md deleted file mode 100644 index 0161ae4..0000000 --- a/docs/windows-client-product-tech-brief.md +++ /dev/null @@ -1,635 +0,0 @@ -# Windows Proxy Client: Product And Technology Brief - -Дата: 2026-07-03 - -Цель документа: описать, как должно выглядеть и работать Windows-приложение для управления proxy/VPN-маршрутизацией приложений, и какой стек лучше использовать для реализации. - -Этот документ можно отдать другой модели или команде как исходное ТЗ. - -## Коротко - -Нужно Windows-приложение, которое разделяет систему на три независимые части: - -1. **Control App**: маленькое desktop-приложение для настройки, статуса, профилей, логов и запуска операций. -2. **Proxyfier Layer**: отдельный компонент, который заставляет выбранные Windows-приложения ходить через SOCKS5/HTTP proxy, даже если они сами не умеют proxy. -3. **Local sing-box**: опциональный локальный VPN/proxy runtime. Его можно установить, не устанавливать, остановить, заменить внешним proxy target. - -Главный принцип: пользователь не обязан ставить все сразу. Если у него уже есть proxy, ему нужны только Control App + Proxyfier. Если нужен локальный VPN-клиент, он отдельно ставит `sing-box`. - -## Как это должно выглядеть - -Приложение должно выглядеть как компактная системная утилита, а не как сайт. - -Главный экран: - -- верхняя строка: общий статус маршрута; -- три карточки компонентов: `Control App`, `Proxyfier`, `Local sing-box`; -- список активных профилей; -- кнопка `Apply changes`; -- короткая лента последних событий. - -Пример главного статуса: - -```text -Selected apps -> ProxiFyre -> Local sing-box 127.0.0.1:1080 -> VPN -``` - -или: - -```text -Selected apps -> ProxiFyre -> Existing proxy 192.168.50.111:8080 -``` - -Если `sing-box` не установлен, это не ошибка. Карточка должна показывать: - -```text -Local sing-box -Not installed -Install if you want this PC to run its own local VPN proxy. -``` - -Если Proxyfier не установлен, профили можно редактировать, но apply должен быть заблокирован: - -```text -Proxyfier is required to route selected apps. -Install Proxyfier -``` - -## Основные экраны - -### 1. Overview - -Показывает: - -- текущий route line; -- статус Control App; -- статус Proxyfier; -- статус Local sing-box; -- активный proxy target; -- сколько приложений сейчас включено в routing; -- последние 5-10 событий. - -Действия: - -- restart Proxyfier; -- restart local sing-box, если установлен; -- open logs; -- copy diagnostics. - -### 2. Profiles - -Профиль - главный объект настройки. - -Профиль содержит: - -- название; -- enabled/disabled; -- proxy target; -- протоколы: TCP, UDP; -- список приложений. - -Типы элементов: - -- `process`: имя процесса, например `Discord`, `Telegram`, `Code`; -- `folder`: папка, приложение сканирует `.exe` внутри; -- `exe`: конкретный путь к `.exe`. - -UI профиля: - -- слева список профилей; -- справа детали выбранного профиля; -- поле выбора target; -- кнопки добавления: `Process`, `Folder`, `EXE`; -- preview resolved apps; -- `Save`; -- `Apply changes`. - -Важно: пользователь должен видеть понятные исходные элементы, а не только сгенерированный конфиг Proxyfier. - -### 3. Targets - -Proxy target - это куда Proxyfier отправляет трафик выбранных приложений. - -Типы targets: - -- `Local sing-box`: `127.0.0.1:1080`, доступен только если local sing-box установлен и запущен; -- `Existing SOCKS5 proxy`: например `127.0.0.1:8080` или `192.168.50.111:8080`; -- `Existing HTTP proxy`, если выбранный proxyfier поддерживает HTTP. - -На экране targets: - -- список targets; -- проверка соединения; -- имя, host, port, protocol; -- статус last checked; -- кнопка set default. - -### 4. Components - -Отдельный экран или часть Overview. - -Компоненты: - -- Control App; -- Proxyfier; -- Local sing-box. - -Для каждого: - -- installed / not installed; -- running / stopped; -- version; -- path; -- service/task status; -- actions. - -Actions должны быть явными: - -- `Install`; -- `Repair`; -- `Start`; -- `Stop`; -- `Restart`; -- `Open folder`; -- `View logs`. - -Нельзя делать скрытую установку `sing-box` при сохранении профиля. - -### 5. Logs / Diagnostics - -Должно быть две зоны: - -- activity: действия пользователя и результат apply; -- runtime logs: proxyfier logs, sing-box logs, helper logs. - -Кнопка `Copy diagnostics` должна собирать: - -- версии компонентов; -- paths; -- running status; -- активные profiles; -- targets без секретов; -- последние ошибки; -- путь к сгенерированному proxyfier config. - -## Пользовательские сценарии - -### Сценарий A: у пользователя уже есть proxy - -1. Пользователь устанавливает Control App. -2. Открывает приложение. -3. Видит, что Proxyfier не установлен, а sing-box отсутствует. -4. Нажимает `Install Proxyfier`. -5. Добавляет target `192.168.50.111:8080`. -6. Создает профиль `Discord`. -7. Добавляет process `Discord`. -8. Нажимает `Apply changes`. -9. Приложение генерирует config для Proxyfier и перезапускает proxyfier service. - -Результат: Discord ходит через внешний proxy. Local sing-box не нужен. - -### Сценарий B: пользователь хочет локальный VPN proxy - -1. Пользователь устанавливает Control App. -2. Устанавливает Proxyfier. -3. Устанавливает Local sing-box. -4. Вводит subscription/VLESS link. -5. Выбирает сервер. -6. Local sing-box поднимает SOCKS5/HTTP endpoint на `127.0.0.1:1080`. -7. Профили используют target `Local sing-box`. - -Результат: выбранные приложения ходят через локальный sing-box. - -### Сценарий C: временно отключить VPN - -1. Пользователь открывает профиль. -2. Меняет target с `Local sing-box` на внешний proxy или `Direct/Disabled`. -3. Нажимает `Apply changes`. - -Результат: Proxyfier перегенерирован, local sing-box можно остановить отдельно. - -## Рекомендуемый стек - -### Desktop shell: Tauri 2 - -Рекомендация: **Tauri 2 + React + TypeScript + Rust backend**. - -Почему: - -- Tauri ориентирован на маленькие desktop-приложения и использует системный web renderer, поэтому приложение легче Electron. -- Можно писать UI на обычном web stack: React/TypeScript/Vite. -- Backend-часть на Rust хорошо подходит для Windows APIs, файлов, процессов, sidecar binaries и безопасных команд. -- Tauri поддерживает sidecar binaries, но требует явно выдать permissions на запуск sidecar, что полезно для security boundary. - -Frontend: - -- React; -- TypeScript; -- Vite; -- TanStack Query для загрузки/кэша status/API; -- Zustand или Jotai для локального UI state; -- Zod для валидации JSON-моделей; -- CSS modules или Tailwind. Для этой утилиты лучше сдержанный Windows-like UI, без тяжелой дизайн-системы. - -Backend внутри Tauri: - -- Rust commands для простых операций; -- отдельный `core` crate с доменной логикой; -- отдельный `windows-helper` binary для elevated/privileged действий. - -Не рекомендую начинать с Electron, если нет жесткой причины. Electron проще для web-команды, но тяжелее по размеру и памяти. Для маленькой системной утилиты Tauri подходит лучше. - -### Privileged helper - -Нужно отделить обычное приложение от операций администратора. - -Рекомендуемая модель: - -```text -Tauri UI - -> Rust app backend - -> unprivileged status/read operations - -> explicit elevated helper for install/repair/service operations -``` - -Privileged helper может быть: - -- Rust CLI, который запускается elevated только для конкретной операции; -- Rust Windows service/helper, если нужен постоянный privileged agent; -- PowerShell scripts только как thin installer layer, не как основная бизнес-логика. - -Для MVP можно сделать проще: - -- installers запускаются отдельно от имени администратора; -- Control App работает обычным пользователем; -- service start/stop/restart идет через helper command; -- helper возвращает JSON, UI не парсит текст PowerShell. - -Контракт helper: - -```json -{ - "action": "proxyfier.apply", - "payload": { - "configPath": "C:\\Tools\\ProxiFyre\\app-config.json", - "config": {} - } -} -``` - -Ответ: - -```json -{ - "success": true, - "action": "proxyfier.apply", - "changed": true, - "message": "Proxyfier config applied and service restarted" -} -``` - -Ошибки: - -```json -{ - "success": false, - "action": "proxyfier.apply", - "error": "Proxyfier service is not installed", - "details": {} -} -``` - -### Service/runtime management - -Для `sing-box` как background runtime: - -- использовать `sing-box check` перед применением config; -- хранить config отдельно; -- запускать как Windows service или scheduled task; -- для service wrapper можно использовать WinSW, если не хочется писать собственный Windows service wrapper. - -Практичный вариант: - -- v1: WinSW wraps `sing-box.exe`; -- v2: собственный Rust service/helper, если понадобится полный контроль. - -Control App не должен напрямую владеть процессом `sing-box`. Он должен управлять service/task через helper. - -### Local sing-box - -`sing-box` - опциональный runtime. - -Его роль: - -- принять subscription/VLESS/sing-box config; -- поднять локальный mixed SOCKS/HTTP inbound; -- слушать только `127.0.0.1`, например `127.0.0.1:1080`; -- маршрутизировать трафик через выбранный outbound. - -Config генерируется из source state приложения и проверяется: - -```powershell -sing-box check -c C:\Tools\VpnProxy\sing-box\config.json -``` - -Local sing-box не должен быть обязательным. Если profile target указывает на внешний proxy, `sing-box` может отсутствовать. - -### Proxyfier layer - -Рекомендуемый стартовый backend: **ProxiFyre**. - -Почему: - -- open-source; -- Windows-focused; -- маршрутизирует TCP и UDP; -- работает per-application; -- использует `app-config.json`; -- может работать как Windows Service. - -Важное ограничение: ProxiFyre лицензируется как AGPL-3.0. Если продукт должен быть закрытым коммерческим приложением, нужно заранее решить юридический вопрос или сделать adapter layer, чтобы можно было заменить engine на: - -- коммерческий Proxifier; -- ProxyBridge; -- собственный WinDivert/NDIS/WFP-based engine; -- другой per-app proxy router. - -Интерфейс должен называться не `ProxiFyreConfig`, а шире: - -```text -ProxyRouterAdapter -``` - -Первый adapter: - -```text -ProxiFyreAdapter -``` - -Это позволит поменять engine без переделки UI и профилей. - -### Data storage - -Для MVP лучше использовать простые JSON-файлы с schema validation. - -Причина: - -- настройки легко читать и бэкапить; -- можно быстро отлаживать; -- config portable; -- подходит для profile/target/source state. - -Рекомендуемые файлы: - -```text -C:\ProgramData\VpnProxy\config\profiles.json -C:\ProgramData\VpnProxy\config\targets.json -C:\ProgramData\VpnProxy\config\components.json -C:\ProgramData\VpnProxy\state\activity.json -C:\ProgramData\VpnProxy\state\last-status.json -C:\ProgramData\VpnProxy\generated\proxifyre-app-config.json -C:\ProgramData\VpnProxy\generated\sing-box-config.json -``` - -Если нужна большая история событий, статистика трафика или сложные миграции, тогда добавить SQLite: - -- `rusqlite` или `sqlx` в Rust; -- миграции; -- таблицы `activity`, `component_status`, `traffic_events`. - -Но source of truth для профилей можно оставить JSON даже при наличии SQLite. - -### Installer strategy - -Нужны три явных installer entrypoints: - -```text -Install Control App -Install Proxyfier Layer -Install Local sing-box -``` - -Они могут быть кнопками в UI, но каждая операция должна быть отдельной и понятной. - -CLI/script names: - -```text -install-control-app.ps1 -install-proxyfier.ps1 -install-singbox.ps1 -``` - -Или в packaged app: - -```text -VpnProxySetup.exe /component control-app -VpnProxySetup.exe /component proxyfier -VpnProxySetup.exe /component sing-box -``` - -Каждый installer: - -- idempotent; -- делает backup перед overwrite; -- не удаляет чужие файлы без подтверждения; -- проверяет admin rights; -- пишет machine-readable install result; -- не трогает остальные компоненты без явного выбора. - -### Security model - -Правила: - -- UI работает без admin rights. -- Admin elevation только для install/repair/service/config apply, если это реально нужно. -- Local API, если будет, слушает только `127.0.0.1`. -- Лучше использовать Tauri commands / named pipe, чем открытый HTTP port. -- Если нужен loopback HTTP, включить token или origin check. -- Секреты subscription URLs не показывать в diagnostics. -- Generated configs не редактируются вручную из UI. -- Every apply creates backup. - -## Архитектура - -```text -+-------------------------------+ -| Tauri Control App | -| React/TypeScript UI | -+---------------+---------------+ - | - v -+-------------------------------+ -| Rust App Backend | -| profiles, targets, validation | -| component status aggregation | -+-------+---------------+-------+ - | | - v v -+---------------+ +-------------------+ -| Proxy Router | | Local sing-box | -| Adapter | | Adapter | -| ProxiFyre v1 | | config + service | -+-------+-------+ +---------+---------+ - | | - v v -+---------------+ +-------------------+ -| ProxiFyre | | sing-box.exe | -| Windows svc | | Windows svc/task | -+---------------+ +-------------------+ -``` - -## Модель данных - -### Profile - -```json -{ - "id": "discord", - "name": "Discord", - "enabled": true, - "targetId": "local-singbox", - "protocols": ["TCP", "UDP"], - "items": [ - { "type": "process", "value": "Discord" }, - { "type": "folder", "value": "%LOCALAPPDATA%\\Discord", "recursive": true }, - { "type": "exe", "value": "C:\\Games\\Game\\game.exe" } - ] -} -``` - -### Target - -```json -{ - "id": "local-singbox", - "name": "Local sing-box", - "type": "local", - "protocol": "socks5", - "host": "127.0.0.1", - "port": 1080, - "requiresComponent": "singbox" -} -``` - -External target: - -```json -{ - "id": "home-gateway", - "name": "Home gateway", - "type": "external", - "protocol": "socks5", - "host": "192.168.50.111", - "port": 8080 -} -``` - -### Component status - -```json -{ - "id": "proxyfier", - "name": "Proxyfier", - "installed": true, - "running": true, - "version": "2.3.0", - "path": "C:\\Tools\\ProxiFyre", - "serviceName": "ProxiFyreService", - "problems": [], - "actions": ["restart", "repair", "openLogs"] -} -``` - -## Apply behavior - -Apply должен делать одно понятное действие: - -1. Прочитать profiles. -2. Прочитать targets. -3. Проверить, что выбранные targets доступны. -4. Проверить, что Proxyfier установлен. -5. Разрешить folder/exe в process names. -6. Сгенерировать proxyfier config. -7. Сделать backup старого config. -8. Записать новый config. -9. Перезапустить Proxyfier service. -10. Записать activity entry. - -Если profile использует `local-singbox`, дополнительно: - -- проверить, что `sing-box` установлен; -- проверить, что service running; -- проверить, что `127.0.0.1:1080` отвечает. - -Если `local-singbox` не установлен, но profile target внешний, apply должен работать. - -## Что не делать - -- Не делать глобальную смену Windows proxy settings. -- Не делать `sing-box` обязательным. -- Не смешивать installer и profile apply. -- Не хранить generated ProxiFyre config как source of truth. -- Не привязывать UI напрямую к ProxiFyre, нужен adapter layer. -- Не запускать privileged операции без явного согласия пользователя. -- Не делать большой dashboard с лишней статистикой в первой версии. - -## MVP - -Самый правильный первый slice: - -1. Tauri app shell. -2. Profiles UI. -3. Targets UI. -4. Component status UI. -5. ProxiFyre adapter. -6. External SOCKS5 target. -7. Apply profile -> generate ProxiFyre config -> restart service. - -В MVP `sing-box` может быть только карточкой `Not installed / Install`. - -После этого добавить: - -1. Local sing-box installer. -2. Subscription import. -3. Server selection. -4. Generate sing-box config. -5. Start/stop/restart local sing-box service. - -## Acceptance criteria - -Приложение считается успешным, если: - -- можно установить только Control App; -- можно установить Proxyfier отдельно; -- можно не устанавливать sing-box; -- можно добавить внешний SOCKS5 target; -- можно создать профиль для Discord; -- можно применить профиль; -- generated ProxiFyre config не редактируется пользователем вручную; -- UI показывает, что local sing-box отсутствует, но это не ломает внешний proxy flow; -- после установки sing-box появляется target `Local sing-box`; -- пользователь может переключить профиль с внешнего target на local sing-box. - -## Prompt For Another AI - -Build a Windows desktop proxy management app. - -Use Tauri 2 with React, TypeScript, Vite, and a Rust backend. The app must manage three independent components: the Control App, a proxyfier layer, and optional local sing-box. Do not make sing-box mandatory. - -The UI must be a compact Windows utility with these screens: Overview, Profiles, Targets, Components, Logs. Profiles contain process/folder/exe entries and choose a proxy target. Targets can be local sing-box or external SOCKS5/HTTP proxies. Proxyfier is the layer that routes selected apps through the chosen target. - -Start with ProxiFyre as the first proxy router adapter, but design an adapter boundary so it can later be replaced. Store source configuration as JSON with schema validation. Generated ProxiFyre and sing-box configs are derived artifacts, not source truth. - -Privileged operations must be isolated in an explicit helper/installer flow. The main UI should run without admin rights. Install Control App, Install Proxyfier, and Install Local sing-box must be separate operations. Applying a profile must not silently install missing components. - -MVP: external SOCKS5 target + ProxiFyre profile apply. Then add optional local sing-box installation, subscription import, server selection, and local sing-box service control. - -## References - -- Tauri 2: https://v2.tauri.app/ -- Tauri sidecar permissions: https://v2.tauri.app/develop/sidecar/ -- sing-box configuration: https://sing-box.sagernet.org/configuration/ -- ProxiFyre repository: https://github.com/wiresock/proxifyre -- WinSW service wrapper: https://github.com/winsw/winsw -- Microsoft Windows Service with Worker Service: https://learn.microsoft.com/en-us/dotnet/core/extensions/windows-service -