diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c99dbbc3ec..7511cbc161 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -31,6 +31,7 @@ name: release # APPLE_CERTIFICATE / APPLE_CERTIFICATE_PASSWORD / KEYCHAIN_PASSWORD — macOS Developer ID signing # APPLE_ID / APPLE_PASSWORD / APPLE_TEAM_ID or APPLE_API_KEY / APPLE_API_ISSUER / APPLE_API_KEY_P8 — macOS notarization # WINDOWS_CERTIFICATE / WINDOWS_CERTIFICATE_PASSWORD — Windows installer signing +# TAURI_SIGNING_PRIVATE_KEY / TAURI_SIGNING_PRIVATE_KEY_PASSWORD — updater artifact signing on: workflow_dispatch: @@ -565,11 +566,11 @@ jobs: - platform: linux-x64 os: ubuntu-22.04 rust_target: x86_64-unknown-linux-gnu - tauri_bundles: deb,rpm + tauri_bundles: appimage,deb,rpm - platform: linux-arm64 os: ubuntu-24.04-arm rust_target: aarch64-unknown-linux-gnu - tauri_bundles: deb,rpm + tauri_bundles: appimage,deb,rpm - platform: windows-x64 os: windows-latest rust_target: x86_64-pc-windows-msvc @@ -769,6 +770,22 @@ jobs: & "$env:GITHUB_WORKSPACE/apps/desktop/src-tauri/windows-sign.ps1" -Path $path } + - name: Detect Tauri updater signing input + env: + TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} + run: | + set -euo pipefail + if [ -z "$TAURI_SIGNING_PRIVATE_KEY" ]; then + echo "UPDATER_SIGNING_ENABLED=false" >> "$GITHUB_ENV" + echo "::notice::Tauri updater signing key is absent; signed updater artifacts and latest.json will be skipped." + exit 0 + fi + if ! bash scripts/release/updater-signing-ready.sh "${{ matrix.platform }}"; then + echo "::error::Updater signing prerequisites failed for ${{ matrix.platform }}." >&2 + exit 1 + fi + echo "UPDATER_SIGNING_ENABLED=true" >> "$GITHUB_ENV" + - name: Build Tauri desktop app timeout-minutes: 70 env: @@ -789,6 +806,8 @@ jobs: WINDOWS_CERTIFICATE_PASSWORD: ${{ secrets.WINDOWS_CERTIFICATE_PASSWORD }} WINDOWS_TIMESTAMP_URL: ${{ secrets.WINDOWS_TIMESTAMP_URL }} WINDOWS_SIGNTOOL_PATH: ${{ secrets.WINDOWS_SIGNTOOL_PATH }} + TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} + TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} run: | set -euo pipefail if [ "${MACOS_SIGNING_ENABLED:-false}" != "true" ]; then @@ -809,6 +828,13 @@ jobs: unset WINDOWS_TIMESTAMP_URL unset WINDOWS_SIGNTOOL_PATH fi + updater_config='{"bundle":{"createUpdaterArtifacts":false}}' + if [ "${UPDATER_SIGNING_ENABLED:-false}" = "true" ]; then + updater_config='{"bundle":{"createUpdaterArtifacts":"v1Compatible"}}' + else + unset TAURI_SIGNING_PRIVATE_KEY + unset TAURI_SIGNING_PRIVATE_KEY_PASSWORD + fi pnpm -C apps --filter @kandev/desktop build:vite cd apps/desktop @@ -871,7 +897,8 @@ jobs: pnpm tauri build \ --features desktop-runtime \ --target "${{ matrix.rust_target }}" \ - --bundles "${{ matrix.tauri_bundles }}" + --bundles "${{ matrix.tauri_bundles }}" \ + --config "$updater_config" } status=0 @@ -1011,7 +1038,11 @@ jobs: -name '*.rpm' -o \ -name '*.msi' -o \ -name '*.exe' -o \ - -name '*.AppImage' \ + -name '*.AppImage' -o \ + -name '*.app.tar.gz' -o \ + -name '*.AppImage.tar.gz' -o \ + -name '*.nsis.zip' -o \ + -name '*.sig' \ \) -print0) if [ "$found" -eq 0 ]; then @@ -1390,6 +1421,9 @@ jobs: ref: ${{ needs.prepare.outputs.ref }} fetch-depth: 0 + - name: Install updater signature verifier toolchain + run: rustup toolchain install 1.88.0 --profile minimal + - name: Generate release notes uses: orhun/git-cliff-action@f50e11560dce63f7c33227798f90b924471a88b5 # v4 with: @@ -1471,6 +1505,30 @@ jobs: scripts/release/verify-desktop-assets.sh dist/release-assets ls -lh dist/release-assets + - name: Generate and verify updater manifest + env: + RUSTUP_TOOLCHAIN: 1.88.0 + run: | + set -euo pipefail + pub_date="$(git show -s --format=%cI '${{ needs.prepare.outputs.tag }}')" + node scripts/release/updater-manifest.mjs generate \ + --assets-dir dist/release-assets \ + --output dist/release-assets/latest.json \ + --version "${{ needs.prepare.outputs.version }}" \ + --tag "${{ needs.prepare.outputs.tag }}" \ + --repository "${{ github.repository }}" \ + --notes-file RELEASE_NOTES.md \ + --pub-date "$pub_date" \ + --allow-unsigned + if [ -f dist/release-assets/latest.json ]; then + node scripts/release/updater-manifest.mjs verify \ + --assets-dir dist/release-assets \ + --manifest dist/release-assets/latest.json \ + --version "${{ needs.prepare.outputs.version }}" \ + --tag "${{ needs.prepare.outputs.tag }}" \ + --repository "${{ github.repository }}" + fi + - name: Publish release uses: softprops/action-gh-release@3bb12739c298aeb8a4eeaf626c5b8d85266b0e65 # v2 with: @@ -1481,6 +1539,7 @@ jobs: dist/release-assets/kandev-*.tar.gz dist/release-assets/kandev-*.tar.gz.sha256 dist/release-assets/kandev-desktop-* + dist/release-assets/latest.json publish-npm: name: Publish npm packages diff --git a/apps/backend/internal/notifications/service/service.go b/apps/backend/internal/notifications/service/service.go index 2defec42c9..b374f7e349 100644 --- a/apps/backend/internal/notifications/service/service.go +++ b/apps/backend/internal/notifications/service/service.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "os" "strings" "github.com/google/uuid" @@ -21,6 +22,7 @@ import ( const ( EventTaskSessionWaitingForInput = "session.waiting_for_input" EventOfficeInboxItem = "office.inbox_item" + desktopNativeNotificationsEnv = "KANDEV_DESKTOP_NATIVE_NOTIFICATIONS" ) var ErrProviderNotFound = errors.New("notification provider not found") @@ -42,7 +44,9 @@ func NewService(repo notificationstore.Repository, taskRepo taskGetter, hub *gat providerMap := map[models.ProviderType]providers.Provider{ models.ProviderTypeLocal: providers.NewLocalProvider(hub), models.ProviderTypeApprise: providers.NewAppriseProvider(), - models.ProviderTypeSystem: providers.NewSystemProvider(), + } + if os.Getenv(desktopNativeNotificationsEnv) != "true" { + providerMap[models.ProviderTypeSystem] = providers.NewSystemProvider() } return &Service{ repo: repo, @@ -74,7 +78,12 @@ func (s *Service) ListProviders(ctx context.Context, userID string) ([]*models.P return nil, nil, err } subscriptions := make(map[string][]string, len(providers)) + availableProviders := make([]*models.Provider, 0, len(providers)) for _, provider := range providers { + if s.providers[provider.Type] == nil { + continue + } + availableProviders = append(availableProviders, provider) subs, err := s.repo.ListSubscriptionsByProvider(ctx, provider.ID) if err != nil { return nil, nil, err @@ -85,7 +94,7 @@ func (s *Service) ListProviders(ctx context.Context, userID string) ([]*models.P } } } - return providers, subscriptions, nil + return availableProviders, subscriptions, nil } func (s *Service) CreateProvider(ctx context.Context, userID, name string, providerType models.ProviderType, config map[string]interface{}, enabled bool, events []string) (*models.Provider, error) { diff --git a/apps/backend/internal/notifications/service/service_test.go b/apps/backend/internal/notifications/service/service_test.go new file mode 100644 index 0000000000..ef667688c1 --- /dev/null +++ b/apps/backend/internal/notifications/service/service_test.go @@ -0,0 +1,40 @@ +package service + +import ( + "testing" + + "github.com/kandev/kandev/internal/common/logger" + "github.com/kandev/kandev/internal/notifications/models" + "go.uber.org/zap" +) + +func TestNewServiceSuppressesSystemProviderForDesktopOwnedLaunch(t *testing.T) { + t.Setenv("KANDEV_DESKTOP_NATIVE_NOTIFICATIONS", "true") + log, err := logger.NewFromZap(zap.NewNop()) + if err != nil { + t.Fatalf("create logger: %v", err) + } + + svc := NewService(nil, nil, nil, log) + + if _, exists := svc.providers[models.ProviderTypeSystem]; exists { + t.Fatal("system notification provider must be suppressed for a desktop-owned launch") + } + if _, exists := svc.providers[models.ProviderTypeLocal]; !exists { + t.Fatal("local websocket notification provider must remain enabled") + } +} + +func TestNewServiceRetainsSystemProviderForNonDesktopLaunch(t *testing.T) { + t.Setenv("KANDEV_DESKTOP_NATIVE_NOTIFICATIONS", "") + log, err := logger.NewFromZap(zap.NewNop()) + if err != nil { + t.Fatalf("create logger: %v", err) + } + + svc := NewService(nil, nil, nil, log) + + if _, exists := svc.providers[models.ProviderTypeSystem]; !exists { + t.Fatal("system notification provider must remain enabled outside the desktop-owned launch") + } +} diff --git a/apps/cli/src/release-config.test.ts b/apps/cli/src/release-config.test.ts index 89a4f973d0..21a4e66823 100644 --- a/apps/cli/src/release-config.test.ts +++ b/apps/cli/src/release-config.test.ts @@ -312,7 +312,66 @@ describe("release desktop artifacts", () => { expect(workflow).toContain(`- platform: linux-x64 os: ubuntu-22.04 rust_target: x86_64-unknown-linux-gnu - tauri_bundles: deb,rpm`); + tauri_bundles: appimage,deb,rpm`); + }); + + it("builds and publishes a complete signed Tauri updater feed", () => { + const workflow = releaseWorkflow(); + const tauriConfig = JSON.parse(readRepoFile("apps/desktop/src-tauri/tauri.conf.json")); + const manifestScript = readRepoFile("scripts/release/updater-manifest.mjs"); + const verifyAssetsScript = readRepoFile("scripts/release/verify-desktop-assets.sh"); + + expect(tauriConfig.bundle.createUpdaterArtifacts).toBe(false); + expect(workflow).toContain( + "TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}", + ); + expect(workflow).toContain( + "TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}", + ); + expect(workflow).toContain('createUpdaterArtifacts":"v1Compatible"'); + expect(workflow).toContain("scripts/release/updater-manifest.mjs generate"); + expect(workflow).toContain("scripts/release/updater-manifest.mjs verify"); + expect(workflow).toContain("dist/release-assets/latest.json"); + + for (const target of [ + "darwin-aarch64", + "darwin-x86_64", + "linux-aarch64", + "linux-x86_64", + "windows-x86_64", + ]) { + expect(manifestScript, `updater manifest must include ${target}`).toContain(`"${target}"`); + } + + for (const extension of ["*.app.tar.gz", "*.AppImage.tar.gz", "*.nsis.zip", "*.sig"]) { + expect(workflow, `desktop collector must retain ${extension}`).toContain( + `-name '${extension}'`, + ); + expect(verifyAssetsScript, `asset verification must recognize ${extension}`).toContain( + extension, + ); + } + }); + + it("keeps updater signing independent from platform identity signing", () => { + const workflow = releaseWorkflow(); + const signingDocs = readRepoFile("docs/desktop-tauri-signing.md"); + const updaterSigningScript = readRepoFile("scripts/release/updater-signing-ready.sh"); + + for (const key of ["TAURI_SIGNING_PRIVATE_KEY", "TAURI_SIGNING_PRIVATE_KEY_PASSWORD"]) { + expect(workflow).toContain(key); + expect(signingDocs).toContain(key); + } + expect(signingDocs).toContain("distinct from macOS and Windows code-signing credentials"); + expect(signingDocs).toContain("Installers remain available"); + expect(signingDocs).toContain("`latest.json` is not"); + expect(workflow).toContain('scripts/release/updater-signing-ready.sh "${{ matrix.platform }}"'); + expect(updaterSigningScript).not.toContain("MACOS_SIGNING_ENABLED"); + expect(updaterSigningScript).not.toContain("WINDOWS_SIGNING_ENABLED"); + expect(signingDocs).toContain("Missing OS credentials do not block Tauri-signed updater"); + expect(workflow).not.toContain( + "Refusing to build an updater artifact that is not also signed for its operating system.", + ); }); it("builds unsigned desktop releases when signing inputs are incomplete", () => { diff --git a/apps/desktop/AGENTS.md b/apps/desktop/AGENTS.md index 6512414c4e..a6362a665e 100644 --- a/apps/desktop/AGENTS.md +++ b/apps/desktop/AGENTS.md @@ -9,7 +9,8 @@ Run from `apps/` unless noted: - `pnpm --filter @kandev/desktop build:vite` builds the startup surface. - `pnpm --filter @kandev/desktop build` builds the Linux desktop bundle locally. - `pnpm --filter @kandev/desktop e2e` builds the app and runs the Linux smoke harness under Xvfb when needed. -- From `apps/desktop/src-tauri`, `cargo test` runs Rust unit tests without requiring the Tauri runtime feature by default. +- From `apps/desktop/src-tauri`, `cargo test --features desktop-runtime` runs the complete Rust + suite, including native command/plugin integration. ## Runtime Resources @@ -30,9 +31,20 @@ Use `scripts/release/prepare-desktop-runtime.sh` and `scripts/release/verify-des - Frontend code in `src/` is only the startup/error surface. The real product UI is still served by the Go backend after `/health` succeeds. - Rust code owns backend process spawning and cleanup. Do not expose broad shell or filesystem permissions to frontend JavaScript. +- Native menus emit versioned `kandev-desktop-v1-*` events for SPA-owned context and navigation. + Updater, notification, and external-link operations use narrow generated Tauri commands scoped + to the owned loopback WebView; do not grant the SPA direct plugin permissions. - Desktop launches force `KANDEV_SERVER_HOST=127.0.0.1`, prefer a stable desktop port with random fallback, and pass `KANDEV_BUNDLE_DIR` to the native launcher. +- Desktop launches set `KANDEV_DESKTOP_NATIVE_NOTIFICATIONS=true` so the owned backend suppresses + only its duplicate System notification provider. - `KANDEV_DESKTOP_RUNTIME_DIR` is a test/development override for runtime resources. ## Release -Desktop artifacts are built in `.github/workflows/release.yml` after the platform runtime bundles. macOS and Windows signing is automatic: complete signing/notarization secrets produce signed artifacts; missing or incomplete inputs produce unsigned desktop artifacts with a release-notes warning. Use `desktop_validation_only=true` for artifact-only validation runs that skip the release PR, tag, publish jobs, and public container tags. +Desktop artifacts are built in `.github/workflows/release.yml` after the platform runtime bundles. +macOS and Windows signing is automatic: complete signing/notarization secrets produce signed +artifacts; missing or incomplete inputs produce unsigned desktop artifacts with a release-notes +warning. A separate Tauri updater key signs `.app.tar.gz`, `.AppImage.tar.gz`, and NSIS updater +bundles and enables `latest.json`; unsigned installers are never advertised by the updater. Use +`desktop_validation_only=true` for artifact-only validation runs that skip the release PR, tag, +publish jobs, and public container tags. diff --git a/apps/desktop/e2e/desktop-launch-smoke.test.mjs b/apps/desktop/e2e/desktop-launch-smoke.test.mjs index c05a745dad..2df7f9cce8 100644 --- a/apps/desktop/e2e/desktop-launch-smoke.test.mjs +++ b/apps/desktop/e2e/desktop-launch-smoke.test.mjs @@ -13,6 +13,8 @@ import { const __dirname = dirname(fileURLToPath(import.meta.url)); const backendRsPath = resolve(__dirname, "../src-tauri/src/backend.rs"); +const mainRsPath = resolve(__dirname, "../src-tauri/src/main.rs"); +const shellRsPath = resolve(__dirname, "../src-tauri/src/shell.rs"); async function withTempDir(run) { const dir = await mkdtemp(join(tmpdir(), "wait-for-file-")); @@ -96,3 +98,36 @@ test("health-requested timeout stays above the Rust backend's own HEALTH_TIMEOUT "ROOT_REQUESTED_TIMEOUT_MS must be a positive integer", ); }); + +test("Close Context owns Cmd/Ctrl+W without a native window-close fallback", async () => { + const [mainSource, shellSource] = await Promise.all([ + readFile(mainRsPath, "utf8"), + readFile(shellRsPath, "utf8"), + ]); + + assert.match(mainSource, /MENU_CLOSE_CONTEXT, "Close Context"[\s\S]*CmdOrCtrl\+KeyW/); + assert.match( + shellSource, + /MENU_CLOSE_CONTEXT\s*=>\s*Some\(MenuAction::Emit\(CLOSE_CONTEXT_EVENT\)\)/, + ); + assert.doesNotMatch(mainSource, /PredefinedMenuItem::close_window/); + assert.doesNotMatch(mainSource, /\.accelerator\("CmdOrCtrl\+KeyW"\)[\s\S]{0,160}shutdown_and_exit/); +}); + +test("generic app activation never consumes a pending notification route", async () => { + const mainSource = await readFile(mainRsPath, "utf8"); + + assert.doesNotMatch(mainSource, /emit_pending_notification_route/); + assert.match( + mainSource, + /tauri_plugin_single_instance::init\([\s\S]*activate_main_window/, + ); + assert.match(mainSource, /RunEvent::Reopen[\s\S]*activate_main_window/); +}); + +test("fullscreen uses the platform-native desktop accelerators", async () => { + const mainSource = await readFile(mainRsPath, "utf8"); + + assert.match(mainSource, /target_os = "macos"[\s\S]*"Ctrl\+Cmd\+F"/); + assert.match(mainSource, /not\(target_os = "macos"\)[\s\S]*"F11"/); +}); diff --git a/apps/desktop/src-tauri/Cargo.lock b/apps/desktop/src-tauri/Cargo.lock index 6ec03c3d91..1d7bb8711f 100644 --- a/apps/desktop/src-tauri/Cargo.lock +++ b/apps/desktop/src-tauri/Cargo.lock @@ -47,6 +47,15 @@ version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" +[[package]] +name = "arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" +dependencies = [ + "derive_arbitrary", +] + [[package]] name = "async-broadcast" version = "0.7.2" @@ -444,6 +453,23 @@ 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 0.10.1", +] + [[package]] name = "chrono" version = "0.4.45" @@ -540,6 +566,15 @@ 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" @@ -674,6 +709,17 @@ dependencies = [ "serde_core", ] +[[package]] +name = "derive_arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + [[package]] name = "derive_more" version = "0.99.20" @@ -960,6 +1006,16 @@ dependencies = [ "rustc_version", ] +[[package]] +name = "filetime" +version = "0.2.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c287a33c7f0a620c38e641e7f60827713987b3c0f26e8ddc9462cc69cf75759" +dependencies = [ + "cfg-if", + "libc", +] + [[package]] name = "find-msvc-tools" version = "0.1.9" @@ -1254,8 +1310,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" dependencies = [ "cfg-if", + "js-sys", "libc", "wasi 0.11.1+wasi-snapshot-preview1", + "wasm-bindgen", ] [[package]] @@ -1277,8 +1335,11 @@ 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 0.10.1", + "wasm-bindgen", ] [[package]] @@ -1546,6 +1607,22 @@ dependencies = [ "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" @@ -1664,6 +1741,25 @@ version = "2.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" +[[package]] +name = "is-docker" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "928bae27f42bc99b60d9ac7334e3a21d10ad8f1835a4e12ec3ec0464765ed1b3" +dependencies = [ + "once_cell", +] + +[[package]] +name = "is-wsl" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "173609498df190136aa7dea1a91db051746d339e18476eed5ca40521f02d7aa5" +dependencies = [ + "is-docker", + "once_cell", +] + [[package]] name = "itoa" version = "1.0.18" @@ -1774,17 +1870,25 @@ dependencies = [ name = "kandev-desktop" version = "0.78.0" dependencies = [ + "base64 0.22.1", "darling", "getrandom 0.3.4", "idna_adapter", "libc", + "minisign-verify", + "notify-rust", "plist", + "serde", "serde_json", "serde_with", "tauri", "tauri-build", + "tauri-plugin-notification", + "tauri-plugin-opener", "tauri-plugin-single-instance", + "tauri-plugin-updater", "tauri-runtime", + "url", ] [[package]] @@ -1880,12 +1984,32 @@ 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 = "mac" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" +[[package]] +name = "mac-notification-sys" +version = "0.6.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd604973958ddcc11b561193c0fb96ba146506ef2f231ef2e7c35fd2cbc9beca" +dependencies = [ + "cc", + "log", + "objc2", + "objc2-foundation", + "time", + "uuid", +] + [[package]] name = "markup5ever" version = "0.14.1" @@ -1949,6 +2073,12 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" +[[package]] +name = "minisign-verify" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22f9645cb765ea72b8111f36c522475d2daa0d22c957a9826437e97534bc4e9e" + [[package]] name = "miniz_oxide" version = "0.8.9" @@ -2033,6 +2163,20 @@ version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb" +[[package]] +name = "notify-rust" +version = "4.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50ff2e74231b72c832d82982193b417f230945be6bdb5575b251d941d31adb00" +dependencies = [ + "futures-lite", + "log", + "mac-notification-sys", + "serde", + "tauri-winrt-notification", + "zbus", +] + [[package]] name = "num-conv" version = "0.2.2" @@ -2231,6 +2375,18 @@ dependencies = [ "objc2-core-foundation", ] +[[package]] +name = "objc2-osa-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f112d1746737b0da274ef79a23aac283376f335f4095a083a267a082f21db0c0" +dependencies = [ + "bitflags 2.13.0", + "objc2", + "objc2-app-kit", + "objc2-foundation", +] + [[package]] name = "objc2-quartz-core" version = "0.3.2" @@ -2288,6 +2444,17 @@ version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" +[[package]] +name = "open" +version = "5.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0b3d059e795d52b8a72fef45658620edd4d9c359b338564aa14391ffa511ed5" +dependencies = [ + "dunce", + "is-wsl", + "libc", +] + [[package]] name = "option-ext" version = "0.2.0" @@ -2304,6 +2471,20 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "osakit" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "732c71caeaa72c065bb69d7ea08717bd3f4863a4f451402fc9513e29dbd5261b" +dependencies = [ + "objc2", + "objc2-foundation", + "objc2-osa-kit", + "serde", + "serde_json", + "thiserror 2.0.18", +] + [[package]] name = "pango" version = "0.18.3" @@ -2698,6 +2879,62 @@ 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 0.10.2", + "rand_pcg 0.10.2", + "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" @@ -2730,7 +2967,7 @@ dependencies = [ "rand_chacha 0.2.2", "rand_core 0.5.1", "rand_hc", - "rand_pcg", + "rand_pcg 0.2.1", ] [[package]] @@ -2744,6 +2981,27 @@ dependencies = [ "rand_core 0.6.4", ] +[[package]] +name = "rand" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9ef1d0d795eb7d84685bca4f72f3649f064e6641543d3a8c415898726a57b41" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.5", +] + +[[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 0.10.1", +] + [[package]] name = "rand_chacha" version = "0.2.2" @@ -2764,6 +3022,16 @@ dependencies = [ "rand_core 0.6.4", ] +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.5", +] + [[package]] name = "rand_core" version = "0.5.1" @@ -2782,6 +3050,21 @@ dependencies = [ "getrandom 0.2.17", ] +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + +[[package]] +name = "rand_core" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" + [[package]] name = "rand_hc" version = "0.2.0" @@ -2800,6 +3083,15 @@ dependencies = [ "rand_core 0.5.1", ] +[[package]] +name = "rand_pcg" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caa0f4137e1c0a72f4c651489402276c8e8e1cf081f3b0ba156d2cbeef09e86a" +dependencies = [ + "rand_core 0.10.1", +] + [[package]] name = "raw-window-handle" version = "0.6.2" @@ -2889,16 +3181,21 @@ dependencies = [ "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", "tokio-util", "tower", "tower-http", @@ -2908,6 +3205,21 @@ dependencies = [ "wasm-bindgen-futures", "wasm-streams", "web-sys", + "webpki-roots", +] + +[[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]] @@ -2938,6 +3250,41 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "rustls" +version = "0.23.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c54fcab019b409d04215d3a17cb438fd7fbf192ee61461f20f4fe18704bc138" +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" @@ -3249,7 +3596,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.2.17", "digest", ] @@ -3418,6 +3765,12 @@ 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" @@ -3522,6 +3875,17 @@ dependencies = [ "syn 2.0.118", ] +[[package]] +name = "tar" +version = "0.4.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f6221d9a6003c78398e3b239969f352578258df48c8eb051caadae0015bc840" +dependencies = [ + "filetime", + "libc", + "xattr", +] + [[package]] name = "target-lexicon" version = "0.12.16" @@ -3643,6 +4007,63 @@ dependencies = [ "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-notification" +version = "2.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01fc2c5ff41105bd1f7242d8201fdf3efd70749b82fa013a17f2126357d194cc" +dependencies = [ + "log", + "notify-rust", + "rand 0.9.5", + "serde", + "serde_json", + "serde_repr", + "tauri", + "tauri-plugin", + "thiserror 2.0.18", + "time", + "url", +] + +[[package]] +name = "tauri-plugin-opener" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecee219f11cdac713ab32959db5d0cceec4810ba4f4458da992292ecf9660321" +dependencies = [ + "dunce", + "glob", + "objc2-app-kit", + "objc2-foundation", + "open", + "schemars 0.8.22", + "serde", + "serde_json", + "tauri", + "tauri-plugin", + "thiserror 2.0.18", + "url", + "windows", + "zbus", +] + [[package]] name = "tauri-plugin-single-instance" version = "2.2.1" @@ -3658,6 +4079,38 @@ dependencies = [ "zbus", ] +[[package]] +name = "tauri-plugin-updater" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27cbc31740f4d507712550694749572ec0e43bdd66992db7599b89fbfd6b167b" +dependencies = [ + "base64 0.22.1", + "dirs", + "flate2", + "futures-util", + "http", + "infer", + "log", + "minisign-verify", + "osakit", + "percent-encoding", + "reqwest", + "semver", + "serde", + "serde_json", + "tar", + "tauri", + "tauri-plugin", + "tempfile", + "thiserror 2.0.18", + "time", + "tokio", + "url", + "windows-sys 0.60.2", + "zip", +] + [[package]] name = "tauri-runtime" version = "2.9.0" @@ -3761,6 +4214,17 @@ dependencies = [ "toml 1.1.2+spec-1.1.0", ] +[[package]] +name = "tauri-winrt-notification" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed071c670382e85fc2f48ae706492d8c338f4f89bf72520d32f8abfe880aade" +dependencies = [ + "thiserror 2.0.18", + "windows", + "windows-version", +] + [[package]] name = "tempfile" version = "3.27.0" @@ -3865,6 +4329,21 @@ dependencies = [ "time-core", ] +[[package]] +name = "tinyvec" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb4ebadaa0af04fab11ae01eb5f9fdb5f9c5b875506e210e71c07873528baa7f" +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" @@ -3879,6 +4358,16 @@ dependencies = [ "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" @@ -4192,6 +4681,12 @@ 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" @@ -4391,6 +4886,16 @@ dependencies = [ "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" @@ -4447,6 +4952,15 @@ dependencies = [ "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" @@ -4677,6 +5191,15 @@ 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" @@ -5017,6 +5540,16 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "xattr" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156" +dependencies = [ + "libc", + "rustix", +] + [[package]] name = "zbus" version = "5.13.2" @@ -5098,6 +5631,24 @@ dependencies = [ "syn 2.0.118", ] +[[package]] +name = "zeroize" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" + +[[package]] +name = "zip" +version = "4.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caa8cd6af31c3b31c6631b8f483848b91589021b28fffe50adada48d4f4d2ed1" +dependencies = [ + "arbitrary", + "crc32fast", + "indexmap 2.14.0", + "memchr", +] + [[package]] name = "zmij" version = "1.0.21" diff --git a/apps/desktop/src-tauri/Cargo.toml b/apps/desktop/src-tauri/Cargo.toml index c24f28aea2..cfad296d58 100644 --- a/apps/desktop/src-tauri/Cargo.toml +++ b/apps/desktop/src-tauri/Cargo.toml @@ -14,10 +14,17 @@ path = "src/main.rs" required-features = ["desktop-runtime"] test = false +[[bin]] +name = "updater-signature-verifier" +path = "src/bin/updater_signature_verifier.rs" + [features] default = [] desktop-runtime = [ "dep:tauri", + "dep:tauri-plugin-opener", + "dep:tauri-plugin-updater", + "dep:tauri-plugin-notification", "dep:tauri-plugin-single-instance", "dep:tauri-runtime", ] @@ -26,13 +33,20 @@ desktop-runtime = [ tauri-build = { version = "=2.4.1", features = [] } [dependencies] +base64 = "0.22" darling = "=0.21.3" getrandom = "0.3" idna_adapter = "=1.0.0" libc = "0.2" +minisign-verify = "=0.2.5" plist = "=1.10.0" serde_json = "1" +serde = { version = "1", features = ["derive"] } serde_with = "=3.15.0" tauri = { version = "=2.8.4", features = [], optional = true } tauri-runtime = { version = "=2.9.0", optional = true } tauri-plugin-single-instance = { version = "=2.2.1", optional = true } +tauri-plugin-updater = { version = "=2.9.0", optional = true } +tauri-plugin-notification = { version = "=2.3.3", optional = true } +tauri-plugin-opener = { version = "=2.4.0", optional = true } +url = "2" diff --git a/apps/desktop/src-tauri/build.rs b/apps/desktop/src-tauri/build.rs index 988efc4453..8da570a386 100644 --- a/apps/desktop/src-tauri/build.rs +++ b/apps/desktop/src-tauri/build.rs @@ -1,5 +1,13 @@ fn main() { if std::env::var_os("CARGO_FEATURE_DESKTOP_RUNTIME").is_some() { - tauri_build::build(); + let manifest = tauri_build::AppManifest::new().commands(&[ + "get_update_state", + "check_for_updates", + "install_update", + "show_native_notification", + "open_external_url", + ]); + tauri_build::try_build(tauri_build::Attributes::new().app_manifest(manifest)) + .expect("failed to build Tauri desktop application"); } } diff --git a/apps/desktop/src-tauri/capabilities/default.json b/apps/desktop/src-tauri/capabilities/default.json index 6e3d2e8c66..0959200d58 100644 --- a/apps/desktop/src-tauri/capabilities/default.json +++ b/apps/desktop/src-tauri/capabilities/default.json @@ -1,7 +1,19 @@ { "$schema": "../gen/schemas/desktop-schema.json", "identifier": "default", - "description": "Main Kandev desktop window permissions", + "description": "Desktop bridge; privileged commands verify the exact owned backend origin", + "local": false, + "remote": { + "urls": ["http://127.0.0.1:*"] + }, "windows": ["main"], - "permissions": ["core:app:default", "core:event:default", "core:window:default"] + "permissions": [ + "core:event:allow-listen", + "core:event:allow-unlisten", + "allow-get-update-state", + "allow-check-for-updates", + "allow-install-update", + "allow-show-native-notification", + "allow-open-external-url" + ] } diff --git a/apps/desktop/src-tauri/gen/schemas/acl-manifests.json b/apps/desktop/src-tauri/gen/schemas/acl-manifests.json index 3c237c9aef..d6917fb0a0 100644 --- a/apps/desktop/src-tauri/gen/schemas/acl-manifests.json +++ b/apps/desktop/src-tauri/gen/schemas/acl-manifests.json @@ -1 +1 @@ -{"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"]},"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-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-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-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-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-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-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-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-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-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-internal-toggle-maximize"]},"permissions":{"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-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-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-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}} \ No newline at end of file +{"__app-acl__":{"default_permission":null,"permissions":{"allow-check-for-updates":{"identifier":"allow-check-for-updates","description":"Enables the check_for_updates command without any pre-configured scope.","commands":{"allow":["check_for_updates"],"deny":[]}},"allow-get-update-state":{"identifier":"allow-get-update-state","description":"Enables the get_update_state command without any pre-configured scope.","commands":{"allow":["get_update_state"],"deny":[]}},"allow-install-update":{"identifier":"allow-install-update","description":"Enables the install_update command without any pre-configured scope.","commands":{"allow":["install_update"],"deny":[]}},"allow-open-external-url":{"identifier":"allow-open-external-url","description":"Enables the open_external_url command without any pre-configured scope.","commands":{"allow":["open_external_url"],"deny":[]}},"allow-show-native-notification":{"identifier":"allow-show-native-notification","description":"Enables the show_native_notification command without any pre-configured scope.","commands":{"allow":["show_native_notification"],"deny":[]}},"deny-check-for-updates":{"identifier":"deny-check-for-updates","description":"Denies the check_for_updates command without any pre-configured scope.","commands":{"allow":[],"deny":["check_for_updates"]}},"deny-get-update-state":{"identifier":"deny-get-update-state","description":"Denies the get_update_state command without any pre-configured scope.","commands":{"allow":[],"deny":["get_update_state"]}},"deny-install-update":{"identifier":"deny-install-update","description":"Denies the install_update command without any pre-configured scope.","commands":{"allow":[],"deny":["install_update"]}},"deny-open-external-url":{"identifier":"deny-open-external-url","description":"Denies the open_external_url command without any pre-configured scope.","commands":{"allow":[],"deny":["open_external_url"]}},"deny-show-native-notification":{"identifier":"deny-show-native-notification","description":"Denies the show_native_notification command without any pre-configured scope.","commands":{"allow":[],"deny":["show_native_notification"]}}},"permission_sets":{},"global_scope_schema":null},"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"]},"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-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-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-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-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-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-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-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-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-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-internal-toggle-maximize"]},"permissions":{"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-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-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-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},"notification":{"default_permission":{"identifier":"default","description":"This permission set configures which\nnotification features are by default exposed.\n\n#### Granted Permissions\n\nIt allows all notification related features.\n\n","permissions":["allow-is-permission-granted","allow-request-permission","allow-notify","allow-register-action-types","allow-register-listener","allow-cancel","allow-get-pending","allow-remove-active","allow-get-active","allow-check-permissions","allow-show","allow-batch","allow-list-channels","allow-delete-channel","allow-create-channel","allow-permission-state"]},"permissions":{"allow-batch":{"identifier":"allow-batch","description":"Enables the batch command without any pre-configured scope.","commands":{"allow":["batch"],"deny":[]}},"allow-cancel":{"identifier":"allow-cancel","description":"Enables the cancel command without any pre-configured scope.","commands":{"allow":["cancel"],"deny":[]}},"allow-check-permissions":{"identifier":"allow-check-permissions","description":"Enables the check_permissions command without any pre-configured scope.","commands":{"allow":["check_permissions"],"deny":[]}},"allow-create-channel":{"identifier":"allow-create-channel","description":"Enables the create_channel command without any pre-configured scope.","commands":{"allow":["create_channel"],"deny":[]}},"allow-delete-channel":{"identifier":"allow-delete-channel","description":"Enables the delete_channel command without any pre-configured scope.","commands":{"allow":["delete_channel"],"deny":[]}},"allow-get-active":{"identifier":"allow-get-active","description":"Enables the get_active command without any pre-configured scope.","commands":{"allow":["get_active"],"deny":[]}},"allow-get-pending":{"identifier":"allow-get-pending","description":"Enables the get_pending command without any pre-configured scope.","commands":{"allow":["get_pending"],"deny":[]}},"allow-is-permission-granted":{"identifier":"allow-is-permission-granted","description":"Enables the is_permission_granted command without any pre-configured scope.","commands":{"allow":["is_permission_granted"],"deny":[]}},"allow-list-channels":{"identifier":"allow-list-channels","description":"Enables the list_channels command without any pre-configured scope.","commands":{"allow":["list_channels"],"deny":[]}},"allow-notify":{"identifier":"allow-notify","description":"Enables the notify command without any pre-configured scope.","commands":{"allow":["notify"],"deny":[]}},"allow-permission-state":{"identifier":"allow-permission-state","description":"Enables the permission_state command without any pre-configured scope.","commands":{"allow":["permission_state"],"deny":[]}},"allow-register-action-types":{"identifier":"allow-register-action-types","description":"Enables the register_action_types command without any pre-configured scope.","commands":{"allow":["register_action_types"],"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-active":{"identifier":"allow-remove-active","description":"Enables the remove_active command without any pre-configured scope.","commands":{"allow":["remove_active"],"deny":[]}},"allow-request-permission":{"identifier":"allow-request-permission","description":"Enables the request_permission command without any pre-configured scope.","commands":{"allow":["request_permission"],"deny":[]}},"allow-show":{"identifier":"allow-show","description":"Enables the show command without any pre-configured scope.","commands":{"allow":["show"],"deny":[]}},"deny-batch":{"identifier":"deny-batch","description":"Denies the batch command without any pre-configured scope.","commands":{"allow":[],"deny":["batch"]}},"deny-cancel":{"identifier":"deny-cancel","description":"Denies the cancel command without any pre-configured scope.","commands":{"allow":[],"deny":["cancel"]}},"deny-check-permissions":{"identifier":"deny-check-permissions","description":"Denies the check_permissions command without any pre-configured scope.","commands":{"allow":[],"deny":["check_permissions"]}},"deny-create-channel":{"identifier":"deny-create-channel","description":"Denies the create_channel command without any pre-configured scope.","commands":{"allow":[],"deny":["create_channel"]}},"deny-delete-channel":{"identifier":"deny-delete-channel","description":"Denies the delete_channel command without any pre-configured scope.","commands":{"allow":[],"deny":["delete_channel"]}},"deny-get-active":{"identifier":"deny-get-active","description":"Denies the get_active command without any pre-configured scope.","commands":{"allow":[],"deny":["get_active"]}},"deny-get-pending":{"identifier":"deny-get-pending","description":"Denies the get_pending command without any pre-configured scope.","commands":{"allow":[],"deny":["get_pending"]}},"deny-is-permission-granted":{"identifier":"deny-is-permission-granted","description":"Denies the is_permission_granted command without any pre-configured scope.","commands":{"allow":[],"deny":["is_permission_granted"]}},"deny-list-channels":{"identifier":"deny-list-channels","description":"Denies the list_channels command without any pre-configured scope.","commands":{"allow":[],"deny":["list_channels"]}},"deny-notify":{"identifier":"deny-notify","description":"Denies the notify command without any pre-configured scope.","commands":{"allow":[],"deny":["notify"]}},"deny-permission-state":{"identifier":"deny-permission-state","description":"Denies the permission_state command without any pre-configured scope.","commands":{"allow":[],"deny":["permission_state"]}},"deny-register-action-types":{"identifier":"deny-register-action-types","description":"Denies the register_action_types command without any pre-configured scope.","commands":{"allow":[],"deny":["register_action_types"]}},"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-active":{"identifier":"deny-remove-active","description":"Denies the remove_active command without any pre-configured scope.","commands":{"allow":[],"deny":["remove_active"]}},"deny-request-permission":{"identifier":"deny-request-permission","description":"Denies the request_permission command without any pre-configured scope.","commands":{"allow":[],"deny":["request_permission"]}},"deny-show":{"identifier":"deny-show","description":"Denies the show command without any pre-configured scope.","commands":{"allow":[],"deny":["show"]}}},"permission_sets":{},"global_scope_schema":null},"opener":{"default_permission":{"identifier":"default","description":"This permission set allows opening `mailto:`, `tel:`, `https://` and `http://` urls using their default application\nas well as reveal file in directories using default file explorer","permissions":["allow-open-url","allow-reveal-item-in-dir","allow-default-urls"]},"permissions":{"allow-default-urls":{"identifier":"allow-default-urls","description":"This enables opening `mailto:`, `tel:`, `https://` and `http://` urls using their default application.","commands":{"allow":[],"deny":[]},"scope":{"allow":[{"url":"mailto:*"},{"url":"tel:*"},{"url":"http://*"},{"url":"https://*"}]}},"allow-open-path":{"identifier":"allow-open-path","description":"Enables the open_path command without any pre-configured scope.","commands":{"allow":["open_path"],"deny":[]}},"allow-open-url":{"identifier":"allow-open-url","description":"Enables the open_url command without any pre-configured scope.","commands":{"allow":["open_url"],"deny":[]}},"allow-reveal-item-in-dir":{"identifier":"allow-reveal-item-in-dir","description":"Enables the reveal_item_in_dir command without any pre-configured scope.","commands":{"allow":["reveal_item_in_dir"],"deny":[]}},"deny-open-path":{"identifier":"deny-open-path","description":"Denies the open_path command without any pre-configured scope.","commands":{"allow":[],"deny":["open_path"]}},"deny-open-url":{"identifier":"deny-open-url","description":"Denies the open_url command without any pre-configured scope.","commands":{"allow":[],"deny":["open_url"]}},"deny-reveal-item-in-dir":{"identifier":"deny-reveal-item-in-dir","description":"Denies the reveal_item_in_dir command without any pre-configured scope.","commands":{"allow":[],"deny":["reveal_item_in_dir"]}}},"permission_sets":{},"global_scope_schema":{"$schema":"http://json-schema.org/draft-07/schema#","anyOf":[{"properties":{"app":{"allOf":[{"$ref":"#/definitions/Application"}],"description":"An application to open this url with, for example: firefox."},"url":{"description":"A URL that can be opened by the webview when using the Opener APIs.\n\nWildcards can be used following the UNIX glob pattern.\n\nExamples:\n\n- \"https://*\" : allows all HTTPS origin\n\n- \"https://*.github.com/tauri-apps/tauri\": allows any subdomain of \"github.com\" with the \"tauri-apps/api\" path\n\n- \"https://myapi.service.com/users/*\": allows access to any URLs that begins with \"https://myapi.service.com/users/\"","type":"string"}},"required":["url"],"type":"object"},{"properties":{"app":{"allOf":[{"$ref":"#/definitions/Application"}],"description":"An application to open this path with, for example: xdg-open."},"path":{"description":"A path that can be opened by the webview when using the Opener APIs.\n\nThe pattern can start with a variable that resolves to a system base directory. The variables are: `$AUDIO`, `$CACHE`, `$CONFIG`, `$DATA`, `$LOCALDATA`, `$DESKTOP`, `$DOCUMENT`, `$DOWNLOAD`, `$EXE`, `$FONT`, `$HOME`, `$PICTURE`, `$PUBLIC`, `$RUNTIME`, `$TEMPLATE`, `$VIDEO`, `$RESOURCE`, `$APP`, `$LOG`, `$TEMP`, `$APPCONFIG`, `$APPDATA`, `$APPLOCALDATA`, `$APPCACHE`, `$APPLOG`.","type":"string"}},"required":["path"],"type":"object"}],"definitions":{"Application":{"anyOf":[{"description":"Open in default application.","type":"null"},{"description":"If true, allow open with any application.","type":"boolean"},{"description":"Allow specific application to open with.","type":"string"}],"description":"Opener scope application."}},"description":"Opener scope entry.","title":"OpenerScopeEntry"}},"updater":{"default_permission":{"identifier":"default","description":"This permission set configures which kind of\nupdater functions are exposed to the frontend.\n\n#### Granted Permissions\n\nThe full workflow from checking for updates to installing them\nis enabled.\n\n","permissions":["allow-check","allow-download","allow-install","allow-download-and-install"]},"permissions":{"allow-check":{"identifier":"allow-check","description":"Enables the check command without any pre-configured scope.","commands":{"allow":["check"],"deny":[]}},"allow-download":{"identifier":"allow-download","description":"Enables the download command without any pre-configured scope.","commands":{"allow":["download"],"deny":[]}},"allow-download-and-install":{"identifier":"allow-download-and-install","description":"Enables the download_and_install command without any pre-configured scope.","commands":{"allow":["download_and_install"],"deny":[]}},"allow-install":{"identifier":"allow-install","description":"Enables the install command without any pre-configured scope.","commands":{"allow":["install"],"deny":[]}},"deny-check":{"identifier":"deny-check","description":"Denies the check command without any pre-configured scope.","commands":{"allow":[],"deny":["check"]}},"deny-download":{"identifier":"deny-download","description":"Denies the download command without any pre-configured scope.","commands":{"allow":[],"deny":["download"]}},"deny-download-and-install":{"identifier":"deny-download-and-install","description":"Denies the download_and_install command without any pre-configured scope.","commands":{"allow":[],"deny":["download_and_install"]}},"deny-install":{"identifier":"deny-install","description":"Denies the install command without any pre-configured scope.","commands":{"allow":[],"deny":["install"]}}},"permission_sets":{},"global_scope_schema":null}} \ No newline at end of file diff --git a/apps/desktop/src-tauri/gen/schemas/capabilities.json b/apps/desktop/src-tauri/gen/schemas/capabilities.json index 27e3b53b03..3367a8ce86 100644 --- a/apps/desktop/src-tauri/gen/schemas/capabilities.json +++ b/apps/desktop/src-tauri/gen/schemas/capabilities.json @@ -1 +1 @@ -{"default":{"identifier":"default","description":"Main Kandev desktop window permissions","local":true,"windows":["main"],"permissions":["core:app:default","core:event:default","core:window:default"]}} \ No newline at end of file +{"default":{"identifier":"default","description":"Desktop bridge; privileged commands verify the exact owned backend origin","remote":{"urls":["http://127.0.0.1:*"]},"local":false,"windows":["main"],"permissions":["core:event:allow-listen","core:event:allow-unlisten","allow-get-update-state","allow-check-for-updates","allow-install-update","allow-show-native-notification","allow-open-external-url"]}} \ No newline at end of file diff --git a/apps/desktop/src-tauri/gen/schemas/desktop-schema.json b/apps/desktop/src-tauri/gen/schemas/desktop-schema.json index b01bfa931d..45ff29b8f1 100644 --- a/apps/desktop/src-tauri/gen/schemas/desktop-schema.json +++ b/apps/desktop/src-tauri/gen/schemas/desktop-schema.json @@ -134,6 +134,174 @@ "description": "Reference a permission or permission set by identifier and extends its scope.", "type": "object", "allOf": [ + { + "if": { + "properties": { + "identifier": { + "anyOf": [ + { + "description": "This permission set allows opening `mailto:`, `tel:`, `https://` and `http://` urls using their default application\nas well as reveal file in directories using default file explorer\n#### This default permission set includes:\n\n- `allow-open-url`\n- `allow-reveal-item-in-dir`\n- `allow-default-urls`", + "type": "string", + "const": "opener:default", + "markdownDescription": "This permission set allows opening `mailto:`, `tel:`, `https://` and `http://` urls using their default application\nas well as reveal file in directories using default file explorer\n#### This default permission set includes:\n\n- `allow-open-url`\n- `allow-reveal-item-in-dir`\n- `allow-default-urls`" + }, + { + "description": "This enables opening `mailto:`, `tel:`, `https://` and `http://` urls using their default application.", + "type": "string", + "const": "opener:allow-default-urls", + "markdownDescription": "This enables opening `mailto:`, `tel:`, `https://` and `http://` urls using their default application." + }, + { + "description": "Enables the open_path command without any pre-configured scope.", + "type": "string", + "const": "opener:allow-open-path", + "markdownDescription": "Enables the open_path command without any pre-configured scope." + }, + { + "description": "Enables the open_url command without any pre-configured scope.", + "type": "string", + "const": "opener:allow-open-url", + "markdownDescription": "Enables the open_url command without any pre-configured scope." + }, + { + "description": "Enables the reveal_item_in_dir command without any pre-configured scope.", + "type": "string", + "const": "opener:allow-reveal-item-in-dir", + "markdownDescription": "Enables the reveal_item_in_dir command without any pre-configured scope." + }, + { + "description": "Denies the open_path command without any pre-configured scope.", + "type": "string", + "const": "opener:deny-open-path", + "markdownDescription": "Denies the open_path command without any pre-configured scope." + }, + { + "description": "Denies the open_url command without any pre-configured scope.", + "type": "string", + "const": "opener:deny-open-url", + "markdownDescription": "Denies the open_url command without any pre-configured scope." + }, + { + "description": "Denies the reveal_item_in_dir command without any pre-configured scope.", + "type": "string", + "const": "opener:deny-reveal-item-in-dir", + "markdownDescription": "Denies the reveal_item_in_dir command without any pre-configured scope." + } + ] + } + } + }, + "then": { + "properties": { + "allow": { + "items": { + "title": "OpenerScopeEntry", + "description": "Opener scope entry.", + "anyOf": [ + { + "type": "object", + "required": [ + "url" + ], + "properties": { + "app": { + "description": "An application to open this url with, for example: firefox.", + "allOf": [ + { + "$ref": "#/definitions/Application" + } + ] + }, + "url": { + "description": "A URL that can be opened by the webview when using the Opener APIs.\n\nWildcards can be used following the UNIX glob pattern.\n\nExamples:\n\n- \"https://*\" : allows all HTTPS origin\n\n- \"https://*.github.com/tauri-apps/tauri\": allows any subdomain of \"github.com\" with the \"tauri-apps/api\" path\n\n- \"https://myapi.service.com/users/*\": allows access to any URLs that begins with \"https://myapi.service.com/users/\"", + "type": "string" + } + } + }, + { + "type": "object", + "required": [ + "path" + ], + "properties": { + "app": { + "description": "An application to open this path with, for example: xdg-open.", + "allOf": [ + { + "$ref": "#/definitions/Application" + } + ] + }, + "path": { + "description": "A path that can be opened by the webview when using the Opener APIs.\n\nThe pattern can start with a variable that resolves to a system base directory. The variables are: `$AUDIO`, `$CACHE`, `$CONFIG`, `$DATA`, `$LOCALDATA`, `$DESKTOP`, `$DOCUMENT`, `$DOWNLOAD`, `$EXE`, `$FONT`, `$HOME`, `$PICTURE`, `$PUBLIC`, `$RUNTIME`, `$TEMPLATE`, `$VIDEO`, `$RESOURCE`, `$APP`, `$LOG`, `$TEMP`, `$APPCONFIG`, `$APPDATA`, `$APPLOCALDATA`, `$APPCACHE`, `$APPLOG`.", + "type": "string" + } + } + } + ] + } + }, + "deny": { + "items": { + "title": "OpenerScopeEntry", + "description": "Opener scope entry.", + "anyOf": [ + { + "type": "object", + "required": [ + "url" + ], + "properties": { + "app": { + "description": "An application to open this url with, for example: firefox.", + "allOf": [ + { + "$ref": "#/definitions/Application" + } + ] + }, + "url": { + "description": "A URL that can be opened by the webview when using the Opener APIs.\n\nWildcards can be used following the UNIX glob pattern.\n\nExamples:\n\n- \"https://*\" : allows all HTTPS origin\n\n- \"https://*.github.com/tauri-apps/tauri\": allows any subdomain of \"github.com\" with the \"tauri-apps/api\" path\n\n- \"https://myapi.service.com/users/*\": allows access to any URLs that begins with \"https://myapi.service.com/users/\"", + "type": "string" + } + } + }, + { + "type": "object", + "required": [ + "path" + ], + "properties": { + "app": { + "description": "An application to open this path with, for example: xdg-open.", + "allOf": [ + { + "$ref": "#/definitions/Application" + } + ] + }, + "path": { + "description": "A path that can be opened by the webview when using the Opener APIs.\n\nThe pattern can start with a variable that resolves to a system base directory. The variables are: `$AUDIO`, `$CACHE`, `$CONFIG`, `$DATA`, `$LOCALDATA`, `$DESKTOP`, `$DOCUMENT`, `$DOWNLOAD`, `$EXE`, `$FONT`, `$HOME`, `$PICTURE`, `$PUBLIC`, `$RUNTIME`, `$TEMPLATE`, `$VIDEO`, `$RESOURCE`, `$APP`, `$LOG`, `$TEMP`, `$APPCONFIG`, `$APPDATA`, `$APPLOCALDATA`, `$APPCACHE`, `$APPLOG`.", + "type": "string" + } + } + } + ] + } + } + } + }, + "properties": { + "identifier": { + "description": "Identifier of the permission or permission set.", + "allOf": [ + { + "$ref": "#/definitions/Identifier" + } + ] + } + } + }, { "properties": { "identifier": { @@ -176,6 +344,66 @@ "Identifier": { "description": "Permission identifier", "oneOf": [ + { + "description": "Enables the check_for_updates command without any pre-configured scope.", + "type": "string", + "const": "allow-check-for-updates", + "markdownDescription": "Enables the check_for_updates command without any pre-configured scope." + }, + { + "description": "Enables the get_update_state command without any pre-configured scope.", + "type": "string", + "const": "allow-get-update-state", + "markdownDescription": "Enables the get_update_state command without any pre-configured scope." + }, + { + "description": "Enables the install_update command without any pre-configured scope.", + "type": "string", + "const": "allow-install-update", + "markdownDescription": "Enables the install_update command without any pre-configured scope." + }, + { + "description": "Enables the open_external_url command without any pre-configured scope.", + "type": "string", + "const": "allow-open-external-url", + "markdownDescription": "Enables the open_external_url command without any pre-configured scope." + }, + { + "description": "Enables the show_native_notification command without any pre-configured scope.", + "type": "string", + "const": "allow-show-native-notification", + "markdownDescription": "Enables the show_native_notification command without any pre-configured scope." + }, + { + "description": "Denies the check_for_updates command without any pre-configured scope.", + "type": "string", + "const": "deny-check-for-updates", + "markdownDescription": "Denies the check_for_updates command without any pre-configured scope." + }, + { + "description": "Denies the get_update_state command without any pre-configured scope.", + "type": "string", + "const": "deny-get-update-state", + "markdownDescription": "Denies the get_update_state command without any pre-configured scope." + }, + { + "description": "Denies the install_update command without any pre-configured scope.", + "type": "string", + "const": "deny-install-update", + "markdownDescription": "Denies the install_update command without any pre-configured scope." + }, + { + "description": "Denies the open_external_url command without any pre-configured scope.", + "type": "string", + "const": "deny-open-external-url", + "markdownDescription": "Denies the open_external_url command without any pre-configured scope." + }, + { + "description": "Denies the show_native_notification command without any pre-configured scope.", + "type": "string", + "const": "deny-show-native-notification", + "markdownDescription": "Denies the show_native_notification command without any pre-configured scope." + }, { "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", @@ -2119,6 +2347,306 @@ "type": "string", "const": "core:window:deny-unminimize", "markdownDescription": "Denies the unminimize command without any pre-configured scope." + }, + { + "description": "This permission set configures which\nnotification features are by default exposed.\n\n#### Granted Permissions\n\nIt allows all notification related features.\n\n\n#### This default permission set includes:\n\n- `allow-is-permission-granted`\n- `allow-request-permission`\n- `allow-notify`\n- `allow-register-action-types`\n- `allow-register-listener`\n- `allow-cancel`\n- `allow-get-pending`\n- `allow-remove-active`\n- `allow-get-active`\n- `allow-check-permissions`\n- `allow-show`\n- `allow-batch`\n- `allow-list-channels`\n- `allow-delete-channel`\n- `allow-create-channel`\n- `allow-permission-state`", + "type": "string", + "const": "notification:default", + "markdownDescription": "This permission set configures which\nnotification features are by default exposed.\n\n#### Granted Permissions\n\nIt allows all notification related features.\n\n\n#### This default permission set includes:\n\n- `allow-is-permission-granted`\n- `allow-request-permission`\n- `allow-notify`\n- `allow-register-action-types`\n- `allow-register-listener`\n- `allow-cancel`\n- `allow-get-pending`\n- `allow-remove-active`\n- `allow-get-active`\n- `allow-check-permissions`\n- `allow-show`\n- `allow-batch`\n- `allow-list-channels`\n- `allow-delete-channel`\n- `allow-create-channel`\n- `allow-permission-state`" + }, + { + "description": "Enables the batch command without any pre-configured scope.", + "type": "string", + "const": "notification:allow-batch", + "markdownDescription": "Enables the batch command without any pre-configured scope." + }, + { + "description": "Enables the cancel command without any pre-configured scope.", + "type": "string", + "const": "notification:allow-cancel", + "markdownDescription": "Enables the cancel command without any pre-configured scope." + }, + { + "description": "Enables the check_permissions command without any pre-configured scope.", + "type": "string", + "const": "notification:allow-check-permissions", + "markdownDescription": "Enables the check_permissions command without any pre-configured scope." + }, + { + "description": "Enables the create_channel command without any pre-configured scope.", + "type": "string", + "const": "notification:allow-create-channel", + "markdownDescription": "Enables the create_channel command without any pre-configured scope." + }, + { + "description": "Enables the delete_channel command without any pre-configured scope.", + "type": "string", + "const": "notification:allow-delete-channel", + "markdownDescription": "Enables the delete_channel command without any pre-configured scope." + }, + { + "description": "Enables the get_active command without any pre-configured scope.", + "type": "string", + "const": "notification:allow-get-active", + "markdownDescription": "Enables the get_active command without any pre-configured scope." + }, + { + "description": "Enables the get_pending command without any pre-configured scope.", + "type": "string", + "const": "notification:allow-get-pending", + "markdownDescription": "Enables the get_pending command without any pre-configured scope." + }, + { + "description": "Enables the is_permission_granted command without any pre-configured scope.", + "type": "string", + "const": "notification:allow-is-permission-granted", + "markdownDescription": "Enables the is_permission_granted command without any pre-configured scope." + }, + { + "description": "Enables the list_channels command without any pre-configured scope.", + "type": "string", + "const": "notification:allow-list-channels", + "markdownDescription": "Enables the list_channels command without any pre-configured scope." + }, + { + "description": "Enables the notify command without any pre-configured scope.", + "type": "string", + "const": "notification:allow-notify", + "markdownDescription": "Enables the notify command without any pre-configured scope." + }, + { + "description": "Enables the permission_state command without any pre-configured scope.", + "type": "string", + "const": "notification:allow-permission-state", + "markdownDescription": "Enables the permission_state command without any pre-configured scope." + }, + { + "description": "Enables the register_action_types command without any pre-configured scope.", + "type": "string", + "const": "notification:allow-register-action-types", + "markdownDescription": "Enables the register_action_types command without any pre-configured scope." + }, + { + "description": "Enables the register_listener command without any pre-configured scope.", + "type": "string", + "const": "notification:allow-register-listener", + "markdownDescription": "Enables the register_listener command without any pre-configured scope." + }, + { + "description": "Enables the remove_active command without any pre-configured scope.", + "type": "string", + "const": "notification:allow-remove-active", + "markdownDescription": "Enables the remove_active command without any pre-configured scope." + }, + { + "description": "Enables the request_permission command without any pre-configured scope.", + "type": "string", + "const": "notification:allow-request-permission", + "markdownDescription": "Enables the request_permission command without any pre-configured scope." + }, + { + "description": "Enables the show command without any pre-configured scope.", + "type": "string", + "const": "notification:allow-show", + "markdownDescription": "Enables the show command without any pre-configured scope." + }, + { + "description": "Denies the batch command without any pre-configured scope.", + "type": "string", + "const": "notification:deny-batch", + "markdownDescription": "Denies the batch command without any pre-configured scope." + }, + { + "description": "Denies the cancel command without any pre-configured scope.", + "type": "string", + "const": "notification:deny-cancel", + "markdownDescription": "Denies the cancel command without any pre-configured scope." + }, + { + "description": "Denies the check_permissions command without any pre-configured scope.", + "type": "string", + "const": "notification:deny-check-permissions", + "markdownDescription": "Denies the check_permissions command without any pre-configured scope." + }, + { + "description": "Denies the create_channel command without any pre-configured scope.", + "type": "string", + "const": "notification:deny-create-channel", + "markdownDescription": "Denies the create_channel command without any pre-configured scope." + }, + { + "description": "Denies the delete_channel command without any pre-configured scope.", + "type": "string", + "const": "notification:deny-delete-channel", + "markdownDescription": "Denies the delete_channel command without any pre-configured scope." + }, + { + "description": "Denies the get_active command without any pre-configured scope.", + "type": "string", + "const": "notification:deny-get-active", + "markdownDescription": "Denies the get_active command without any pre-configured scope." + }, + { + "description": "Denies the get_pending command without any pre-configured scope.", + "type": "string", + "const": "notification:deny-get-pending", + "markdownDescription": "Denies the get_pending command without any pre-configured scope." + }, + { + "description": "Denies the is_permission_granted command without any pre-configured scope.", + "type": "string", + "const": "notification:deny-is-permission-granted", + "markdownDescription": "Denies the is_permission_granted command without any pre-configured scope." + }, + { + "description": "Denies the list_channels command without any pre-configured scope.", + "type": "string", + "const": "notification:deny-list-channels", + "markdownDescription": "Denies the list_channels command without any pre-configured scope." + }, + { + "description": "Denies the notify command without any pre-configured scope.", + "type": "string", + "const": "notification:deny-notify", + "markdownDescription": "Denies the notify command without any pre-configured scope." + }, + { + "description": "Denies the permission_state command without any pre-configured scope.", + "type": "string", + "const": "notification:deny-permission-state", + "markdownDescription": "Denies the permission_state command without any pre-configured scope." + }, + { + "description": "Denies the register_action_types command without any pre-configured scope.", + "type": "string", + "const": "notification:deny-register-action-types", + "markdownDescription": "Denies the register_action_types command without any pre-configured scope." + }, + { + "description": "Denies the register_listener command without any pre-configured scope.", + "type": "string", + "const": "notification:deny-register-listener", + "markdownDescription": "Denies the register_listener command without any pre-configured scope." + }, + { + "description": "Denies the remove_active command without any pre-configured scope.", + "type": "string", + "const": "notification:deny-remove-active", + "markdownDescription": "Denies the remove_active command without any pre-configured scope." + }, + { + "description": "Denies the request_permission command without any pre-configured scope.", + "type": "string", + "const": "notification:deny-request-permission", + "markdownDescription": "Denies the request_permission command without any pre-configured scope." + }, + { + "description": "Denies the show command without any pre-configured scope.", + "type": "string", + "const": "notification:deny-show", + "markdownDescription": "Denies the show command without any pre-configured scope." + }, + { + "description": "This permission set allows opening `mailto:`, `tel:`, `https://` and `http://` urls using their default application\nas well as reveal file in directories using default file explorer\n#### This default permission set includes:\n\n- `allow-open-url`\n- `allow-reveal-item-in-dir`\n- `allow-default-urls`", + "type": "string", + "const": "opener:default", + "markdownDescription": "This permission set allows opening `mailto:`, `tel:`, `https://` and `http://` urls using their default application\nas well as reveal file in directories using default file explorer\n#### This default permission set includes:\n\n- `allow-open-url`\n- `allow-reveal-item-in-dir`\n- `allow-default-urls`" + }, + { + "description": "This enables opening `mailto:`, `tel:`, `https://` and `http://` urls using their default application.", + "type": "string", + "const": "opener:allow-default-urls", + "markdownDescription": "This enables opening `mailto:`, `tel:`, `https://` and `http://` urls using their default application." + }, + { + "description": "Enables the open_path command without any pre-configured scope.", + "type": "string", + "const": "opener:allow-open-path", + "markdownDescription": "Enables the open_path command without any pre-configured scope." + }, + { + "description": "Enables the open_url command without any pre-configured scope.", + "type": "string", + "const": "opener:allow-open-url", + "markdownDescription": "Enables the open_url command without any pre-configured scope." + }, + { + "description": "Enables the reveal_item_in_dir command without any pre-configured scope.", + "type": "string", + "const": "opener:allow-reveal-item-in-dir", + "markdownDescription": "Enables the reveal_item_in_dir command without any pre-configured scope." + }, + { + "description": "Denies the open_path command without any pre-configured scope.", + "type": "string", + "const": "opener:deny-open-path", + "markdownDescription": "Denies the open_path command without any pre-configured scope." + }, + { + "description": "Denies the open_url command without any pre-configured scope.", + "type": "string", + "const": "opener:deny-open-url", + "markdownDescription": "Denies the open_url command without any pre-configured scope." + }, + { + "description": "Denies the reveal_item_in_dir command without any pre-configured scope.", + "type": "string", + "const": "opener:deny-reveal-item-in-dir", + "markdownDescription": "Denies the reveal_item_in_dir command without any pre-configured scope." + }, + { + "description": "This permission set configures which kind of\nupdater functions are exposed to the frontend.\n\n#### Granted Permissions\n\nThe full workflow from checking for updates to installing them\nis enabled.\n\n\n#### This default permission set includes:\n\n- `allow-check`\n- `allow-download`\n- `allow-install`\n- `allow-download-and-install`", + "type": "string", + "const": "updater:default", + "markdownDescription": "This permission set configures which kind of\nupdater functions are exposed to the frontend.\n\n#### Granted Permissions\n\nThe full workflow from checking for updates to installing them\nis enabled.\n\n\n#### This default permission set includes:\n\n- `allow-check`\n- `allow-download`\n- `allow-install`\n- `allow-download-and-install`" + }, + { + "description": "Enables the check command without any pre-configured scope.", + "type": "string", + "const": "updater:allow-check", + "markdownDescription": "Enables the check command without any pre-configured scope." + }, + { + "description": "Enables the download command without any pre-configured scope.", + "type": "string", + "const": "updater:allow-download", + "markdownDescription": "Enables the download command without any pre-configured scope." + }, + { + "description": "Enables the download_and_install command without any pre-configured scope.", + "type": "string", + "const": "updater:allow-download-and-install", + "markdownDescription": "Enables the download_and_install command without any pre-configured scope." + }, + { + "description": "Enables the install command without any pre-configured scope.", + "type": "string", + "const": "updater:allow-install", + "markdownDescription": "Enables the install command without any pre-configured scope." + }, + { + "description": "Denies the check command without any pre-configured scope.", + "type": "string", + "const": "updater:deny-check", + "markdownDescription": "Denies the check command without any pre-configured scope." + }, + { + "description": "Denies the download command without any pre-configured scope.", + "type": "string", + "const": "updater:deny-download", + "markdownDescription": "Denies the download command without any pre-configured scope." + }, + { + "description": "Denies the download_and_install command without any pre-configured scope.", + "type": "string", + "const": "updater:deny-download-and-install", + "markdownDescription": "Denies the download_and_install command without any pre-configured scope." + }, + { + "description": "Denies the install command without any pre-configured scope.", + "type": "string", + "const": "updater:deny-install", + "markdownDescription": "Denies the install command without any pre-configured scope." } ] }, @@ -2215,6 +2743,23 @@ ] } ] + }, + "Application": { + "description": "Opener scope application.", + "anyOf": [ + { + "description": "Open in default application.", + "type": "null" + }, + { + "description": "If true, allow open with any application.", + "type": "boolean" + }, + { + "description": "Allow specific application to open with.", + "type": "string" + } + ] } } } \ No newline at end of file diff --git a/apps/desktop/src-tauri/gen/schemas/linux-schema.json b/apps/desktop/src-tauri/gen/schemas/linux-schema.json index b01bfa931d..45ff29b8f1 100644 --- a/apps/desktop/src-tauri/gen/schemas/linux-schema.json +++ b/apps/desktop/src-tauri/gen/schemas/linux-schema.json @@ -134,6 +134,174 @@ "description": "Reference a permission or permission set by identifier and extends its scope.", "type": "object", "allOf": [ + { + "if": { + "properties": { + "identifier": { + "anyOf": [ + { + "description": "This permission set allows opening `mailto:`, `tel:`, `https://` and `http://` urls using their default application\nas well as reveal file in directories using default file explorer\n#### This default permission set includes:\n\n- `allow-open-url`\n- `allow-reveal-item-in-dir`\n- `allow-default-urls`", + "type": "string", + "const": "opener:default", + "markdownDescription": "This permission set allows opening `mailto:`, `tel:`, `https://` and `http://` urls using their default application\nas well as reveal file in directories using default file explorer\n#### This default permission set includes:\n\n- `allow-open-url`\n- `allow-reveal-item-in-dir`\n- `allow-default-urls`" + }, + { + "description": "This enables opening `mailto:`, `tel:`, `https://` and `http://` urls using their default application.", + "type": "string", + "const": "opener:allow-default-urls", + "markdownDescription": "This enables opening `mailto:`, `tel:`, `https://` and `http://` urls using their default application." + }, + { + "description": "Enables the open_path command without any pre-configured scope.", + "type": "string", + "const": "opener:allow-open-path", + "markdownDescription": "Enables the open_path command without any pre-configured scope." + }, + { + "description": "Enables the open_url command without any pre-configured scope.", + "type": "string", + "const": "opener:allow-open-url", + "markdownDescription": "Enables the open_url command without any pre-configured scope." + }, + { + "description": "Enables the reveal_item_in_dir command without any pre-configured scope.", + "type": "string", + "const": "opener:allow-reveal-item-in-dir", + "markdownDescription": "Enables the reveal_item_in_dir command without any pre-configured scope." + }, + { + "description": "Denies the open_path command without any pre-configured scope.", + "type": "string", + "const": "opener:deny-open-path", + "markdownDescription": "Denies the open_path command without any pre-configured scope." + }, + { + "description": "Denies the open_url command without any pre-configured scope.", + "type": "string", + "const": "opener:deny-open-url", + "markdownDescription": "Denies the open_url command without any pre-configured scope." + }, + { + "description": "Denies the reveal_item_in_dir command without any pre-configured scope.", + "type": "string", + "const": "opener:deny-reveal-item-in-dir", + "markdownDescription": "Denies the reveal_item_in_dir command without any pre-configured scope." + } + ] + } + } + }, + "then": { + "properties": { + "allow": { + "items": { + "title": "OpenerScopeEntry", + "description": "Opener scope entry.", + "anyOf": [ + { + "type": "object", + "required": [ + "url" + ], + "properties": { + "app": { + "description": "An application to open this url with, for example: firefox.", + "allOf": [ + { + "$ref": "#/definitions/Application" + } + ] + }, + "url": { + "description": "A URL that can be opened by the webview when using the Opener APIs.\n\nWildcards can be used following the UNIX glob pattern.\n\nExamples:\n\n- \"https://*\" : allows all HTTPS origin\n\n- \"https://*.github.com/tauri-apps/tauri\": allows any subdomain of \"github.com\" with the \"tauri-apps/api\" path\n\n- \"https://myapi.service.com/users/*\": allows access to any URLs that begins with \"https://myapi.service.com/users/\"", + "type": "string" + } + } + }, + { + "type": "object", + "required": [ + "path" + ], + "properties": { + "app": { + "description": "An application to open this path with, for example: xdg-open.", + "allOf": [ + { + "$ref": "#/definitions/Application" + } + ] + }, + "path": { + "description": "A path that can be opened by the webview when using the Opener APIs.\n\nThe pattern can start with a variable that resolves to a system base directory. The variables are: `$AUDIO`, `$CACHE`, `$CONFIG`, `$DATA`, `$LOCALDATA`, `$DESKTOP`, `$DOCUMENT`, `$DOWNLOAD`, `$EXE`, `$FONT`, `$HOME`, `$PICTURE`, `$PUBLIC`, `$RUNTIME`, `$TEMPLATE`, `$VIDEO`, `$RESOURCE`, `$APP`, `$LOG`, `$TEMP`, `$APPCONFIG`, `$APPDATA`, `$APPLOCALDATA`, `$APPCACHE`, `$APPLOG`.", + "type": "string" + } + } + } + ] + } + }, + "deny": { + "items": { + "title": "OpenerScopeEntry", + "description": "Opener scope entry.", + "anyOf": [ + { + "type": "object", + "required": [ + "url" + ], + "properties": { + "app": { + "description": "An application to open this url with, for example: firefox.", + "allOf": [ + { + "$ref": "#/definitions/Application" + } + ] + }, + "url": { + "description": "A URL that can be opened by the webview when using the Opener APIs.\n\nWildcards can be used following the UNIX glob pattern.\n\nExamples:\n\n- \"https://*\" : allows all HTTPS origin\n\n- \"https://*.github.com/tauri-apps/tauri\": allows any subdomain of \"github.com\" with the \"tauri-apps/api\" path\n\n- \"https://myapi.service.com/users/*\": allows access to any URLs that begins with \"https://myapi.service.com/users/\"", + "type": "string" + } + } + }, + { + "type": "object", + "required": [ + "path" + ], + "properties": { + "app": { + "description": "An application to open this path with, for example: xdg-open.", + "allOf": [ + { + "$ref": "#/definitions/Application" + } + ] + }, + "path": { + "description": "A path that can be opened by the webview when using the Opener APIs.\n\nThe pattern can start with a variable that resolves to a system base directory. The variables are: `$AUDIO`, `$CACHE`, `$CONFIG`, `$DATA`, `$LOCALDATA`, `$DESKTOP`, `$DOCUMENT`, `$DOWNLOAD`, `$EXE`, `$FONT`, `$HOME`, `$PICTURE`, `$PUBLIC`, `$RUNTIME`, `$TEMPLATE`, `$VIDEO`, `$RESOURCE`, `$APP`, `$LOG`, `$TEMP`, `$APPCONFIG`, `$APPDATA`, `$APPLOCALDATA`, `$APPCACHE`, `$APPLOG`.", + "type": "string" + } + } + } + ] + } + } + } + }, + "properties": { + "identifier": { + "description": "Identifier of the permission or permission set.", + "allOf": [ + { + "$ref": "#/definitions/Identifier" + } + ] + } + } + }, { "properties": { "identifier": { @@ -176,6 +344,66 @@ "Identifier": { "description": "Permission identifier", "oneOf": [ + { + "description": "Enables the check_for_updates command without any pre-configured scope.", + "type": "string", + "const": "allow-check-for-updates", + "markdownDescription": "Enables the check_for_updates command without any pre-configured scope." + }, + { + "description": "Enables the get_update_state command without any pre-configured scope.", + "type": "string", + "const": "allow-get-update-state", + "markdownDescription": "Enables the get_update_state command without any pre-configured scope." + }, + { + "description": "Enables the install_update command without any pre-configured scope.", + "type": "string", + "const": "allow-install-update", + "markdownDescription": "Enables the install_update command without any pre-configured scope." + }, + { + "description": "Enables the open_external_url command without any pre-configured scope.", + "type": "string", + "const": "allow-open-external-url", + "markdownDescription": "Enables the open_external_url command without any pre-configured scope." + }, + { + "description": "Enables the show_native_notification command without any pre-configured scope.", + "type": "string", + "const": "allow-show-native-notification", + "markdownDescription": "Enables the show_native_notification command without any pre-configured scope." + }, + { + "description": "Denies the check_for_updates command without any pre-configured scope.", + "type": "string", + "const": "deny-check-for-updates", + "markdownDescription": "Denies the check_for_updates command without any pre-configured scope." + }, + { + "description": "Denies the get_update_state command without any pre-configured scope.", + "type": "string", + "const": "deny-get-update-state", + "markdownDescription": "Denies the get_update_state command without any pre-configured scope." + }, + { + "description": "Denies the install_update command without any pre-configured scope.", + "type": "string", + "const": "deny-install-update", + "markdownDescription": "Denies the install_update command without any pre-configured scope." + }, + { + "description": "Denies the open_external_url command without any pre-configured scope.", + "type": "string", + "const": "deny-open-external-url", + "markdownDescription": "Denies the open_external_url command without any pre-configured scope." + }, + { + "description": "Denies the show_native_notification command without any pre-configured scope.", + "type": "string", + "const": "deny-show-native-notification", + "markdownDescription": "Denies the show_native_notification command without any pre-configured scope." + }, { "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", @@ -2119,6 +2347,306 @@ "type": "string", "const": "core:window:deny-unminimize", "markdownDescription": "Denies the unminimize command without any pre-configured scope." + }, + { + "description": "This permission set configures which\nnotification features are by default exposed.\n\n#### Granted Permissions\n\nIt allows all notification related features.\n\n\n#### This default permission set includes:\n\n- `allow-is-permission-granted`\n- `allow-request-permission`\n- `allow-notify`\n- `allow-register-action-types`\n- `allow-register-listener`\n- `allow-cancel`\n- `allow-get-pending`\n- `allow-remove-active`\n- `allow-get-active`\n- `allow-check-permissions`\n- `allow-show`\n- `allow-batch`\n- `allow-list-channels`\n- `allow-delete-channel`\n- `allow-create-channel`\n- `allow-permission-state`", + "type": "string", + "const": "notification:default", + "markdownDescription": "This permission set configures which\nnotification features are by default exposed.\n\n#### Granted Permissions\n\nIt allows all notification related features.\n\n\n#### This default permission set includes:\n\n- `allow-is-permission-granted`\n- `allow-request-permission`\n- `allow-notify`\n- `allow-register-action-types`\n- `allow-register-listener`\n- `allow-cancel`\n- `allow-get-pending`\n- `allow-remove-active`\n- `allow-get-active`\n- `allow-check-permissions`\n- `allow-show`\n- `allow-batch`\n- `allow-list-channels`\n- `allow-delete-channel`\n- `allow-create-channel`\n- `allow-permission-state`" + }, + { + "description": "Enables the batch command without any pre-configured scope.", + "type": "string", + "const": "notification:allow-batch", + "markdownDescription": "Enables the batch command without any pre-configured scope." + }, + { + "description": "Enables the cancel command without any pre-configured scope.", + "type": "string", + "const": "notification:allow-cancel", + "markdownDescription": "Enables the cancel command without any pre-configured scope." + }, + { + "description": "Enables the check_permissions command without any pre-configured scope.", + "type": "string", + "const": "notification:allow-check-permissions", + "markdownDescription": "Enables the check_permissions command without any pre-configured scope." + }, + { + "description": "Enables the create_channel command without any pre-configured scope.", + "type": "string", + "const": "notification:allow-create-channel", + "markdownDescription": "Enables the create_channel command without any pre-configured scope." + }, + { + "description": "Enables the delete_channel command without any pre-configured scope.", + "type": "string", + "const": "notification:allow-delete-channel", + "markdownDescription": "Enables the delete_channel command without any pre-configured scope." + }, + { + "description": "Enables the get_active command without any pre-configured scope.", + "type": "string", + "const": "notification:allow-get-active", + "markdownDescription": "Enables the get_active command without any pre-configured scope." + }, + { + "description": "Enables the get_pending command without any pre-configured scope.", + "type": "string", + "const": "notification:allow-get-pending", + "markdownDescription": "Enables the get_pending command without any pre-configured scope." + }, + { + "description": "Enables the is_permission_granted command without any pre-configured scope.", + "type": "string", + "const": "notification:allow-is-permission-granted", + "markdownDescription": "Enables the is_permission_granted command without any pre-configured scope." + }, + { + "description": "Enables the list_channels command without any pre-configured scope.", + "type": "string", + "const": "notification:allow-list-channels", + "markdownDescription": "Enables the list_channels command without any pre-configured scope." + }, + { + "description": "Enables the notify command without any pre-configured scope.", + "type": "string", + "const": "notification:allow-notify", + "markdownDescription": "Enables the notify command without any pre-configured scope." + }, + { + "description": "Enables the permission_state command without any pre-configured scope.", + "type": "string", + "const": "notification:allow-permission-state", + "markdownDescription": "Enables the permission_state command without any pre-configured scope." + }, + { + "description": "Enables the register_action_types command without any pre-configured scope.", + "type": "string", + "const": "notification:allow-register-action-types", + "markdownDescription": "Enables the register_action_types command without any pre-configured scope." + }, + { + "description": "Enables the register_listener command without any pre-configured scope.", + "type": "string", + "const": "notification:allow-register-listener", + "markdownDescription": "Enables the register_listener command without any pre-configured scope." + }, + { + "description": "Enables the remove_active command without any pre-configured scope.", + "type": "string", + "const": "notification:allow-remove-active", + "markdownDescription": "Enables the remove_active command without any pre-configured scope." + }, + { + "description": "Enables the request_permission command without any pre-configured scope.", + "type": "string", + "const": "notification:allow-request-permission", + "markdownDescription": "Enables the request_permission command without any pre-configured scope." + }, + { + "description": "Enables the show command without any pre-configured scope.", + "type": "string", + "const": "notification:allow-show", + "markdownDescription": "Enables the show command without any pre-configured scope." + }, + { + "description": "Denies the batch command without any pre-configured scope.", + "type": "string", + "const": "notification:deny-batch", + "markdownDescription": "Denies the batch command without any pre-configured scope." + }, + { + "description": "Denies the cancel command without any pre-configured scope.", + "type": "string", + "const": "notification:deny-cancel", + "markdownDescription": "Denies the cancel command without any pre-configured scope." + }, + { + "description": "Denies the check_permissions command without any pre-configured scope.", + "type": "string", + "const": "notification:deny-check-permissions", + "markdownDescription": "Denies the check_permissions command without any pre-configured scope." + }, + { + "description": "Denies the create_channel command without any pre-configured scope.", + "type": "string", + "const": "notification:deny-create-channel", + "markdownDescription": "Denies the create_channel command without any pre-configured scope." + }, + { + "description": "Denies the delete_channel command without any pre-configured scope.", + "type": "string", + "const": "notification:deny-delete-channel", + "markdownDescription": "Denies the delete_channel command without any pre-configured scope." + }, + { + "description": "Denies the get_active command without any pre-configured scope.", + "type": "string", + "const": "notification:deny-get-active", + "markdownDescription": "Denies the get_active command without any pre-configured scope." + }, + { + "description": "Denies the get_pending command without any pre-configured scope.", + "type": "string", + "const": "notification:deny-get-pending", + "markdownDescription": "Denies the get_pending command without any pre-configured scope." + }, + { + "description": "Denies the is_permission_granted command without any pre-configured scope.", + "type": "string", + "const": "notification:deny-is-permission-granted", + "markdownDescription": "Denies the is_permission_granted command without any pre-configured scope." + }, + { + "description": "Denies the list_channels command without any pre-configured scope.", + "type": "string", + "const": "notification:deny-list-channels", + "markdownDescription": "Denies the list_channels command without any pre-configured scope." + }, + { + "description": "Denies the notify command without any pre-configured scope.", + "type": "string", + "const": "notification:deny-notify", + "markdownDescription": "Denies the notify command without any pre-configured scope." + }, + { + "description": "Denies the permission_state command without any pre-configured scope.", + "type": "string", + "const": "notification:deny-permission-state", + "markdownDescription": "Denies the permission_state command without any pre-configured scope." + }, + { + "description": "Denies the register_action_types command without any pre-configured scope.", + "type": "string", + "const": "notification:deny-register-action-types", + "markdownDescription": "Denies the register_action_types command without any pre-configured scope." + }, + { + "description": "Denies the register_listener command without any pre-configured scope.", + "type": "string", + "const": "notification:deny-register-listener", + "markdownDescription": "Denies the register_listener command without any pre-configured scope." + }, + { + "description": "Denies the remove_active command without any pre-configured scope.", + "type": "string", + "const": "notification:deny-remove-active", + "markdownDescription": "Denies the remove_active command without any pre-configured scope." + }, + { + "description": "Denies the request_permission command without any pre-configured scope.", + "type": "string", + "const": "notification:deny-request-permission", + "markdownDescription": "Denies the request_permission command without any pre-configured scope." + }, + { + "description": "Denies the show command without any pre-configured scope.", + "type": "string", + "const": "notification:deny-show", + "markdownDescription": "Denies the show command without any pre-configured scope." + }, + { + "description": "This permission set allows opening `mailto:`, `tel:`, `https://` and `http://` urls using their default application\nas well as reveal file in directories using default file explorer\n#### This default permission set includes:\n\n- `allow-open-url`\n- `allow-reveal-item-in-dir`\n- `allow-default-urls`", + "type": "string", + "const": "opener:default", + "markdownDescription": "This permission set allows opening `mailto:`, `tel:`, `https://` and `http://` urls using their default application\nas well as reveal file in directories using default file explorer\n#### This default permission set includes:\n\n- `allow-open-url`\n- `allow-reveal-item-in-dir`\n- `allow-default-urls`" + }, + { + "description": "This enables opening `mailto:`, `tel:`, `https://` and `http://` urls using their default application.", + "type": "string", + "const": "opener:allow-default-urls", + "markdownDescription": "This enables opening `mailto:`, `tel:`, `https://` and `http://` urls using their default application." + }, + { + "description": "Enables the open_path command without any pre-configured scope.", + "type": "string", + "const": "opener:allow-open-path", + "markdownDescription": "Enables the open_path command without any pre-configured scope." + }, + { + "description": "Enables the open_url command without any pre-configured scope.", + "type": "string", + "const": "opener:allow-open-url", + "markdownDescription": "Enables the open_url command without any pre-configured scope." + }, + { + "description": "Enables the reveal_item_in_dir command without any pre-configured scope.", + "type": "string", + "const": "opener:allow-reveal-item-in-dir", + "markdownDescription": "Enables the reveal_item_in_dir command without any pre-configured scope." + }, + { + "description": "Denies the open_path command without any pre-configured scope.", + "type": "string", + "const": "opener:deny-open-path", + "markdownDescription": "Denies the open_path command without any pre-configured scope." + }, + { + "description": "Denies the open_url command without any pre-configured scope.", + "type": "string", + "const": "opener:deny-open-url", + "markdownDescription": "Denies the open_url command without any pre-configured scope." + }, + { + "description": "Denies the reveal_item_in_dir command without any pre-configured scope.", + "type": "string", + "const": "opener:deny-reveal-item-in-dir", + "markdownDescription": "Denies the reveal_item_in_dir command without any pre-configured scope." + }, + { + "description": "This permission set configures which kind of\nupdater functions are exposed to the frontend.\n\n#### Granted Permissions\n\nThe full workflow from checking for updates to installing them\nis enabled.\n\n\n#### This default permission set includes:\n\n- `allow-check`\n- `allow-download`\n- `allow-install`\n- `allow-download-and-install`", + "type": "string", + "const": "updater:default", + "markdownDescription": "This permission set configures which kind of\nupdater functions are exposed to the frontend.\n\n#### Granted Permissions\n\nThe full workflow from checking for updates to installing them\nis enabled.\n\n\n#### This default permission set includes:\n\n- `allow-check`\n- `allow-download`\n- `allow-install`\n- `allow-download-and-install`" + }, + { + "description": "Enables the check command without any pre-configured scope.", + "type": "string", + "const": "updater:allow-check", + "markdownDescription": "Enables the check command without any pre-configured scope." + }, + { + "description": "Enables the download command without any pre-configured scope.", + "type": "string", + "const": "updater:allow-download", + "markdownDescription": "Enables the download command without any pre-configured scope." + }, + { + "description": "Enables the download_and_install command without any pre-configured scope.", + "type": "string", + "const": "updater:allow-download-and-install", + "markdownDescription": "Enables the download_and_install command without any pre-configured scope." + }, + { + "description": "Enables the install command without any pre-configured scope.", + "type": "string", + "const": "updater:allow-install", + "markdownDescription": "Enables the install command without any pre-configured scope." + }, + { + "description": "Denies the check command without any pre-configured scope.", + "type": "string", + "const": "updater:deny-check", + "markdownDescription": "Denies the check command without any pre-configured scope." + }, + { + "description": "Denies the download command without any pre-configured scope.", + "type": "string", + "const": "updater:deny-download", + "markdownDescription": "Denies the download command without any pre-configured scope." + }, + { + "description": "Denies the download_and_install command without any pre-configured scope.", + "type": "string", + "const": "updater:deny-download-and-install", + "markdownDescription": "Denies the download_and_install command without any pre-configured scope." + }, + { + "description": "Denies the install command without any pre-configured scope.", + "type": "string", + "const": "updater:deny-install", + "markdownDescription": "Denies the install command without any pre-configured scope." } ] }, @@ -2215,6 +2743,23 @@ ] } ] + }, + "Application": { + "description": "Opener scope application.", + "anyOf": [ + { + "description": "Open in default application.", + "type": "null" + }, + { + "description": "If true, allow open with any application.", + "type": "boolean" + }, + { + "description": "Allow specific application to open with.", + "type": "string" + } + ] } } } \ No newline at end of file diff --git a/apps/desktop/src-tauri/permissions/autogenerated/check_for_updates.toml b/apps/desktop/src-tauri/permissions/autogenerated/check_for_updates.toml new file mode 100644 index 0000000000..f24151fec5 --- /dev/null +++ b/apps/desktop/src-tauri/permissions/autogenerated/check_for_updates.toml @@ -0,0 +1,11 @@ +# Automatically generated - DO NOT EDIT! + +[[permission]] +identifier = "allow-check-for-updates" +description = "Enables the check_for_updates command without any pre-configured scope." +commands.allow = ["check_for_updates"] + +[[permission]] +identifier = "deny-check-for-updates" +description = "Denies the check_for_updates command without any pre-configured scope." +commands.deny = ["check_for_updates"] diff --git a/apps/desktop/src-tauri/permissions/autogenerated/get_update_state.toml b/apps/desktop/src-tauri/permissions/autogenerated/get_update_state.toml new file mode 100644 index 0000000000..0136b3b8d5 --- /dev/null +++ b/apps/desktop/src-tauri/permissions/autogenerated/get_update_state.toml @@ -0,0 +1,11 @@ +# Automatically generated - DO NOT EDIT! + +[[permission]] +identifier = "allow-get-update-state" +description = "Enables the get_update_state command without any pre-configured scope." +commands.allow = ["get_update_state"] + +[[permission]] +identifier = "deny-get-update-state" +description = "Denies the get_update_state command without any pre-configured scope." +commands.deny = ["get_update_state"] diff --git a/apps/desktop/src-tauri/permissions/autogenerated/install_update.toml b/apps/desktop/src-tauri/permissions/autogenerated/install_update.toml new file mode 100644 index 0000000000..e82a75384d --- /dev/null +++ b/apps/desktop/src-tauri/permissions/autogenerated/install_update.toml @@ -0,0 +1,11 @@ +# Automatically generated - DO NOT EDIT! + +[[permission]] +identifier = "allow-install-update" +description = "Enables the install_update command without any pre-configured scope." +commands.allow = ["install_update"] + +[[permission]] +identifier = "deny-install-update" +description = "Denies the install_update command without any pre-configured scope." +commands.deny = ["install_update"] diff --git a/apps/desktop/src-tauri/permissions/autogenerated/open_external_url.toml b/apps/desktop/src-tauri/permissions/autogenerated/open_external_url.toml new file mode 100644 index 0000000000..3a89010201 --- /dev/null +++ b/apps/desktop/src-tauri/permissions/autogenerated/open_external_url.toml @@ -0,0 +1,11 @@ +# Automatically generated - DO NOT EDIT! + +[[permission]] +identifier = "allow-open-external-url" +description = "Enables the open_external_url command without any pre-configured scope." +commands.allow = ["open_external_url"] + +[[permission]] +identifier = "deny-open-external-url" +description = "Denies the open_external_url command without any pre-configured scope." +commands.deny = ["open_external_url"] diff --git a/apps/desktop/src-tauri/permissions/autogenerated/show_native_notification.toml b/apps/desktop/src-tauri/permissions/autogenerated/show_native_notification.toml new file mode 100644 index 0000000000..40e1eb83a8 --- /dev/null +++ b/apps/desktop/src-tauri/permissions/autogenerated/show_native_notification.toml @@ -0,0 +1,11 @@ +# Automatically generated - DO NOT EDIT! + +[[permission]] +identifier = "allow-show-native-notification" +description = "Enables the show_native_notification command without any pre-configured scope." +commands.allow = ["show_native_notification"] + +[[permission]] +identifier = "deny-show-native-notification" +description = "Denies the show_native_notification command without any pre-configured scope." +commands.deny = ["show_native_notification"] diff --git a/apps/desktop/src-tauri/src/backend.rs b/apps/desktop/src-tauri/src/backend.rs index b8b7ef2be7..15a19a26ea 100644 --- a/apps/desktop/src-tauri/src/backend.rs +++ b/apps/desktop/src-tauri/src/backend.rs @@ -13,6 +13,7 @@ use std::{ thread, time::{Duration, Instant}, }; +use url::Url; #[cfg(feature = "desktop-runtime")] use tauri::{AppHandle, Manager, WebviewWindow}; @@ -22,6 +23,7 @@ const LOOPBACK_HOST: &str = "127.0.0.1"; const DEFAULT_DESKTOP_PORT: u16 = 38430; const DESKTOP_PORT_ENV: &str = "KANDEV_DESKTOP_PORT"; const DESKTOP_HEALTH_TOKEN_ENV: &str = "KANDEV_DESKTOP_HEALTH_TOKEN"; +const DESKTOP_NATIVE_NOTIFICATIONS_ENV: &str = "KANDEV_DESKTOP_NATIVE_NOTIFICATIONS"; const DESKTOP_HEALTH_TOKEN_HEADER: &str = "x-kandev-desktop-health-token"; const STARTUP_OUTPUT_LIMIT: usize = 12 * 1024; const HEALTH_READY_SETTLE: Duration = Duration::from_millis(100); @@ -37,6 +39,7 @@ pub struct BackendState { child: Arc>>, startup_output: Arc>, shutdown_started: Arc, + owned_origin: Arc>>, } impl Default for BackendState { @@ -45,6 +48,7 @@ impl Default for BackendState { child: Arc::new(Mutex::new(None)), startup_output: Arc::new(Mutex::new(StartupOutput::default())), shutdown_started: Arc::new(AtomicBool::new(false)), + owned_origin: Arc::new(Mutex::new(None)), } } } @@ -56,7 +60,12 @@ impl BackendState { pub fn stop(&self) { self.shutdown_started.store(true, Ordering::SeqCst); - let child = self.child.lock().expect("backend child mutex poisoned").take(); + self.clear_owned_origin(); + let child = self + .child + .lock() + .expect("backend child mutex poisoned") + .take(); if let Some(mut child) = child { terminate_child(&mut child); } @@ -66,6 +75,55 @@ impl BackendState { self.shutdown_started.load(Ordering::SeqCst) } + pub fn set_owned_origin(&self, backend_url: &str) -> Result<(), String> { + let origin = loopback_origin(backend_url)?; + *self + .owned_origin + .lock() + .expect("desktop origin mutex poisoned") = Some(origin); + Ok(()) + } + + pub fn accepts_url(&self, url: &str) -> bool { + let expected = self + .owned_origin + .lock() + .expect("desktop origin mutex poisoned") + .clone(); + expected.is_some_and(|origin| same_origin(&origin, url)) + } + + #[cfg(feature = "desktop-runtime")] + pub fn require_owned_origin(&self, webview: &WebviewWindow) -> Result<(), String> { + if !self.has_live_child() { + self.clear_owned_origin(); + return Err("desktop backend is no longer running".to_string()); + } + let url = webview + .url() + .map_err(|err| format!("could not read desktop WebView URL: {err}"))?; + if self.accepts_url(url.as_str()) { + Ok(()) + } else { + Err("desktop command is only available from the Kandev backend".to_string()) + } + } + + fn clear_owned_origin(&self) { + *self + .owned_origin + .lock() + .expect("desktop origin mutex poisoned") = None; + } + + fn has_live_child(&self) -> bool { + self.child + .lock() + .expect("backend child mutex poisoned") + .as_mut() + .is_some_and(|child| matches!(child.try_wait(), Ok(None))) + } + fn set_child(&self, mut child: Child) -> bool { let mut guard = self.child.lock().expect("backend child mutex poisoned"); if self.is_shutdown_started() { @@ -160,7 +218,11 @@ pub fn start_desktop_backend(app: AppHandle, window: WebviewWindow) { ); match launch_and_wait(&app, &state) { Ok(url) => { - if let Err(err) = navigate_to_backend(&window, &url) { + if let Err(err) = state + .set_owned_origin(&url) + .and_then(|_| navigate_to_backend(&window, &url)) + { + state.clear_owned_origin(); state.stop(); set_status( &window, @@ -168,6 +230,8 @@ pub fn start_desktop_backend(app: AppHandle, window: WebviewWindow) { &format!("Backend started, but the window could not navigate: {err}"), true, ); + } else { + crate::updater::start_automatic_checks(app); } } Err(err) => { @@ -178,6 +242,23 @@ pub fn start_desktop_backend(app: AppHandle, window: WebviewWindow) { }); } +fn loopback_origin(input: &str) -> Result { + let url = Url::parse(input).map_err(|err| format!("invalid desktop backend URL: {err}"))?; + if url.scheme() != "http" + || url.host_str() != Some(LOOPBACK_HOST) + || url.port_or_known_default().is_none() + { + return Err("desktop backend URL must use a loopback HTTP origin".to_string()); + } + Ok(url.origin().ascii_serialization()) +} + +fn same_origin(expected: &str, input: &str) -> bool { + Url::parse(input) + .map(|url| url.origin().ascii_serialization() == expected) + .unwrap_or(false) +} + #[cfg(feature = "desktop-runtime")] fn launch_and_wait(app: &AppHandle, state: &BackendState) -> Result { let runtime_dir = resolve_runtime_dir(app)?; @@ -228,7 +309,12 @@ pub fn build_backend_command( let program = runtime_dir.join("bin").join(executable_name("kandev")); let cwd = program .parent() - .ok_or_else(|| format!("Kandev launcher has no parent directory: {}", program.display()))? + .ok_or_else(|| { + format!( + "Kandev launcher has no parent directory: {}", + program.display() + ) + })? .to_path_buf(); Ok(BackendCommandSpec { program, @@ -244,8 +330,14 @@ pub fn build_backend_command( pub fn validate_runtime_dir(runtime_dir: &Path) -> Result<(), String> { let bin_dir = runtime_dir.join("bin"); - require_runtime_file(&bin_dir.join(executable_name("kandev")), "Kandev launcher binary")?; - require_runtime_file(&bin_dir.join(executable_name("agentctl")), "agentctl binary")?; + require_runtime_file( + &bin_dir.join(executable_name("kandev")), + "Kandev launcher binary", + )?; + require_runtime_file( + &bin_dir.join(executable_name("agentctl")), + "agentctl binary", + )?; for &(name, label) in REMOTE_AGENTCTL_HELPERS.iter() { require_runtime_file(&bin_dir.join(name), label)?; } @@ -266,6 +358,11 @@ pub fn desktop_environment( home_dir: Option<&Path>, ) -> BTreeMap { let path = normalized_path(env.get(OsStr::new("PATH")), home_dir); + env.retain(|key, _| { + !key + .to_string_lossy() + .eq_ignore_ascii_case(DESKTOP_NATIVE_NOTIFICATIONS_ENV) + }); env.insert( OsString::from("KANDEV_SERVER_HOST"), OsString::from(LOOPBACK_HOST), @@ -274,6 +371,10 @@ pub fn desktop_environment( OsString::from("KANDEV_BUNDLE_DIR"), runtime_dir.as_os_str().to_os_string(), ); + env.insert( + OsString::from(DESKTOP_NATIVE_NOTIFICATIONS_ENV), + OsString::from("true"), + ); env.insert(OsString::from("PATH"), path); env } @@ -319,7 +420,9 @@ fn preferred_desktop_port(value: Option) -> Result { .parse::() .map_err(|_| format!("{DESKTOP_PORT_ENV} must be a TCP port between 1 and 65535"))?; if port == 0 { - Err(format!("{DESKTOP_PORT_ENV} must be a TCP port between 1 and 65535")) + Err(format!( + "{DESKTOP_PORT_ENV} must be a TCP port between 1 and 65535" + )) } else { Ok(port) } @@ -332,7 +435,9 @@ fn pick_available_loopback_port(preferred: u16) -> Result { .map(|addr| addr.port()) .map_err(|err| err.to_string()), Err(err) if err.kind() == ErrorKind::AddrInUse => pick_loopback_port(), - Err(err) => Err(format!("Could not reserve {LOOPBACK_HOST}:{preferred}: {err}")), + Err(err) => Err(format!( + "Could not reserve {LOOPBACK_HOST}:{preferred}: {err}" + )), } } @@ -395,7 +500,9 @@ fn wait_for_backend( return Err("Desktop startup cancelled".to_string()); } if let Some(message) = state.child_exit_message()? { - return Err(format!("Kandev launcher exited before /health became ready ({message})")); + return Err(format!( + "Kandev launcher exited before /health became ready ({message})" + )); } if health_ready(port, expected_health_token) { thread::sleep(HEALTH_READY_SETTLE); @@ -646,7 +753,10 @@ mod tests { ) .expect("command spec"); - assert_eq!(spec.program, runtime_dir.join("bin").join(executable_name("kandev"))); + assert_eq!( + spec.program, + runtime_dir.join("bin").join(executable_name("kandev")) + ); assert_eq!( spec.args, vec![ @@ -655,7 +765,10 @@ mod tests { OsString::from("48123"), ] ); - assert_eq!(spec.env.get(OsStr::new("CUSTOM_ENV")), Some(&OsString::from("kept"))); + assert_eq!( + spec.env.get(OsStr::new("CUSTOM_ENV")), + Some(&OsString::from("kept")) + ); assert_eq!( spec.env.get(OsStr::new("KANDEV_SERVER_HOST")), Some(&OsString::from(LOOPBACK_HOST)) @@ -668,6 +781,11 @@ mod tests { spec.env.get(OsStr::new(DESKTOP_HEALTH_TOKEN_ENV)), Some(&OsString::from("health-token")) ); + assert_eq!( + spec.env + .get(OsStr::new("KANDEV_DESKTOP_NATIVE_NOTIFICATIONS")), + Some(&OsString::from("true")) + ); } #[test] @@ -686,11 +804,28 @@ mod tests { assert!(entries.contains(&home_dir.join(".local/bin"))); } + #[test] + fn desktop_environment_replaces_notification_flag_case_insensitively() { + let mut inherited = BTreeMap::new(); + inherited.insert( + OsString::from("kandev_desktop_native_notifications"), + OsString::from("false"), + ); + + let env = desktop_environment(Path::new("/opt/kandev"), inherited, None); + + assert_eq!( + env.get(OsStr::new(DESKTOP_NATIVE_NOTIFICATIONS_ENV)), + Some(&OsString::from("true")) + ); + assert!(!env.contains_key(OsStr::new("kandev_desktop_native_notifications"))); + } + #[test] fn missing_launcher_returns_readable_error() { let dir = temp_root("missing-launcher"); - let err = - build_backend_command(&dir, 48123, BTreeMap::new(), None).expect_err("missing launcher"); + let err = build_backend_command(&dir, 48123, BTreeMap::new(), None) + .expect_err("missing launcher"); assert!(err.contains("Kandev launcher binary is missing"), "{err}"); } @@ -701,8 +836,8 @@ mod tests { fs::create_dir_all(&bin).expect("create bin"); fs::write(bin.join(executable_name("kandev")), b"stub").expect("write launcher"); - let err = - build_backend_command(&dir, 48123, BTreeMap::new(), None).expect_err("missing agentctl"); + let err = build_backend_command(&dir, 48123, BTreeMap::new(), None) + .expect_err("missing agentctl"); assert!(err.contains("agentctl binary is missing"), "{err}"); } @@ -718,7 +853,10 @@ mod tests { let err = build_backend_command(&dir, 48123, BTreeMap::new(), None).expect_err("missing helper"); - assert!(err.contains("agentctl linux/amd64 helper is missing"), "{err}"); + assert!( + err.contains("agentctl linux/amd64 helper is missing"), + "{err}" + ); } #[test] @@ -735,7 +873,10 @@ mod tests { let err = build_backend_command(&dir, 48123, BTreeMap::new(), None).expect_err("missing helper"); - assert!(err.contains("agentctl darwin/arm64 helper is missing"), "{err}"); + assert!( + err.contains("agentctl darwin/arm64 helper is missing"), + "{err}" + ); } #[test] @@ -815,6 +956,38 @@ mod tests { assert!(state.is_shutdown_started()); } + #[test] + fn desktop_commands_require_the_exact_owned_backend_origin() { + let state = BackendState::default(); + state + .set_owned_origin("http://127.0.0.1:38430") + .expect("set owned backend origin"); + + assert!(state.accepts_url("http://127.0.0.1:38430/settings")); + assert!(!state.accepts_url("http://127.0.0.1:38431/settings")); + assert!(!state.accepts_url("http://localhost:38430/settings")); + assert!(!state.accepts_url("https://127.0.0.1:38430/settings")); + } + + #[test] + fn desktop_commands_are_denied_before_backend_origin_is_set() { + assert!(!BackendState::default().accepts_url("http://127.0.0.1:38430")); + } + + #[cfg(unix)] + #[test] + fn live_child_is_recognized_as_running() { + let state = BackendState::default(); + let child = Command::new("sh") + .args(["-c", "sleep 1"]) + .spawn() + .expect("start child"); + + assert!(state.set_child(child)); + assert!(state.has_live_child()); + state.stop(); + } + #[test] fn launcher_exit_message_includes_recent_output() { let message = launcher_exit_message("exit status: 1", Some("database failed".to_string())); @@ -838,7 +1011,11 @@ mod tests { fn capture_stream_retries_interrupted_reads() { let output = Arc::new(Mutex::new(StartupOutput::default())); - capture_stream("stdout", InterruptedThenData::new(b"backend ready"), output.clone()); + capture_stream( + "stdout", + InterruptedThenData::new(b"backend ready"), + output.clone(), + ); let deadline = Instant::now() + Duration::from_secs(1); while Instant::now() < deadline { @@ -875,8 +1052,16 @@ mod tests { fn response_header_check_requires_matching_desktop_health_token() { let response = b"HTTP/1.1 200 OK\r\nX-Kandev-Desktop-Health-Token: token\r\n\r\n"; - assert!(response_has_header(response, DESKTOP_HEALTH_TOKEN_HEADER, "token")); - assert!(!response_has_header(response, DESKTOP_HEALTH_TOKEN_HEADER, "other-token")); + assert!(response_has_header( + response, + DESKTOP_HEALTH_TOKEN_HEADER, + "token" + )); + assert!(!response_has_header( + response, + DESKTOP_HEALTH_TOKEN_HEADER, + "other-token" + )); assert!(!response_has_header( b"HTTP/1.1 200 OK\r\n\r\n", DESKTOP_HEALTH_TOKEN_HEADER, @@ -901,7 +1086,10 @@ mod tests { assert!(state.set_child(child)); state.stop(); - assert!(!process_exists(pid), "backend child {pid} should be terminated"); + assert!( + !process_exists(pid), + "backend child {pid} should be terminated" + ); } #[cfg(unix)] @@ -921,7 +1109,10 @@ mod tests { state.stop(); assert!(!state.set_child(child)); - assert!(!process_exists(pid), "backend child {pid} should be terminated"); + assert!( + !process_exists(pid), + "backend child {pid} should be terminated" + ); } fn temp_runtime_dir(name: &str) -> PathBuf { @@ -937,10 +1128,7 @@ mod tests { } fn temp_root(name: &str) -> PathBuf { - let dir = env::temp_dir().join(format!( - "kandev-desktop-{name}-{}", - std::process::id() - )); + let dir = env::temp_dir().join(format!("kandev-desktop-{name}-{}", std::process::id())); let _ = fs::remove_dir_all(&dir); fs::create_dir_all(&dir).expect("create temp root"); dir diff --git a/apps/desktop/src-tauri/src/bin/updater_signature_verifier.rs b/apps/desktop/src-tauri/src/bin/updater_signature_verifier.rs new file mode 100644 index 0000000000..7daad9b757 --- /dev/null +++ b/apps/desktop/src-tauri/src/bin/updater_signature_verifier.rs @@ -0,0 +1,69 @@ +use base64::{engine::general_purpose::STANDARD, Engine}; +use minisign_verify::{PublicKey, Signature}; +use std::{env, fs, path::Path}; + +fn main() { + if let Err(error) = run(env::args().skip(1).collect()) { + eprintln!("{error}"); + std::process::exit(1); + } +} + +fn run(arguments: Vec) -> Result<(), String> { + if arguments.len() < 3 || arguments.len() % 2 == 0 { + return Err( + "usage: updater-signature-verifier [...]" + .to_string(), + ); + } + + let public_key = decode_text(&arguments[0], "updater public key")?; + let public_key = PublicKey::decode(&public_key) + .map_err(|error| format!("invalid updater public key: {error}"))?; + + for pair in arguments[1..].chunks_exact(2) { + verify_pair(&public_key, Path::new(&pair[0]), Path::new(&pair[1]))?; + } + Ok(()) +} + +fn verify_pair( + public_key: &PublicKey, + artifact: &Path, + signature_path: &Path, +) -> Result<(), String> { + let bytes = fs::read(artifact).map_err(|error| { + format!( + "could not read updater artifact {}: {error}", + artifact.display() + ) + })?; + let encoded_signature = fs::read_to_string(signature_path).map_err(|error| { + format!( + "could not read updater signature {}: {error}", + signature_path.display() + ) + })?; + let signature_text = decode_text(encoded_signature.trim(), "updater signature")?; + let signature = Signature::decode(&signature_text).map_err(|error| { + format!( + "invalid updater signature {}: {error}", + signature_path.display() + ) + })?; + public_key + .verify(&bytes, &signature, true) + .map_err(|error| { + format!( + "updater signature does not match embedded public key for {}: {error}", + artifact.display() + ) + }) +} + +fn decode_text(encoded: &str, label: &str) -> Result { + let bytes = STANDARD + .decode(encoded) + .map_err(|error| format!("invalid base64 {label}: {error}"))?; + String::from_utf8(bytes).map_err(|error| format!("invalid UTF-8 {label}: {error}")) +} diff --git a/apps/desktop/src-tauri/src/external_links.rs b/apps/desktop/src-tauri/src/external_links.rs new file mode 100644 index 0000000000..9fed2cf08d --- /dev/null +++ b/apps/desktop/src-tauri/src/external_links.rs @@ -0,0 +1,146 @@ +use url::{Host, Url}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ExternalLinkError { + Empty, + Malformed, + UnsupportedScheme, + CredentialsNotAllowed, + MissingDestination, + LocalDestination, +} + +impl std::fmt::Display for ExternalLinkError { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let message = match self { + Self::Empty => "the URL is empty", + Self::Malformed => "the URL is malformed", + Self::UnsupportedScheme => "the URL scheme is not allowed", + Self::CredentialsNotAllowed => "credentials in URLs are not allowed", + Self::MissingDestination => "the URL has no destination", + Self::LocalDestination => "local URLs must remain inside Kandev", + }; + formatter.write_str(message) + } +} + +pub fn validate_external_url(input: &str) -> Result { + if input.is_empty() || input.trim() != input { + return Err(ExternalLinkError::Empty); + } + + let parsed = Url::parse(input).map_err(|_| ExternalLinkError::Malformed)?; + if !parsed.username().is_empty() || parsed.password().is_some() { + return Err(ExternalLinkError::CredentialsNotAllowed); + } + + match parsed.scheme() { + "http" | "https" => validate_web_url(parsed), + "mailto" if !parsed.path().is_empty() => Ok(parsed), + "mailto" => Err(ExternalLinkError::MissingDestination), + _ => Err(ExternalLinkError::UnsupportedScheme), + } +} + +fn validate_web_url(parsed: Url) -> Result { + let host = parsed.host().ok_or(ExternalLinkError::MissingDestination)?; + let is_local = match host { + Host::Domain(domain) => { + let domain = domain.trim_end_matches('.'); + domain.eq_ignore_ascii_case("localhost") + || domain + .to_ascii_lowercase() + .strip_suffix(".localhost") + .is_some() + } + Host::Ipv4(address) => address.is_loopback() || address.is_unspecified(), + Host::Ipv6(address) => { + address.is_loopback() + || address.is_unspecified() + || address + .to_ipv4_mapped() + .is_some_and(|mapped| mapped.is_loopback() || mapped.is_unspecified()) + } + }; + if is_local { + Err(ExternalLinkError::LocalDestination) + } else { + Ok(parsed) + } +} + +#[cfg(feature = "desktop-runtime")] +pub fn open_validated_external_url(app: &tauri::AppHandle, input: &str) -> Result<(), String> { + use tauri_plugin_opener::OpenerExt; + + let url = validate_external_url(input).map_err(|error| error.to_string())?; + app.opener() + .open_url(url.as_str(), None::<&str>) + .map_err(|error| format!("failed to open external URL: {error}")) +} + +#[cfg(feature = "desktop-runtime")] +#[tauri::command] +pub fn open_external_url( + app: tauri::AppHandle, + backend: tauri::State<'_, crate::backend::BackendState>, + webview: tauri::WebviewWindow, + url: String, +) -> Result<(), String> { + backend.require_owned_origin(&webview)?; + open_validated_external_url(&app, &url) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn permits_external_web_and_mail_destinations() { + assert!(validate_external_url("https://github.com/kdlbs/kandev").is_ok()); + assert!(validate_external_url("http://example.com/docs?q=desktop").is_ok()); + assert!(validate_external_url("mailto:support@example.com?subject=Kandev").is_ok()); + } + + #[test] + fn rejects_unsupported_malformed_and_credentialed_destinations() { + assert_eq!( + validate_external_url("javascript:alert(1)"), + Err(ExternalLinkError::UnsupportedScheme) + ); + assert_eq!( + validate_external_url("https://user:secret@example.com"), + Err(ExternalLinkError::CredentialsNotAllowed) + ); + assert_eq!( + validate_external_url("not a URL"), + Err(ExternalLinkError::Malformed) + ); + assert_eq!( + validate_external_url("mailto:"), + Err(ExternalLinkError::MissingDestination) + ); + } + + #[test] + fn keeps_local_web_destinations_inside_kandev() { + for input in [ + "http://localhost:8080/task", + "http://preview.localhost:3000", + "http://localhost.:8080", + "http://preview.localhost.:3000", + "http://127.0.0.1:4242", + "http://127.9.8.7", + "http://[::1]:4000", + "http://[::ffff:127.0.0.1]:4000", + "http://0.0.0.0:8080", + "http://[::]:8080", + ] { + assert_eq!( + validate_external_url(input), + Err(ExternalLinkError::LocalDestination), + "{input}" + ); + } + } +} diff --git a/apps/desktop/src-tauri/src/lib.rs b/apps/desktop/src-tauri/src/lib.rs index fceb14190a..31d5d3c39d 100644 --- a/apps/desktop/src-tauri/src/lib.rs +++ b/apps/desktop/src-tauri/src/lib.rs @@ -1 +1,6 @@ pub mod backend; +pub mod external_links; +pub mod native_notifications; +pub mod shell; +pub mod updater; +pub mod window_state; diff --git a/apps/desktop/src-tauri/src/main.rs b/apps/desktop/src-tauri/src/main.rs index 2262c8a40f..587e162319 100644 --- a/apps/desktop/src-tauri/src/main.rs +++ b/apps/desktop/src-tauri/src/main.rs @@ -1,41 +1,87 @@ -use kandev_desktop::backend; +use kandev_desktop::{ + backend, external_links, + native_notifications::{self, NativeNotificationState}, + shell::{self, MenuAction, ZoomState}, + updater::{self, UpdaterState}, + window_state::WindowStateStore, +}; use std::thread; -use tauri::{Manager, RunEvent, WindowEvent}; +use tauri::{ + menu::{Menu, MenuItemBuilder, PredefinedMenuItem, Submenu}, + Emitter, Manager, RunEvent, WindowEvent, +}; + +const MAIN_WINDOW_LABEL: &str = "main"; +const WINDOW_STATE_FILE: &str = "window-state-v1.json"; +#[cfg(target_os = "macos")] +const FULLSCREEN_ACCELERATOR: &str = "Ctrl+Cmd+F"; +#[cfg(not(target_os = "macos"))] +const FULLSCREEN_ACCELERATOR: &str = "F11"; fn main() { let app = tauri::Builder::default() + .plugin(tauri_plugin_updater::Builder::new().build()) + .plugin(tauri_plugin_notification::init()) + .plugin( + tauri_plugin_opener::Builder::new() + .open_js_links_on_click(false) + .build(), + ) .plugin(tauri_plugin_single_instance::init(|app, _argv, _cwd| { - if let Some(window) = app.get_webview_window("main") { - let _ = window.show(); - let _ = window.set_focus(); - } + activate_main_window(app); })) .manage(backend::BackendState::default()) + .manage(UpdaterState::new(env!("CARGO_PKG_VERSION"))) + .manage(NativeNotificationState::default()) + .manage(ZoomState::default()) + .invoke_handler(tauri::generate_handler![ + updater::get_update_state, + updater::check_for_updates, + updater::install_update, + native_notifications::show_native_notification, + external_links::open_external_url, + ]) + .menu(build_menu) + .on_menu_event(handle_menu_event) .setup(|app| { let window = app - .get_webview_window("main") + .get_webview_window(MAIN_WINDOW_LABEL) .expect("main window should exist"); + let state_path = app.path().app_data_dir()?.join(WINDOW_STATE_FILE); + let window_state = WindowStateStore::new(state_path); + if let Err(err) = window_state.restore(&window) { + eprintln!("Could not restore desktop window state: {err}"); + } + if let Err(err) = window_state.save(&window) { + eprintln!("Could not initialize desktop window state: {err}"); + } + app.manage(window_state); + window.show()?; backend::start_desktop_backend(app.handle().clone(), window); Ok(()) }) - .on_window_event(|window, event| { - if let WindowEvent::CloseRequested { api, .. } = event { - let state = window.app_handle().state::().inner().clone(); - if state.begin_shutdown() { - api.prevent_close(); - let window = window.clone(); - thread::spawn(move || { - state.stop(); - let _ = window.close(); - }); + .on_window_event(|window, event| match event { + WindowEvent::Moved(_) | WindowEvent::Resized(_) => { + if let Some(webview) = window.app_handle().get_webview_window(window.label()) { + if let Some(state) = window.try_state::() { + if let Err(err) = state.schedule_save(&webview) { + eprintln!("Could not schedule desktop window state: {err}"); + } + } } } + WindowEvent::CloseRequested { api, .. } => { + if shutdown_and_exit(window.app_handle()) { + api.prevent_close(); + } + } + _ => {} }) .build(tauri::generate_context!()) .expect("error while building Kandev desktop app"); - app.run(|app_handle, event| { - if let RunEvent::ExitRequested { api, .. } = event { + app.run(|app_handle, event| match event { + RunEvent::ExitRequested { code, api, .. } if code.is_none() => { let state = app_handle.state::().inner().clone(); if state.begin_shutdown() { api.prevent_exit(); @@ -46,5 +92,231 @@ fn main() { }); } } + #[cfg(target_os = "macos")] + RunEvent::Reopen { .. } => activate_main_window(app_handle), + _ => {} }); } + +fn build_menu(app: &tauri::AppHandle) -> tauri::Result> { + let settings = MenuItemBuilder::with_id(shell::MENU_SETTINGS, "Settings...") + .accelerator("CmdOrCtrl+Comma") + .build(app)?; + let check_updates = + MenuItemBuilder::with_id(shell::MENU_CHECK_FOR_UPDATES, "Check for Updates...") + .build(app)?; + let quit = MenuItemBuilder::with_id(shell::MENU_QUIT, "Quit Kandev") + .accelerator("CmdOrCtrl+KeyQ") + .build(app)?; + let app_menu = build_application_menu(app, &settings, &check_updates, &quit)?; + + let new_task = MenuItemBuilder::with_id(shell::MENU_NEW_TASK, "New Task") + .accelerator("CmdOrCtrl+KeyN") + .build(app)?; + let close_context = MenuItemBuilder::with_id(shell::MENU_CLOSE_CONTEXT, "Close Context") + .accelerator("CmdOrCtrl+KeyW") + .build(app)?; + let file_menu = Submenu::with_items(app, "File", true, &[&new_task, &close_context])?; + + let edit_menu = Submenu::with_items( + app, + "Edit", + true, + &[ + &PredefinedMenuItem::undo(app, None)?, + &PredefinedMenuItem::redo(app, None)?, + &PredefinedMenuItem::separator(app)?, + &PredefinedMenuItem::cut(app, None)?, + &PredefinedMenuItem::copy(app, None)?, + &PredefinedMenuItem::paste(app, None)?, + &PredefinedMenuItem::select_all(app, None)?, + ], + )?; + + let zoom_in = MenuItemBuilder::with_id(shell::MENU_ZOOM_IN, "Zoom In") + .accelerator("CmdOrCtrl+Shift+Equal") + .build(app)?; + let zoom_in_equals = MenuItemBuilder::with_id(shell::MENU_ZOOM_IN_EQUALS, "Zoom In (=)") + .accelerator("CmdOrCtrl+Equal") + .build(app)?; + let zoom_out = MenuItemBuilder::with_id(shell::MENU_ZOOM_OUT, "Zoom Out") + .accelerator("CmdOrCtrl+Minus") + .build(app)?; + let actual_size = MenuItemBuilder::with_id(shell::MENU_ZOOM_RESET, "Actual Size") + .accelerator("CmdOrCtrl+Digit0") + .build(app)?; + let fullscreen = MenuItemBuilder::with_id(shell::MENU_FULLSCREEN, "Toggle Full Screen") + .accelerator(FULLSCREEN_ACCELERATOR) + .build(app)?; + let view_menu = Submenu::with_items( + app, + "View", + true, + &[ + &zoom_in, + &zoom_in_equals, + &zoom_out, + &actual_size, + &PredefinedMenuItem::separator(app)?, + &fullscreen, + ], + )?; + + let window_menu = Submenu::with_items( + app, + "Window", + true, + &[ + &PredefinedMenuItem::minimize(app, None)?, + &PredefinedMenuItem::maximize(app, None)?, + ], + )?; + + let docs = + MenuItemBuilder::with_id(shell::MENU_HELP_DOCS, "Kandev Documentation").build(app)?; + let repository = + MenuItemBuilder::with_id(shell::MENU_HELP_REPOSITORY, "Kandev Repository").build(app)?; + let releases = + MenuItemBuilder::with_id(shell::MENU_HELP_RELEASES, "Release Notes").build(app)?; + let help_menu = Submenu::with_items(app, "Help", true, &[&docs, &repository, &releases])?; + + Menu::with_items( + app, + &[ + &app_menu, + &file_menu, + &edit_menu, + &view_menu, + &window_menu, + &help_menu, + ], + ) +} + +#[cfg(target_os = "macos")] +fn build_application_menu( + app: &tauri::AppHandle, + settings: &tauri::menu::MenuItem, + check_updates: &tauri::menu::MenuItem, + quit: &tauri::menu::MenuItem, +) -> tauri::Result> { + Submenu::with_items( + app, + "Kandev", + true, + &[ + &PredefinedMenuItem::about(app, None, None)?, + &PredefinedMenuItem::separator(app)?, + settings, + check_updates, + &PredefinedMenuItem::separator(app)?, + &PredefinedMenuItem::services(app, None)?, + &PredefinedMenuItem::separator(app)?, + &PredefinedMenuItem::hide(app, None)?, + &PredefinedMenuItem::hide_others(app, None)?, + &PredefinedMenuItem::separator(app)?, + quit, + ], + ) +} + +#[cfg(not(target_os = "macos"))] +fn build_application_menu( + app: &tauri::AppHandle, + settings: &tauri::menu::MenuItem, + check_updates: &tauri::menu::MenuItem, + quit: &tauri::menu::MenuItem, +) -> tauri::Result> { + Submenu::with_items( + app, + "Kandev", + true, + &[ + &PredefinedMenuItem::about(app, None, None)?, + &PredefinedMenuItem::separator(app)?, + settings, + check_updates, + &PredefinedMenuItem::separator(app)?, + quit, + ], + ) +} + +fn handle_menu_event(app: &tauri::AppHandle, event: tauri::menu::MenuEvent) { + let Some(action) = shell::menu_action(event.id().as_ref()) else { + return; + }; + match action { + MenuAction::Emit(event_name) => { + focus_main_window(app); + if let Some(window) = app.get_webview_window(MAIN_WINDOW_LABEL) { + let _ = window.emit(event_name, ()); + } + } + MenuAction::ZoomIn | MenuAction::ZoomOut | MenuAction::ZoomReset => { + apply_zoom(app, action); + } + MenuAction::Fullscreen => { + if let Some(window) = app.get_webview_window(MAIN_WINDOW_LABEL) { + if let Ok(fullscreen) = window.is_fullscreen() { + let _ = window.set_fullscreen(!fullscreen); + } + } + } + MenuAction::Quit => { + let _ = shutdown_and_exit(app); + } + MenuAction::HelpDocs => open_help_url(app, "https://github.com/kdlbs/kandev#readme"), + MenuAction::HelpRepository => open_help_url(app, "https://github.com/kdlbs/kandev"), + MenuAction::HelpReleases => open_help_url(app, "https://github.com/kdlbs/kandev/releases"), + } +} + +fn open_help_url(app: &tauri::AppHandle, url: &str) { + if let Err(error) = external_links::open_validated_external_url(app, url) { + eprintln!("Could not open desktop help URL: {error}"); + } +} + +fn apply_zoom(app: &tauri::AppHandle, action: MenuAction) { + let Some(window) = app.get_webview_window(MAIN_WINDOW_LABEL) else { + return; + }; + let zoom = app.state::(); + let Some(level) = zoom.preview(action) else { + return; + }; + if window.set_zoom(level).is_ok() { + zoom.commit(level); + } +} + +fn focus_main_window(app: &tauri::AppHandle) { + if let Some(window) = app.get_webview_window(MAIN_WINDOW_LABEL) { + let _ = window.show(); + let _ = window.unminimize(); + let _ = window.set_focus(); + } +} + +fn activate_main_window(app: &tauri::AppHandle) { + focus_main_window(app); +} + +fn shutdown_and_exit(app: &tauri::AppHandle) -> bool { + let state = app.state::().inner().clone(); + if !state.begin_shutdown() { + return false; + } + if let Some(window) = app.get_webview_window(MAIN_WINDOW_LABEL) { + if let Some(window_state) = app.try_state::() { + let _ = window_state.save(&window); + } + } + let app = app.clone(); + thread::spawn(move || { + state.stop(); + app.exit(0); + }); + true +} diff --git a/apps/desktop/src-tauri/src/native_notifications.rs b/apps/desktop/src-tauri/src/native_notifications.rs new file mode 100644 index 0000000000..7ae3fe7073 --- /dev/null +++ b/apps/desktop/src-tauri/src/native_notifications.rs @@ -0,0 +1,225 @@ +use serde::{Deserialize, Serialize}; +use std::{ + collections::VecDeque, + sync::{ + atomic::{AtomicBool, Ordering}, + Mutex, + }, +}; + +const MAX_SEEN_EVENT_IDS: usize = 1_024; + +#[derive(Debug, Clone, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct NativeNotificationRequest { + pub event_id: String, + pub title: String, + pub body: String, + pub task_id: String, + pub session_id: Option, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)] +#[serde(rename_all = "kebab-case")] +pub enum NativeNotificationResult { + Shown, + Duplicate, + PermissionDenied, +} + +#[derive(Debug, Default)] +pub struct NativeNotificationState { + seen_event_ids: Mutex>, + permission_prompt_attempted: AtomicBool, +} + +impl NativeNotificationState { + fn claim(&self, event_id: &str) -> bool { + let mut seen = self + .seen_event_ids + .lock() + .expect("notification identity mutex poisoned"); + if seen.iter().any(|seen_id| seen_id == event_id) { + return false; + } + if seen.len() == MAX_SEEN_EVENT_IDS { + seen.pop_front(); + } + seen.push_back(event_id.to_string()); + true + } + + fn release(&self, event_id: &str) { + self.seen_event_ids + .lock() + .expect("notification identity mutex poisoned") + .retain(|seen_id| seen_id != event_id); + } + + fn claim_after_permission( + &self, + request: &NativeNotificationRequest, + permission_granted: bool, + ) -> NativeNotificationResult { + if !permission_granted { + return NativeNotificationResult::PermissionDenied; + } + if self.claim(&request.event_id) { + NativeNotificationResult::Shown + } else { + NativeNotificationResult::Duplicate + } + } + + fn begin_permission_prompt(&self) -> bool { + !self + .permission_prompt_attempted + .swap(true, Ordering::SeqCst) + } + + fn allow_permission_prompt_retry(&self) { + self.permission_prompt_attempted + .store(false, Ordering::SeqCst); + } +} + +fn validate_request(request: &NativeNotificationRequest) -> Result<(), String> { + if !request.event_id.starts_with("session.waiting_for_input:") + && !request.event_id.starts_with("session.failed:") + { + return Err("unsupported native notification event".to_string()); + } + if request.event_id.len() > 256 + || request.title.is_empty() + || request.title.len() > 160 + || request.body.len() > 1000 + || request.task_id.is_empty() + || request.task_id.len() > 256 + || request.session_id.as_ref().is_some_and(|id| id.len() > 256) + { + return Err("invalid native notification payload".to_string()); + } + Ok(()) +} + +#[cfg(feature = "desktop-runtime")] +#[tauri::command] +pub fn show_native_notification( + app: tauri::AppHandle, + state: tauri::State<'_, NativeNotificationState>, + backend: tauri::State<'_, crate::backend::BackendState>, + webview: tauri::WebviewWindow, + request: NativeNotificationRequest, +) -> Result { + use tauri::plugin::PermissionState; + use tauri_plugin_notification::NotificationExt; + + backend.require_owned_origin(&webview)?; + validate_request(&request)?; + let mut permission = app + .notification() + .permission_state() + .map_err(|err| err.to_string())?; + if permission != PermissionState::Granted + && permission != PermissionState::Denied + && state.begin_permission_prompt() + { + permission = app.notification().request_permission().map_err(|err| { + state.allow_permission_prompt_retry(); + err.to_string() + })?; + } + let claim_result = + state.claim_after_permission(&request, permission == PermissionState::Granted); + if claim_result != NativeNotificationResult::Shown { + return Ok(claim_result); + } + let show_result = app + .notification() + .builder() + .title(&request.title) + .body(&request.body) + .show(); + if let Err(err) = show_result { + state.release(&request.event_id); + return Err(err.to_string()); + } + Ok(NativeNotificationResult::Shown) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn request(event_id: &str, task_id: &str) -> NativeNotificationRequest { + NativeNotificationRequest { + event_id: event_id.to_string(), + title: "Task needs your input".to_string(), + body: "Waiting".to_string(), + task_id: task_id.to_string(), + session_id: Some("session-1".to_string()), + } + } + + #[test] + fn event_identity_is_delivered_at_most_once() { + let state = NativeNotificationState::default(); + let request = request("session.waiting_for_input:session-1", "task-1"); + + assert!(state.claim(&request.event_id)); + assert!(!state.claim(&request.event_id)); + } + + #[test] + fn event_identity_cache_is_bounded() { + let state = NativeNotificationState::default(); + + for index in 0..=MAX_SEEN_EVENT_IDS { + assert!(state.claim(&format!("session.failed:{index}"))); + } + + assert!(state.claim("session.failed:0")); + } + + #[test] + fn bridge_rejects_events_outside_the_two_notification_types() { + let request = request("office.inbox_item:item-1", "task-1"); + + assert_eq!( + validate_request(&request), + Err("unsupported native notification event".to_string()) + ); + } + + #[test] + fn denied_delivery_does_not_claim_the_event() { + let state = NativeNotificationState::default(); + let request = request("session.failed:session-1", "task-1"); + + assert_eq!( + state.claim_after_permission(&request, false), + NativeNotificationResult::PermissionDenied + ); + assert_eq!( + state.claim_after_permission(&request, true), + NativeNotificationResult::Shown + ); + } + + #[test] + fn permission_prompt_is_attempted_only_once_per_process() { + let state = NativeNotificationState::default(); + + assert!(state.begin_permission_prompt()); + assert!(!state.begin_permission_prompt()); + } + + #[test] + fn permission_prompt_can_retry_after_a_transient_error() { + let state = NativeNotificationState::default(); + + assert!(state.begin_permission_prompt()); + state.allow_permission_prompt_retry(); + assert!(state.begin_permission_prompt()); + } +} diff --git a/apps/desktop/src-tauri/src/shell.rs b/apps/desktop/src-tauri/src/shell.rs new file mode 100644 index 0000000000..9875c63448 --- /dev/null +++ b/apps/desktop/src-tauri/src/shell.rs @@ -0,0 +1,158 @@ +use std::sync::Mutex; + +pub const DESKTOP_EVENT_PREFIX: &str = "kandev-desktop-v1-"; +pub const CLOSE_CONTEXT_EVENT: &str = "kandev-desktop-v1-close-context"; +pub const OPEN_SETTINGS_EVENT: &str = "kandev-desktop-v1-open-settings"; +pub const NEW_TASK_EVENT: &str = "kandev-desktop-v1-new-task"; +pub const CHECK_FOR_UPDATES_EVENT: &str = "kandev-desktop-v1-check-for-updates"; + +pub const MENU_SETTINGS: &str = "desktop.v1.open-settings"; +pub const MENU_NEW_TASK: &str = "desktop.v1.new-task"; +pub const MENU_CLOSE_CONTEXT: &str = "desktop.v1.close-context"; +pub const MENU_CHECK_FOR_UPDATES: &str = "desktop.v1.check-for-updates"; +pub const MENU_ZOOM_IN: &str = "desktop.v1.zoom-in"; +pub const MENU_ZOOM_IN_EQUALS: &str = "desktop.v1.zoom-in-equals"; +pub const MENU_ZOOM_OUT: &str = "desktop.v1.zoom-out"; +pub const MENU_ZOOM_RESET: &str = "desktop.v1.zoom-reset"; +pub const MENU_FULLSCREEN: &str = "desktop.v1.fullscreen"; +pub const MENU_QUIT: &str = "desktop.v1.quit"; +pub const MENU_HELP_DOCS: &str = "desktop.v1.help-docs"; +pub const MENU_HELP_REPOSITORY: &str = "desktop.v1.help-repository"; +pub const MENU_HELP_RELEASES: &str = "desktop.v1.help-releases"; + +const DEFAULT_ZOOM: f64 = 1.0; +const ZOOM_STEP: f64 = 0.1; +const MIN_ZOOM: f64 = 0.5; +const MAX_ZOOM: f64 = 2.0; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum MenuAction { + Emit(&'static str), + ZoomIn, + ZoomOut, + ZoomReset, + Fullscreen, + Quit, + HelpDocs, + HelpRepository, + HelpReleases, +} + +pub fn menu_action(id: &str) -> Option { + match id { + MENU_SETTINGS => Some(MenuAction::Emit(OPEN_SETTINGS_EVENT)), + MENU_NEW_TASK => Some(MenuAction::Emit(NEW_TASK_EVENT)), + MENU_CLOSE_CONTEXT => Some(MenuAction::Emit(CLOSE_CONTEXT_EVENT)), + MENU_CHECK_FOR_UPDATES => Some(MenuAction::Emit(CHECK_FOR_UPDATES_EVENT)), + MENU_ZOOM_IN | MENU_ZOOM_IN_EQUALS => Some(MenuAction::ZoomIn), + MENU_ZOOM_OUT => Some(MenuAction::ZoomOut), + MENU_ZOOM_RESET => Some(MenuAction::ZoomReset), + MENU_FULLSCREEN => Some(MenuAction::Fullscreen), + MENU_QUIT => Some(MenuAction::Quit), + MENU_HELP_DOCS => Some(MenuAction::HelpDocs), + MENU_HELP_REPOSITORY => Some(MenuAction::HelpRepository), + MENU_HELP_RELEASES => Some(MenuAction::HelpReleases), + _ => None, + } +} + +#[derive(Debug)] +pub struct ZoomState { + level: Mutex, +} + +impl Default for ZoomState { + fn default() -> Self { + Self { + level: Mutex::new(DEFAULT_ZOOM), + } + } +} + +impl ZoomState { + pub fn current(&self) -> f64 { + *self.level.lock().expect("zoom state mutex poisoned") + } + + pub fn preview(&self, action: MenuAction) -> Option { + let current = self.current(); + match action { + MenuAction::ZoomIn => Some(clamp_zoom(current + ZOOM_STEP)), + MenuAction::ZoomOut => Some(clamp_zoom(current - ZOOM_STEP)), + MenuAction::ZoomReset => Some(DEFAULT_ZOOM), + _ => None, + } + } + + pub fn commit(&self, level: f64) { + *self.level.lock().expect("zoom state mutex poisoned") = clamp_zoom(level); + } +} + +fn clamp_zoom(level: f64) -> f64 { + level.clamp(MIN_ZOOM, MAX_ZOOM) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn contextual_close_maps_only_to_the_versioned_bridge_event() { + assert_eq!( + menu_action(MENU_CLOSE_CONTEXT), + Some(MenuAction::Emit(CLOSE_CONTEXT_EVENT)) + ); + assert!(CLOSE_CONTEXT_EVENT.starts_with(DESKTOP_EVENT_PREFIX)); + } + + #[test] + fn native_commands_map_to_frozen_v1_events() { + assert_eq!(CLOSE_CONTEXT_EVENT, "kandev-desktop-v1-close-context"); + assert_eq!(OPEN_SETTINGS_EVENT, "kandev-desktop-v1-open-settings"); + assert_eq!(NEW_TASK_EVENT, "kandev-desktop-v1-new-task"); + assert_eq!( + CHECK_FOR_UPDATES_EVENT, + "kandev-desktop-v1-check-for-updates" + ); + assert_eq!( + menu_action(MENU_SETTINGS), + Some(MenuAction::Emit(OPEN_SETTINGS_EVENT)) + ); + assert_eq!( + menu_action(MENU_NEW_TASK), + Some(MenuAction::Emit(NEW_TASK_EVENT)) + ); + assert_eq!( + menu_action(MENU_CHECK_FOR_UPDATES), + Some(MenuAction::Emit(CHECK_FOR_UPDATES_EVENT)) + ); + } + + #[test] + fn zoom_clamps_and_reset_restores_actual_size() { + let state = ZoomState::default(); + + for _ in 0..30 { + let level = state.preview(MenuAction::ZoomIn).unwrap(); + state.commit(level); + } + assert_eq!(state.current(), MAX_ZOOM); + + for _ in 0..30 { + let level = state.preview(MenuAction::ZoomOut).unwrap(); + state.commit(level); + } + assert_eq!(state.current(), MIN_ZOOM); + + state.commit(state.preview(MenuAction::ZoomReset).unwrap()); + assert_eq!(state.current(), DEFAULT_ZOOM); + } + + #[test] + fn failed_zoom_can_leave_the_previous_level_uncommitted() { + let state = ZoomState::default(); + assert_eq!(state.preview(MenuAction::ZoomIn), Some(1.1)); + assert_eq!(state.current(), DEFAULT_ZOOM); + } +} diff --git a/apps/desktop/src-tauri/src/updater.rs b/apps/desktop/src-tauri/src/updater.rs new file mode 100644 index 0000000000..529588e7cc --- /dev/null +++ b/apps/desktop/src-tauri/src/updater.rs @@ -0,0 +1,690 @@ +use serde::Serialize; +use std::{path::Path, sync::Mutex, time::Duration}; + +#[cfg(feature = "desktop-runtime")] +use crate::backend::BackendState; +#[cfg(feature = "desktop-runtime")] +use std::time::{Instant, SystemTime, UNIX_EPOCH}; +#[cfg(feature = "desktop-runtime")] +use tauri::{AppHandle, Manager, State, WebviewWindow}; +#[cfg(feature = "desktop-runtime")] +use tauri_plugin_updater::{Update, UpdaterExt}; + +pub const AUTOMATIC_CHECK_INTERVAL: Duration = Duration::from_secs(24 * 60 * 60); +const LINUX_INSTALL_UNSUPPORTED: &str = "Automatic update installation is available only when Kandev is running from an AppImage. For .deb or .rpm installations, download the latest package from Release notes and update it with your package manager."; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)] +#[serde(rename_all = "kebab-case")] +pub enum UpdatePhase { + Idle, + Checking, + Available, + UpToDate, + Downloading, + Installing, + Error, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct UpdateSnapshot { + pub phase: UpdatePhase, + pub current_version: String, + pub latest_version: Option, + pub release_notes: Option, + pub release_url: Option, + pub checked_at_epoch_ms: Option, + pub downloaded_bytes: Option, + pub total_bytes: Option, + pub install_supported: bool, + pub install_unsupported_reason: Option, + pub error: Option, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +struct InstallSupport { + supported: bool, + unsupported_reason: Option, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum UpdatePlatform { + Linux, + MacOs, + Windows, + Other, +} + +struct InstallEnvironment<'a> { + appimage: Option<&'a Path>, + appdir: Option<&'a Path>, + current_exe: Option<&'a Path>, + temp_dir: &'a Path, + appimage_is_file: bool, +} + +impl InstallSupport { + fn supported() -> Self { + Self { + supported: true, + unsupported_reason: None, + } + } + + fn unsupported(reason: impl Into) -> Self { + Self { + supported: false, + unsupported_reason: Some(reason.into()), + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct AvailableUpdate { + pub version: String, + pub release_notes: Option, + pub release_url: Option, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum Operation { + Check, + Install, +} + +#[derive(Debug)] +struct Machine { + snapshot: UpdateSnapshot, + operation: Option, + last_automatic_check: Option, +} + +pub struct UpdaterState { + machine: Mutex, + #[cfg(feature = "desktop-runtime")] + pending: Mutex>, +} + +impl UpdaterState { + pub fn new(current_version: impl Into) -> Self { + Self::with_install_support(current_version, native_install_support()) + } + + fn with_install_support( + current_version: impl Into, + install_support: InstallSupport, + ) -> Self { + Self { + machine: Mutex::new(Machine { + snapshot: UpdateSnapshot { + phase: UpdatePhase::Idle, + current_version: current_version.into(), + latest_version: None, + release_notes: None, + release_url: None, + checked_at_epoch_ms: None, + downloaded_bytes: None, + total_bytes: None, + install_supported: install_support.supported, + install_unsupported_reason: install_support.unsupported_reason, + error: None, + }, + operation: None, + last_automatic_check: None, + }), + #[cfg(feature = "desktop-runtime")] + pending: Mutex::new(None), + } + } + + pub fn should_run_automatic_check(&self, elapsed: Duration) -> bool { + let mut machine = self.machine.lock().expect("updater state mutex poisoned"); + let due = machine + .last_automatic_check + .map(|last| elapsed.saturating_sub(last) >= AUTOMATIC_CHECK_INTERVAL) + .unwrap_or(true); + if due { + machine.last_automatic_check = Some(elapsed); + } + due + } + + pub fn begin_check(&self) -> Result<(), String> { + let mut machine = self.machine.lock().expect("updater state mutex poisoned"); + ensure_idle(&machine)?; + machine.operation = Some(Operation::Check); + machine.snapshot.phase = UpdatePhase::Checking; + machine.snapshot.error = None; + machine.snapshot.downloaded_bytes = None; + machine.snapshot.total_bytes = None; + Ok(()) + } + + pub fn begin_install(&self) -> Result<(), String> { + let mut machine = self.machine.lock().expect("updater state mutex poisoned"); + ensure_idle(&machine)?; + if machine.snapshot.phase != UpdatePhase::Available { + return Err("No desktop update is ready to install.".to_string()); + } + if !machine.snapshot.install_supported { + return Err(machine + .snapshot + .install_unsupported_reason + .clone() + .unwrap_or_else(|| "Automatic update installation is unavailable.".to_string())); + } + machine.operation = Some(Operation::Install); + machine.snapshot.phase = UpdatePhase::Downloading; + machine.snapshot.error = None; + machine.snapshot.downloaded_bytes = Some(0); + machine.snapshot.total_bytes = None; + Ok(()) + } + + pub fn snapshot(&self) -> UpdateSnapshot { + self.machine + .lock() + .expect("updater state mutex poisoned") + .snapshot + .clone() + } + + pub fn complete_check(&self, update: Option, checked_at_epoch_ms: u64) { + let mut machine = self.machine.lock().expect("updater state mutex poisoned"); + machine.operation = None; + machine.snapshot.checked_at_epoch_ms = Some(checked_at_epoch_ms); + machine.snapshot.downloaded_bytes = None; + machine.snapshot.total_bytes = None; + machine.snapshot.error = None; + match update { + Some(update) => { + machine.snapshot.phase = UpdatePhase::Available; + machine.snapshot.latest_version = Some(update.version); + machine.snapshot.release_notes = update.release_notes; + machine.snapshot.release_url = update.release_url; + } + None => { + machine.snapshot.phase = UpdatePhase::UpToDate; + machine.snapshot.latest_version = None; + machine.snapshot.release_notes = None; + machine.snapshot.release_url = None; + } + } + } + + pub fn update_download_progress(&self, chunk_bytes: usize, total_bytes: Option) { + let mut machine = self.machine.lock().expect("updater state mutex poisoned"); + let downloaded = machine.snapshot.downloaded_bytes.unwrap_or(0); + machine.snapshot.downloaded_bytes = Some(downloaded.saturating_add(chunk_bytes as u64)); + if total_bytes.is_some() { + machine.snapshot.total_bytes = total_bytes; + } + } + + pub fn mark_installing(&self) { + self.machine + .lock() + .expect("updater state mutex poisoned") + .snapshot + .phase = UpdatePhase::Installing; + } + + pub fn fail_operation(&self, message: impl Into) { + let mut machine = self.machine.lock().expect("updater state mutex poisoned"); + let operation = machine.operation.take(); + machine.snapshot.phase = if machine.snapshot.latest_version.is_some() + && matches!(operation, Some(Operation::Check | Operation::Install)) + { + UpdatePhase::Available + } else { + UpdatePhase::Error + }; + machine.snapshot.error = Some(message.into()); + } +} + +fn native_install_support() -> InstallSupport { + let appimage = std::env::var_os("APPIMAGE").map(std::path::PathBuf::from); + let appdir = std::env::var_os("APPDIR").map(std::path::PathBuf::from); + let current_exe = std::env::current_exe().ok(); + let temp_dir = std::env::temp_dir(); + let environment = InstallEnvironment { + appimage: appimage.as_deref(), + appdir: appdir.as_deref(), + current_exe: current_exe.as_deref(), + temp_dir: &temp_dir, + appimage_is_file: appimage.as_ref().is_some_and(|path| path.is_file()), + }; + install_support_for(current_platform(), environment) +} + +fn current_platform() -> UpdatePlatform { + match std::env::consts::OS { + "linux" => UpdatePlatform::Linux, + "macos" => UpdatePlatform::MacOs, + "windows" => UpdatePlatform::Windows, + _ => UpdatePlatform::Other, + } +} + +fn install_support_for( + platform: UpdatePlatform, + environment: InstallEnvironment<'_>, +) -> InstallSupport { + match platform { + UpdatePlatform::MacOs | UpdatePlatform::Windows => InstallSupport::supported(), + UpdatePlatform::Linux if is_validated_appimage(&environment) => InstallSupport::supported(), + UpdatePlatform::Linux => InstallSupport::unsupported(LINUX_INSTALL_UNSUPPORTED), + UpdatePlatform::Other => InstallSupport::unsupported( + "Automatic update installation is not supported on this operating system.", + ), + } +} + +fn is_validated_appimage(environment: &InstallEnvironment<'_>) -> bool { + let (Some(appimage), Some(appdir), Some(current_exe)) = ( + environment.appimage, + environment.appdir, + environment.current_exe, + ) else { + return false; + }; + appimage.is_absolute() + && environment.appimage_is_file + && appdir.is_absolute() + && appdir.starts_with(environment.temp_dir) + && appdir + .file_name() + .is_some_and(|name| name.to_string_lossy().starts_with(".mount_")) + && current_exe.starts_with(appdir) +} + +#[cfg(feature = "desktop-runtime")] +impl UpdaterState { + fn set_pending(&self, update: Option) { + *self.pending.lock().expect("pending update mutex poisoned") = update; + } + + fn pending(&self) -> Option { + self.pending + .lock() + .expect("pending update mutex poisoned") + .clone() + } +} + +fn ensure_idle(machine: &Machine) -> Result<(), String> { + if machine.operation.is_some() { + Err("Another update operation is already in progress.".to_string()) + } else { + Ok(()) + } +} + +pub trait BackendShutdown { + fn stop_owned_backend(&self); +} + +pub fn prepare_updater_restart(backend: &impl BackendShutdown) { + backend.stop_owned_backend(); +} + +#[cfg(feature = "desktop-runtime")] +impl BackendShutdown for BackendState { + fn stop_owned_backend(&self) { + self.stop(); + } +} + +#[cfg(feature = "desktop-runtime")] +#[tauri::command] +pub fn get_update_state( + state: State<'_, UpdaterState>, + backend: State<'_, BackendState>, + webview: WebviewWindow, +) -> Result { + backend.require_owned_origin(&webview)?; + Ok(state.snapshot()) +} + +#[cfg(feature = "desktop-runtime")] +#[tauri::command] +pub async fn check_for_updates( + app: AppHandle, + state: State<'_, UpdaterState>, + backend: State<'_, BackendState>, + webview: WebviewWindow, +) -> Result { + backend.require_owned_origin(&webview)?; + run_check(&app, &state).await +} + +#[cfg(feature = "desktop-runtime")] +#[tauri::command] +pub async fn install_update( + app: AppHandle, + state: State<'_, UpdaterState>, + backend: State<'_, BackendState>, + webview: WebviewWindow, +) -> Result { + backend.require_owned_origin(&webview)?; + let update = state + .pending() + .ok_or_else(|| "No desktop update is ready to install.".to_string())?; + state.begin_install()?; + let download_result = update + .download_and_install( + |chunk_bytes, total_bytes| { + state.update_download_progress(chunk_bytes, total_bytes); + }, + || state.mark_installing(), + ) + .await; + if let Err(err) = download_result { + let message = format!("Desktop update installation failed: {err}"); + state.fail_operation(&message); + return Err(message); + } + + let backend = app.state::().inner().clone(); + prepare_updater_restart(&backend); + app.restart(); +} + +#[cfg(feature = "desktop-runtime")] +pub fn start_automatic_checks(app: AppHandle) { + std::thread::spawn(move || { + let started = Instant::now(); + loop { + let state = app.state::(); + if state.should_run_automatic_check(started.elapsed()) { + let _ = tauri::async_runtime::block_on(run_check(&app, &state)); + } + std::thread::sleep(AUTOMATIC_CHECK_INTERVAL); + } + }); +} + +#[cfg(feature = "desktop-runtime")] +async fn run_check(app: &AppHandle, state: &UpdaterState) -> Result { + state.begin_check()?; + let backend = app.state::().inner().clone(); + let updater = match app + .updater_builder() + .on_before_exit(move || backend.stop()) + .build() + { + Ok(updater) => updater, + Err(err) => return fail_check(state, err), + }; + match updater.check().await { + Ok(update) => { + let metadata = update.as_ref().map(|update| AvailableUpdate { + version: update.version.clone(), + release_notes: update.body.clone(), + release_url: Some(release_url(&update.version)), + }); + state.set_pending(update); + state.complete_check(metadata, now_epoch_ms()); + Ok(state.snapshot()) + } + Err(err) => fail_check(state, err), + } +} + +#[cfg(feature = "desktop-runtime")] +fn fail_check( + state: &UpdaterState, + error: impl std::fmt::Display, +) -> Result { + let message = format!("Desktop update check failed: {error}"); + state.fail_operation(&message); + Err(message) +} + +#[cfg(feature = "desktop-runtime")] +fn release_url(version: &str) -> String { + let tag = if version.starts_with('v') { + version.to_string() + } else { + format!("v{version}") + }; + format!("https://github.com/kdlbs/kandev/releases/tag/{tag}") +} + +#[cfg(feature = "desktop-runtime")] +fn now_epoch_ms() -> u64 { + SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap_or_default() + .as_millis() + .try_into() + .unwrap_or(u64::MAX) +} + +#[cfg(test)] +mod tests { + use super::*; + use std::sync::atomic::{AtomicUsize, Ordering}; + + fn install_environment<'a>( + appimage: Option<&'a Path>, + appdir: Option<&'a Path>, + current_exe: Option<&'a Path>, + appimage_is_file: bool, + ) -> InstallEnvironment<'a> { + InstallEnvironment { + appimage, + appdir, + current_exe, + temp_dir: Path::new("/tmp"), + appimage_is_file, + } + } + + fn supported_updater_state() -> UpdaterState { + UpdaterState::with_install_support("1.0.0", InstallSupport::supported()) + } + + #[test] + fn automatic_checks_run_immediately_then_no_more_than_once_per_day() { + let state = UpdaterState::new("1.0.0"); + + assert!(state.should_run_automatic_check(Duration::ZERO)); + assert!( + !state.should_run_automatic_check(AUTOMATIC_CHECK_INTERVAL - Duration::from_secs(1)) + ); + assert!(state.should_run_automatic_check(AUTOMATIC_CHECK_INTERVAL)); + } + + #[test] + fn forged_appimage_environment_is_not_installable() { + let support = install_support_for( + UpdatePlatform::Linux, + install_environment( + Some(Path::new("/opt/Kandev.AppImage")), + Some(Path::new("/tmp/.mount_kandev")), + Some(Path::new("/usr/bin/kandev-desktop")), + false, + ), + ); + + assert!(!support.supported); + } + + #[test] + fn validated_appimage_and_native_desktop_platforms_are_installable() { + let valid_appimage = install_environment( + Some(Path::new("/opt/Kandev.AppImage")), + Some(Path::new("/tmp/.mount_kandev")), + Some(Path::new("/tmp/.mount_kandev/usr/bin/kandev-desktop")), + true, + ); + + assert!(install_support_for(UpdatePlatform::Linux, valid_appimage).supported); + assert!( + install_support_for( + UpdatePlatform::MacOs, + install_environment(None, None, None, false) + ) + .supported + ); + assert!( + install_support_for( + UpdatePlatform::Windows, + install_environment(None, None, None, false) + ) + .supported + ); + } + + #[test] + fn linux_package_install_exposes_manual_update_guidance() { + let support = install_support_for( + UpdatePlatform::Linux, + install_environment(None, None, None, false), + ); + + assert!(!support.supported); + assert!(support + .unsupported_reason + .as_deref() + .unwrap_or_default() + .contains("package manager")); + } + + #[test] + fn only_one_update_operation_can_be_in_flight() { + let state = UpdaterState::new("1.0.0"); + + state.begin_check().expect("first check should start"); + + assert_eq!( + state.begin_check(), + Err("Another update operation is already in progress.".to_string()) + ); + assert_eq!( + state.begin_install(), + Err("Another update operation is already in progress.".to_string()) + ); + } + + #[test] + fn install_requires_a_confirmed_available_update() { + let state = UpdaterState::new("1.0.0"); + + assert_eq!( + state.begin_install(), + Err("No desktop update is ready to install.".to_string()) + ); + assert_eq!(state.snapshot().phase, UpdatePhase::Idle); + } + + #[cfg(target_os = "linux")] + #[test] + fn install_is_blocked_outside_a_validated_appimage_runtime() { + let state = UpdaterState::new("1.0.0"); + state.begin_check().unwrap(); + state.complete_check( + Some(AvailableUpdate { + version: "1.1.0".to_string(), + release_notes: None, + release_url: None, + }), + 42, + ); + + assert_eq!( + state.begin_install(), + Err(LINUX_INSTALL_UNSUPPORTED.to_string()) + ); + assert_eq!(state.snapshot().phase, UpdatePhase::Available); + } + + #[test] + fn updater_restart_stops_the_owned_backend_once() { + struct FakeBackend(AtomicUsize); + impl BackendShutdown for FakeBackend { + fn stop_owned_backend(&self) { + self.0.fetch_add(1, Ordering::SeqCst); + } + } + let backend = FakeBackend(AtomicUsize::new(0)); + + prepare_updater_restart(&backend); + + assert_eq!(backend.0.load(Ordering::SeqCst), 1); + } + + #[test] + fn successful_checks_publish_available_and_no_update_states() { + let state = supported_updater_state(); + state.begin_check().unwrap(); + state.complete_check( + Some(AvailableUpdate { + version: "1.1.0".to_string(), + release_notes: Some("Changes".to_string()), + release_url: Some("https://example.test/v1.1.0".to_string()), + }), + 42, + ); + + assert_eq!( + state.snapshot(), + UpdateSnapshot { + phase: UpdatePhase::Available, + current_version: "1.0.0".to_string(), + latest_version: Some("1.1.0".to_string()), + release_notes: Some("Changes".to_string()), + release_url: Some("https://example.test/v1.1.0".to_string()), + checked_at_epoch_ms: Some(42), + downloaded_bytes: None, + total_bytes: None, + install_supported: true, + install_unsupported_reason: None, + error: None, + } + ); + + state.begin_check().unwrap(); + state.complete_check(None, 84); + let snapshot = state.snapshot(); + assert_eq!(snapshot.phase, UpdatePhase::UpToDate); + assert_eq!(snapshot.checked_at_epoch_ms, Some(84)); + assert_eq!(snapshot.latest_version, None); + } + + #[test] + fn download_progress_and_failure_preserve_a_retryable_update() { + let state = supported_updater_state(); + state.begin_check().unwrap(); + state.complete_check( + Some(AvailableUpdate { + version: "1.1.0".to_string(), + release_notes: None, + release_url: None, + }), + 42, + ); + state.begin_install().unwrap(); + state.update_download_progress(25, Some(100)); + + assert_eq!(state.snapshot().downloaded_bytes, Some(25)); + assert_eq!(state.snapshot().total_bytes, Some(100)); + + state.mark_installing(); + assert_eq!(state.snapshot().phase, UpdatePhase::Installing); + state.fail_operation("Signature verification failed"); + + let snapshot = state.snapshot(); + assert_eq!(snapshot.phase, UpdatePhase::Available); + assert_eq!(snapshot.latest_version.as_deref(), Some("1.1.0")); + assert_eq!( + snapshot.error.as_deref(), + Some("Signature verification failed") + ); + state + .begin_install() + .expect("failed installs remain retryable"); + } +} diff --git a/apps/desktop/src-tauri/src/window_state.rs b/apps/desktop/src-tauri/src/window_state.rs new file mode 100644 index 0000000000..01f4aa6fd1 --- /dev/null +++ b/apps/desktop/src-tauri/src/window_state.rs @@ -0,0 +1,334 @@ +#[cfg(feature = "desktop-runtime")] +use serde::{Deserialize, Serialize}; +#[cfg(feature = "desktop-runtime")] +use std::{ + fs, + path::PathBuf, + sync::{ + mpsc::{self, Receiver, RecvTimeoutError, Sender}, + Arc, Mutex, + }, + thread, + time::Duration, +}; +#[cfg(feature = "desktop-runtime")] +use tauri::{PhysicalPosition, PhysicalSize, WebviewWindow}; + +const MIN_VISIBLE_WIDTH: i32 = 120; +const MIN_VISIBLE_HEIGHT: i32 = 80; +#[cfg(feature = "desktop-runtime")] +const SAVE_DEBOUNCE: Duration = Duration::from_millis(350); + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[cfg_attr(feature = "desktop-runtime", derive(Serialize, Deserialize))] +pub struct WindowBounds { + pub x: i32, + pub y: i32, + pub width: u32, + pub height: u32, + pub maximized: bool, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct MonitorBounds { + pub x: i32, + pub y: i32, + pub width: u32, + pub height: u32, +} + +pub fn visible_restore(saved: WindowBounds, monitors: &[MonitorBounds]) -> WindowBounds { + if monitors.is_empty() || monitors.iter().any(|monitor| intersects(saved, *monitor)) { + return saved; + } + + let monitor = monitors[0]; + let width = saved.width.max(MIN_VISIBLE_WIDTH as u32).min(monitor.width); + let height = saved + .height + .max(MIN_VISIBLE_HEIGHT as u32) + .min(monitor.height); + WindowBounds { + x: monitor.x, + y: monitor.y, + width, + height, + maximized: saved.maximized, + } +} + +fn intersects(window: WindowBounds, monitor: MonitorBounds) -> bool { + let window_right = window.x.saturating_add(window.width as i32); + let window_bottom = window.y.saturating_add(window.height as i32); + let monitor_right = monitor.x.saturating_add(monitor.width as i32); + let monitor_bottom = monitor.y.saturating_add(monitor.height as i32); + + window_right >= monitor.x.saturating_add(MIN_VISIBLE_WIDTH) + && window.x <= monitor_right.saturating_sub(MIN_VISIBLE_WIDTH) + && window_bottom >= monitor.y.saturating_add(MIN_VISIBLE_HEIGHT) + && window.y <= monitor_bottom.saturating_sub(MIN_VISIBLE_HEIGHT) +} + +#[derive(Debug, Default)] +struct SaveSchedule { + revision: u64, +} + +impl SaveSchedule { + fn next(&mut self) -> u64 { + self.revision = self.revision.wrapping_add(1); + self.revision + } + + fn is_current(&self, revision: u64) -> bool { + self.revision == revision + } +} + +#[cfg(feature = "desktop-runtime")] +pub struct WindowStateStore { + path: PathBuf, + last_normal: Mutex>, + pending: Sender<(u64, WindowBounds)>, + schedule: Arc>, + write_lock: Arc>, +} + +#[cfg(feature = "desktop-runtime")] +impl WindowStateStore { + pub fn new(path: PathBuf) -> Self { + let (pending, receiver) = mpsc::channel(); + let schedule = Arc::new(Mutex::new(SaveSchedule::default())); + let write_lock = Arc::new(Mutex::new(())); + start_save_worker(path.clone(), receiver, schedule.clone(), write_lock.clone()); + Self { + path, + last_normal: Mutex::new(None), + pending, + schedule, + write_lock, + } + } + + pub fn restore(&self, window: &WebviewWindow) -> Result<(), String> { + let saved = match fs::read(&self.path) { + Ok(bytes) => serde_json::from_slice::(&bytes) + .map_err(|err| format!("invalid saved desktop window state: {err}"))?, + Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(()), + Err(err) => return Err(format!("could not read desktop window state: {err}")), + }; + let monitors = window + .available_monitors() + .map_err(|err| err.to_string())? + .into_iter() + .map(|monitor| { + let work_area = monitor.work_area(); + MonitorBounds { + x: work_area.position.x, + y: work_area.position.y, + width: work_area.size.width, + height: work_area.size.height, + } + }) + .collect::>(); + let restored = visible_restore(saved, &monitors); + window + .set_size(PhysicalSize::new(restored.width, restored.height)) + .map_err(|err| err.to_string())?; + window + .set_position(PhysicalPosition::new(restored.x, restored.y)) + .map_err(|err| err.to_string())?; + *self + .last_normal + .lock() + .expect("window state mutex poisoned") = Some(WindowBounds { + maximized: false, + ..restored + }); + if restored.maximized { + window.maximize().map_err(|err| err.to_string())?; + } + Ok(()) + } + + pub fn schedule_save(&self, window: &WebviewWindow) -> Result<(), String> { + let Some(bounds) = self.capture(window)? else { + return Ok(()); + }; + let revision = self + .schedule + .lock() + .expect("window save schedule mutex poisoned") + .next(); + self.pending + .send((revision, bounds)) + .map_err(|_| "desktop window state writer stopped unexpectedly".to_string()) + } + + pub fn save(&self, window: &WebviewWindow) -> Result<(), String> { + let Some(bounds) = self.capture(window)? else { + return Ok(()); + }; + self.schedule + .lock() + .expect("window save schedule mutex poisoned") + .next(); + let _guard = self + .write_lock + .lock() + .expect("window state writer mutex poisoned"); + write_bounds(&self.path, bounds) + } + + fn capture(&self, window: &WebviewWindow) -> Result, String> { + if window.is_minimized().unwrap_or(false) { + return Ok(None); + } + if window.is_fullscreen().unwrap_or(false) { + return Ok(None); + } + + let maximized = window.is_maximized().unwrap_or(false); + let bounds = if maximized { + self.last_normal + .lock() + .expect("window state mutex poisoned") + .map(|bounds| WindowBounds { + maximized: true, + ..bounds + }) + } else { + let position = window.outer_position().map_err(|err| err.to_string())?; + let size = window.outer_size().map_err(|err| err.to_string())?; + let bounds = WindowBounds { + x: position.x, + y: position.y, + width: size.width, + height: size.height, + maximized: false, + }; + *self + .last_normal + .lock() + .expect("window state mutex poisoned") = Some(bounds); + Some(bounds) + }; + Ok(bounds) + } +} + +#[cfg(feature = "desktop-runtime")] +fn start_save_worker( + path: PathBuf, + receiver: Receiver<(u64, WindowBounds)>, + schedule: Arc>, + write_lock: Arc>, +) { + thread::spawn(move || { + while let Ok(mut pending) = receiver.recv() { + loop { + match receiver.recv_timeout(SAVE_DEBOUNCE) { + Ok(next) => pending = next, + Err(RecvTimeoutError::Timeout) => break, + Err(RecvTimeoutError::Disconnected) => return, + } + } + let _guard = write_lock + .lock() + .expect("window state writer mutex poisoned"); + if schedule + .lock() + .expect("window save schedule mutex poisoned") + .is_current(pending.0) + { + if let Err(err) = write_bounds(&path, pending.1) { + eprintln!("Could not persist desktop window state: {err}"); + } + } + } + }); +} + +#[cfg(feature = "desktop-runtime")] +fn write_bounds(path: &PathBuf, bounds: WindowBounds) -> Result<(), String> { + if let Some(parent) = path.parent() { + fs::create_dir_all(parent) + .map_err(|err| format!("could not create desktop state directory: {err}"))?; + } + let bytes = serde_json::to_vec(&bounds) + .map_err(|err| format!("could not serialize desktop window state: {err}"))?; + fs::write(path, bytes).map_err(|err| format!("could not save desktop window state: {err}")) +} + +#[cfg(test)] +mod tests { + use super::*; + + const PRIMARY: MonitorBounds = MonitorBounds { + x: 0, + y: 0, + width: 1920, + height: 1080, + }; + + #[test] + fn visible_saved_bounds_are_preserved() { + let saved = WindowBounds { + x: 100, + y: 80, + width: 1280, + height: 900, + maximized: true, + }; + assert_eq!(visible_restore(saved, &[PRIMARY]), saved); + } + + #[test] + fn disconnected_monitor_bounds_restore_on_the_primary_monitor() { + let saved = WindowBounds { + x: 3000, + y: 200, + width: 1280, + height: 900, + maximized: false, + }; + assert_eq!( + visible_restore(saved, &[PRIMARY]), + WindowBounds { + x: 0, + y: 0, + width: 1280, + height: 900, + maximized: false, + } + ); + } + + #[test] + fn oversized_bounds_are_clamped_to_the_restore_monitor() { + let saved = WindowBounds { + x: -5000, + y: -5000, + width: 4000, + height: 3000, + maximized: false, + }; + let restored = visible_restore(saved, &[PRIMARY]); + assert_eq!(restored.width, PRIMARY.width); + assert_eq!(restored.height, PRIMARY.height); + assert_eq!((restored.x, restored.y), (PRIMARY.x, PRIMARY.y)); + } + + #[test] + fn newer_save_revisions_supersede_pending_work() { + let mut schedule = SaveSchedule::default(); + let first = schedule.next(); + let second = schedule.next(); + + assert!(!schedule.is_current(first)); + assert!(schedule.is_current(second)); + + schedule.next(); + assert!(!schedule.is_current(second)); + } +} diff --git a/apps/desktop/src-tauri/tauri.conf.json b/apps/desktop/src-tauri/tauri.conf.json index ec9ea185f7..5b6c8795c8 100644 --- a/apps/desktop/src-tauri/tauri.conf.json +++ b/apps/desktop/src-tauri/tauri.conf.json @@ -19,6 +19,7 @@ "minWidth": 960, "minHeight": 640, "resizable": true, + "visible": false, "fullscreen": false } ], @@ -29,9 +30,18 @@ "csp": "default-src 'self'; script-src 'self' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self' data:; connect-src 'self' http://127.0.0.1:* ws://127.0.0.1:*; frame-ancestors 'none'" } }, + "plugins": { + "updater": { + "endpoints": [ + "https://github.com/kdlbs/kandev/releases/latest/download/latest.json" + ], + "pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEI0NzVGQ0MwODhDODM0OEMKUldTTU5NaUl3UHgxdENoYmIvRDR1Y3F1Tys2ZXN5MFNRbHRHSHpmeWhIdzIxVFJEVUJubDAwWmwK" + } + }, "bundle": { "active": true, "targets": "all", + "createUpdaterArtifacts": false, "publisher": "Kandev", "homepage": "https://github.com/kdlbs/kandev", "resources": { diff --git a/apps/web/app/office/workspace/settings/export/export-preview.tsx b/apps/web/app/office/workspace/settings/export/export-preview.tsx index a9fbf4ccb0..ea9246132a 100644 --- a/apps/web/app/office/workspace/settings/export/export-preview.tsx +++ b/apps/web/app/office/workspace/settings/export/export-preview.tsx @@ -53,6 +53,7 @@ export function ExportPreview() { const handleExport = useCallback(() => { if (!activeWorkspaceId) return; + // This same-origin endpoint is a file download, not an external navigation. window.open(officeApi.exportConfigZipUrl(activeWorkspaceId), "_blank"); }, [activeWorkspaceId]); diff --git a/apps/web/components/app-sidebar/app-sidebar-new-task-item.test.tsx b/apps/web/components/app-sidebar/app-sidebar-new-task-item.test.tsx index e282066246..2e3693b979 100644 --- a/apps/web/components/app-sidebar/app-sidebar-new-task-item.test.tsx +++ b/apps/web/components/app-sidebar/app-sidebar-new-task-item.test.tsx @@ -1,5 +1,5 @@ import { describe, it, expect, beforeEach, afterEach, vi } from "vitest"; -import { cleanup, render, screen } from "@testing-library/react"; +import { act, cleanup, render, screen } from "@testing-library/react"; import { TooltipProvider } from "@kandev/ui/tooltip"; const mocks = vi.hoisted(() => ({ @@ -51,8 +51,10 @@ vi.mock("@/app/office/components/new-task-dialog", () => ({ })); vi.mock("@/components/task-create-dialog", () => ({ TaskCreateDialog: ({ + open, onSuccess, }: { + open?: boolean; onSuccess?: ( task: { id: string }, mode: "create" | "edit", @@ -62,6 +64,7 @@ vi.mock("@/components/task-create-dialog", () => ({